A Docker-out-of-Docker development environment for building Splunk applications — custom search commands (Python), React UI apps with Dashboard Studio components, and any combination of backend + frontend Splunk apps.
- Custom search commands — Python chunked-protocol commands, modular inputs, REST handlers, alert actions
- React UI apps — built with
@splunk/create, using@splunk/dashboard-coreand Dashboard Studio exported components - Multi-app development — multiple Splunk apps developed and tested simultaneously
- Simultaneous dev + staging — dev on port 8000 (bind-mounted, live reload) and staging on port 18000 (packaged apps mounted from splunk/stage/)
- Python debugging — VSCode attaches to debugpy inside the Splunk container via SA-VSCode add-on
- React debugging — Chrome DevTools via source maps in Splunk Web (webpack watch updates stage/ live)
┌──────────────────────┐ ┌──────────────────────────────────────┐
│ Dev Container │ │ Docker Compose │
│ (tools only) │ │ │
│ Node 22 / Python 3.12 │ splunk-dev (port 8000, 8089) │
│ Docker CLI / Go Task│────▶│ bind-mounted apps, skip-provision │
│ AppInspect / ruff │ │ │
│ debugpy │ │ splunk-staging (port 18000, 18089) │
└──────────────────────┘ │ mounted apps from splunk/stage/ │
│ └──────────────────────────────────────┘
│ ▲
├── splunk/config/apps/ ─────────────┘ (bind-mounted + symlinked)
└── react/ → stage/ symlinked → /opt/splunk/etc/apps/<app>/
.devcontainer/
devcontainer.json # Tools-only container (Node, Python, Docker CLI, Task)
docker-compose.yml # Dev Splunk (target: dev, bind mounts, port 8000)
docker-compose.staging.yml # Staging Splunk (reuses dev image, mounts splunk/stage/, port 18000)
post-create.sh # Tool install + Splunk image build
splunk/
Dockerfile # Multi-stage: base → dev
entrypoint-wrapper.sh # Skip-provision + auto-discover /tmp/apps/*.tgz for SPLUNK_APPS_URL
config/
apps/ # Splunk app source directories (symlinked into Splunk)
splunk-config-dev/ # Dev-mode web.conf (js_no_cache, enableWebDebug, etc.)
<your_app>/ # Created via `task app:create APP_NAME=<your_app>`
bin/ # Python scripts (custom commands, modular inputs)
lib/ # Shared Python libraries
default/ # commands.conf, searchbnf.conf, restmap.conf, app.conf
metadata/
deps.yml # Splunkbase dependencies (MLTK, SA-VSCode, etc.)
stage/ # Built tarballs (.tgz) — gitignored, created on demand
react/ # React/JS source (monorepo via @splunk/create) — created on demand
packages/
main/ # Shared component library (@splunk/main)
<app>/ # Splunk app package (scaffolded by @splunk/create)
src/main/
webapp/pages/ # React page entry points (one bundle per page)
resources/splunk/ # Splunk app config (app.conf, views, nav, templates)
stage/ # Webpack output — a COMPLETE Splunk app dir (gitignored)
webpack.config.js # Outputs to stage/appserver/static/pages/
Taskfile.yml # All automation
.env # Secrets/config — gitignored
splunk.env.example # Template for .env
AGENTS.md # Concise repo summary + windloop hint
ARCHITECTURE.md # This file — full architecture docs
.vscode/launch.json # Debug configs (Python attach, Chrome, React dev server)
tests/e2e/ # E2E test scripts (devcontainer + Splunk lifecycle)
.ref/ # Reference repos (docker-splunk, kveditor) — gitignored
Copy splunk.env.example to .env and fill in your values. .env is gitignored.
cp splunk.env.example .envLOCAL_WORKSPACE_FOLDER is injected automatically via runArgs in devcontainer.json — no manual setup needed. It resolves to the Mac host path of this repo, which is required so docker compose bind mounts point to the correct location on the Docker host.
docker compose resolves relative paths in docker-compose.yml relative to the compose file location on the Mac host — so bind mounts work correctly without any extra configuration. Just run task commands directly from the repo root.
splunk/config/apps/
splunk-config-dev/ # Dev settings (always present, ships with template)
my_custom_app/ # Created via: task app:create APP_NAME=my_custom_app
bin/ # Python scripts (custom commands, modular inputs)
default/
app.conf # [package] id = my_custom_app, [ui] label = my custom app
commands.conf # Custom search command definitions
data/ui/views/ # Dashboard XML views
data/ui/nav/default.xml
metadata/default.meta
APP_NAME is the single identifier that ties everything together:
| Concept | Value | Location |
|---|---|---|
| Folder name | APP_NAME |
splunk/config/apps/<APP_NAME>/ |
Package ID (app.conf [package] id) |
APP_NAME |
Splunkbase, REST API |
UI Label (app.conf [ui] label) |
APP_NAME with underscores → spaces |
Splunk Web |
| React source (if applicable) | APP_NAME |
react/packages/<APP_NAME>/ |
Set APP_NAME in .env to identify the primary app you're developing. All app:*, react:*, and CI/CD commands use it as the default when no explicit APP_NAME= argument is passed.
For React-based apps, @splunk/create outputs a complete Splunk app to react/packages/<APP_NAME>/stage/. In dev, react:link symlinks stage/ directly into Splunk's etc/apps/ — no separate app skeleton needed. react:package builds and packages stage/ as a tgz for staging image bake-in.
Helper apps (like splunk-config-dev) are always present and don't need APP_NAME.
task dev:build-image # build dev Splunk Docker image (first time / Dockerfile change)
task dev:up # start + sync app symlinks (no rebuild)
task dev:refresh # reload configs + static assets via REST API (~2s)
task dev:restartd # restart splunkd process (~10s)
task dev:restart # restart container (skip-provision ~30s)
task dev:reprovision # force full Ansible re-provisioning
task dev:down # stop container
task dev:clean # stop + remove volumes (full reset)
task dev:logs # follow container logs
task dev:status # check container statustask stage:deploy # package + start + install (full pipeline)
task stage:package # package all apps to splunk/stage/
task stage:up # start staging container + health wait
task stage:install # install tgz into running staging
task stage:down # stop staging container
task stage:clean # stop staging + remove volumes
task stage:logs # follow staging container logstask app:create APP_NAME=x # scaffold + symlink + refresh Splunk (no recreate)
task app:package APP_NAME=x # package to splunk/stage/x.tgz (for staging)Which one do I need?
| Situation | Command |
|---|---|
| New app just created, want it visible in Splunk | app:create (calls sync-links automatically) |
Symlinks got out of sync (e.g. after dev:clean) |
dev:ensure-links |
| Preparing a release build for staging | stage:deploy (packages + starts + installs) |
dev:ensure-links— fastest (~1s). Makes the app visible via symlink. Enough for.confedits, dashboards, and Python scripts (no packaging needed). Called automatically bydev:upandapp:create.package— produces a.tgzinsplunk/stage/. Used as input for staging (stage:installinstalls from there). Not needed for day-to-day dev.
task react:create # scaffold + initial build, syncs APP_NAME in .env (interactive)
task react:add-page # add a page/component via @splunk/create (interactive)
task react:link # symlink stage/ into dev Splunk (auto-builds if stage/ missing)
task react:start # webpack watch — stage/ updates live via symlink
task react:build # production webpack build (always rebuilds stage/)
task react:package # build + package stage/ as splunk/stage/<APP_NAME>.tgz for stagingtask deps:install # install Splunkbase deps from deps.yml (idempotent)
task python:lint # ruff check
task python:format # ruff format
task python:test # pytest
task test:lifecycle # Splunk lifecycle tests — 7 suites (any host)
task test:devcontainer # build devcontainer + static checks + lifecycle
task test:all # lint + devcontainer- Open repo in VS Code → "Reopen in Container"
post-create.shinstalls tools and builds the Splunk imagetask dev:up→ starts Splunk, syncs app symlinks (full Ansible provisioning on first boot, ~50s)task deps:install→ install Splunkbase dependencies (SA-VSCode, MLTK, etc.)
task app:create APP_NAME=my_app→ scaffolds app, creates symlink, refreshes Splunk (~2-10s, no container recreation)- App is immediately visible in Splunk
- Edit
.conffiles →task dev:refreshto reload (~2s, no restart)
task react:create→ scaffolds via@splunk/create, detects app name, updates.env, runs initial buildtask react:link→ symlinksstage/into Splunketc/apps/(run once, or afterdev:clean)task react:start→ webpack watch;stage/updates live through the symlinktask react:add-page→ add more pages interactively via@splunk/createtask react:package→ build + packagestage/as tgz for staging
| Change | Command | Time |
|---|---|---|
Edit .conf / dashboard |
task dev:refresh |
~2s |
| Edit Python code | task dev:restartd |
~10s |
| New app | task app:create (symlink + refresh) |
~2-10s |
| Dockerfile change | task dev:build-image then task dev:up |
varies |
| Full reset | task dev:clean then task dev:up |
~90s |
task stage:deploy→ packages all apps, starts staging, installs via CLI- Or manually:
task stage:package→task stage:up→task stage:install
- Or manually:
- For React apps:
task react:packagefirst (puts tgz insplunk/stage/), thentask stage:deploy
The splunk/Dockerfile has two stages:
base— installs acl + entrypoint wrapper on the official Splunk imagedev— entrypoint-wrapper for skip-provision; used by both dev and staging compose files
Staging reuses the dev image. Packaged apps are bind-mounted from splunk/stage/ → /tmp/apps/; the entrypoint auto-discovers and installs them on first start.
Dev and staging run as separate compose projects with different ports:
| Dev | Staging | |
|---|---|---|
| Compose file | docker-compose.yml |
docker-compose.staging.yml |
| Container | splunk-dev |
splunk-staging |
| Web | :8000 |
:18000 |
| REST API | :8089 |
:18089 |
| HEC | :8088 |
:18088 |
| Apps | Bind-mounted (live edit) | Mounted from splunk/stage/ (auto-installed on first start) |
| Entrypoint | entrypoint-wrapper.sh | entrypoint-wrapper.sh (same) |
- First start: No marker → full Ansible → writes
/opt/splunk/var/.provisioned - Subsequent starts: Marker exists → starts splunkd directly (~30s vs ~90s)
- Force reprovision:
task dev:reprovisionremoves marker and restarts - Clean reset:
task dev:cleanremoves volumes (including marker)
The wrapper also writes the Docker healthcheck state file so checkstate.sh passes in skip-provision mode.
The Splunk image is built once during post-create.sh (or explicitly via task dev:build-image). task dev:up only starts the container — it does not rebuild. This avoids unnecessary rebuilds during daily development.
The entire splunk/config/apps/ directory is bind-mounted to /opt/splunk/dev-apps/ inside the container. Symlinks in /opt/splunk/etc/apps/ point to each app under /opt/splunk/dev-apps/. This means:
- No container recreation when adding a new app — just
docker exec ln -s+ refresh - Live editing — file changes on the host are immediately visible through the symlink
task dev:ensure-linksmanages symlinks (creates missing, removes stale)- This pattern is validated by Splunk's own
@splunk/createtooling (yarn run link:app)
| Change type | Action needed |
|---|---|
.conf files, dashboards |
task dev:refresh (~2s, no restart) |
Python code in bin/ |
Re-run the search command (no restart) |
| Python code needing restart | task dev:restartd (~10s) |
React source (src/) |
task react:start (webpack watch → stage/ live via symlink) |
| React production build | task react:package (build + package tgz for staging) |
| Dashboard Studio JSON | Edit definition.json → HMR picks it up |
| New app added | task app:create (symlink + refresh ~2-10s) |
splunk-varvolume — indexed data, KV store, search artifacts, provisioning markersplunk-etcvolume — Splunk config, installed apps, and app symlinks; persists across container recreation/opt/splunk/dev-apps/— bind mount ofsplunk/config/apps/(live source)splunk/stage/— built tarballs mounted to/tmp/appsin the container
task dev:clean removes all volumes for a full reset.
- Scaffold:
task react:create→ runs@splunk/createinteractively, creates monorepo underreact/packages/, syncsAPP_NAMEin.env, runs initial build - Link:
task react:link→ symlinksreact/packages/<app>/stage/into Splunketc/apps/<app>/ - Add dashboard page:
task react:add-page→@splunk/createoffers "Add a Dashboard Page" - Install dashboard packages:
yarn add @splunk/dashboard-core @splunk/dashboard-presets @splunk/dashboard-context(fromreact/) - Dev:
task react:start→ webpack watch; editsrc/→stage/updates live through symlink - Production:
task react:package→ yarn build → packagestage/as tgz for staging
splunk/config/deps.yml declares Splunkbase dependencies. task deps:install downloads and installs them idempotently. Default deps include:
- Python for Scientific Computing — NumPy/SciPy for custom commands
- Splunk ML Toolkit — ML framework
- SA-VSCode — enables Python debugging from VSCode
Uses the SA-VSCode add-on + debugpy for remote debugging:
- Install SA-VSCode:
task deps:install(included indeps.yml) - Add debug hook to your Python code:
import sys, os sys.path.append(os.path.join(os.environ['SPLUNK_HOME'], 'etc', 'apps', 'SA-VSCode', 'bin')) import splunk_debug as dbg dbg.enable_debugging(timeout=25)
- Trigger the code (run a search, enable an input, etc.)
- In VSCode: Run → "Python: Attach to Splunk (debugpy)"
- Debugger connects to
localhost:5678(forwarded from Splunk container)
Path mapping: splunk/config/apps/<app>/bin/ ↔ /opt/splunk/etc/apps/<app>/bin/
Webpack watch mode (recommended for development):
task react:link→ symlinkstage/into Splunk (run once)task react:start→ webpack watch; edits tosrc/rebuildstage/live- Refresh Splunk Web to see changes (source maps available for debugging)
- VSCode: Run → "Chrome: Splunk Web App" for breakpoints
Production build (for staging verification):
task react:package→ builds and packagesstage/as tgztask stage:deploy- VSCode: Run → "Chrome: Staging Splunk Web"
Staging mode:
task stage:deploy→ staging on:18000- VSCode: Run → "Chrome: Staging Splunk Web"
The splunk-config-dev app sets web.conf options essential for debugging:
js_no_cache = True— disables JS cachingminify_js = False— preserves readable JSenableWebDebug = True— enables Splunk Web debug modeminify_css = False— preserves readable CSS
This app is always bind-mounted in dev. Do not include it in staging builds.
Required (in .env):
SPLUNK_PASSWORD— Splunk admin password
Optional:
APP_NAME— default app for tasksSPLUNKBASE_USERNAME/SPLUNKBASE_PASSWORD— for Splunkbase downloadsSPLUNK_APPS_URL— comma-separated URLs for first-run app installSPLUNK_VERSION— Splunk image version (default: 9.4.0)
| Port | Service |
|---|---|
| 8000 | Splunk Web (dev) |
| 8089 | splunkd REST API (dev) |
| 8088 | HTTP Event Collector (dev) |
| 18000 | Splunk Web (staging) |
| 18089 | splunkd REST API (staging) |
| 18088 | HTTP Event Collector (staging) |
| 5678 | Python debugpy (SA-VSCode) |
| 3000 | React dev server (webpack HMR) |
Requires: Docker Desktop running, .env configured, task installed on Mac.
task test:lifecycle # Splunk lifecycle tests — 7 suites (guards, boot, app, deps, react, staging, skip-provision)
task python:lint # ruff linttest:lifecycle runs tests/e2e/run-lifecycle.sh directly — starts Splunk, runs all 7 lifecycle suites, cleans up. Works on any host with Docker + task.
task test:devcontainer # devcontainer build + static checks + lifecycle tests
task test:all # python:lint + test:devcontainertest:devcontainer uses @devcontainers/cli to build and start the devcontainer, runs static checks (tool availability, LOCAL_WORKSPACE_FOLDER, compose config), then calls task test:lifecycle inside it — same test code path as running on the host.
| Suite | Script | What it tests |
|---|---|---|
| guards | test-guards.sh |
Guard tasks fail fast with clear errors (no container, no image, no APP_NAME) |
| boot | test-boot.sh |
dev:up, health, SPLUNK_PASSWORD auth, splunk-config-dev symlink |
| app-lifecycle | test-app-lifecycle.sh |
app:create, symlinks, app:package, REST verify |
| deps-install | test-deps-install.sh |
deps:install idempotency |
| react-build | test-react-build.sh |
react:build, react:package, tgz validation, idempotency |
| staging | test-staging.sh |
stage:deploy (package + start + install) |
| skip-provision | test-skip-provision.sh |
container restart skips Ansible |
- Splunk won't start:
task dev:logs→ check for errors - App changes not visible:
task dev:refresh(reloads configs without restart); if that doesn't work,task dev:restartd - New app not visible:
task dev:ensure-linkscreates missing symlinks;task dev:refreshreloads configs - Python debugger won't connect: ensure SA-VSCode is installed (
task deps:install), code hasdbg.enable_debugging(), and port 5678 is exposed - React HMR not working: check webpack proxy config points to
splunk-dev:8089 - Corrupt state after restart:
task dev:reprovision(forces full Ansible) - Full reset:
task dev:clean && task dev:up - Port conflicts: edit ports in
.devcontainer/docker-compose.ymlordocker-compose.staging.yml