A self-service platform for spinning up isolated temporary environments, deploying apps, simulating outages, monitoring health, and auto-destroying everything. Think miniature internal Heroku.
Every environment is short-lived by design.
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Linux VM / Host β
β β
User / CI β βββββββββββββββββββββββββββββββββββββββββββββββ β
β β β Docker Engine β β
β make / curl β β β β
βΌ β β ββββββββββββ ββββββββββββ βββββββββββ β β
βββββββββββββ β β β β β β β β β β
β Makefile ββββββββββββββββΆβ β β nginx β β API β β daemon β β β
β (make up) β β β β :8080 β β :7000 β β(cleanup)β β β
βββββββββββββ β β β β β β β β β β
β β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬βββββ β β
βββββββββββββ β β β β β β β
β REST API ββββββββββββββββΆβ β β sandbox-nginx-net β β β
β /envs β β β ββββββββββββββββββββββ β β β
βββββββββββββ β β β β β
β β βββββββββββββββββββββββββββββββββββ β β β
β β β Sandbox Environments β β β β
β β β β β β β
β β β βββββββββββββ βββββββββββββ β β β β
β β β βenv-abc123 β βenv-def456 β βββ β β
β β β β app:5000 β β app:5000 β β β β
β β β β net: own β β net: own β β β β
β β β βββββββββββββ βββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββ β β
β β β β
β β ββββββββββββ logs/ βββββββββββββββββββββββΆβ β
β β β monitor β envs/ (state files) β β
β β β(health) β nginx/conf.d/ (per-env) β β
β β ββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Request flow:
Browser β Nginx (:8080) β upstream sandbox-app-<env_id>:5000
Nginx routes by Host header: env-abc123.sandbox.local β env-abc123 container
Data flow:
create_env.sh β Docker network + container + Nginx conf + state file
cleanup_daemon.sh β reads envs/*.json β calls destroy_env.sh when TTL expired
health_monitor.py β polls localhost:<port>/health every 30s β writes health.log
- Docker β₯ 24.x and Docker Compose β₯ 2.20
- Python 3.11+ (on the host, for the health monitor)
- Bash 4+, GNU Make
- A Linux VM (tested on Ubuntu 22.04/24.04)
Zero to first running environment in 5 commands:
git clone https://github.com/YOUR_USERNAME/devops-sandbox.git
cd devops-sandbox
cp .env.example .env # review defaults; edit ports if needed
make up # starts Nginx, API, cleanup daemon, monitor
make create # prompts for name + TTL, creates envAfter make create you'll see:
ββββββββββββββββββββββββββββββββββββββββββββ
β Environment Ready! β
β ID: env-1716000000-a1b2c3 β
β URL: http://localhost:8412 β
β TTL: 1800s (expires in 30 min) β
ββββββββββββββββββββββββββββββββββββββββββββ
make create
# name: myapp
# ttl: 300 (5 minutes for demo)Or via the API:
curl -s -X POST http://localhost:7000/envs \
-H 'Content-Type: application/json' \
-d '{"name":"myapp","ttl":300}' | python3 -m json.toolmake status
# or
curl -s http://localhost:7000/envs | python3 -m json.toolHit the app directly (port shown at creation time):
curl http://localhost:<PORT>/health
# {"status": "ok", "env_id": "env-...", ...}make health
# or via API (last 10 results):
curl -s http://localhost:7000/envs/<ENV_ID>/health | python3 -m json.toolCrash the container:
make simulate ENV=env-1716000000-a1b2c3 MODE=crashPause it (freeze, not kill):
make simulate ENV=env-1716000000-a1b2c3 MODE=pauseNetwork isolation:
make simulate ENV=env-1716000000-a1b2c3 MODE=networkOr via the API:
curl -s -X POST http://localhost:7000/envs/<ENV_ID>/outage \
-H 'Content-Type: application/json' \
-d '{"mode":"crash"}'Within 90 seconds the health monitor will detect failures. After 3 consecutive failures, status becomes degraded:
make health
# status=degraded
curl -s http://localhost:7000/envs/<ENV_ID>/healthWatch the health log live:
tail -f logs/<ENV_ID>/health.logmake simulate ENV=env-1716000000-a1b2c3 MODE=recover
# or
curl -s -X POST http://localhost:7000/envs/<ENV_ID>/outage \
-H 'Content-Type: application/json' \
-d '{"mode":"recover"}'Health monitor detects recovery and resets status to running.
make logs ENV=env-1716000000-a1b2c3
# tails logs/<ENV_ID>/app.log live
# or via API (last 100 lines):
curl -s http://localhost:7000/envs/<ENV_ID>/logsmake destroy ENV=env-1716000000-a1b2c3If you set a short TTL (e.g. 60s), the cleanup daemon destroys it automatically. Watch it happen:
tail -f logs/cleanup.log| Method | Endpoint | Description |
|---|---|---|
POST |
/envs |
Create env β body: {name, ttl} |
GET |
/envs |
List all active envs + TTL remaining |
GET |
/envs/:id |
Get single env details |
DELETE |
/envs/:id |
Destroy env |
GET |
/envs/:id/logs |
Last 100 lines of app.log |
GET |
/envs/:id/health |
Last 10 health check results |
POST |
/envs/:id/outage |
Trigger simulation β body: {mode} |
| Target | Description |
|---|---|
make up |
Start Nginx, daemon, API, monitor |
make down |
Stop everything, destroy all envs |
make create |
Interactive: create new env |
make destroy ENV=<id> |
Destroy specific env |
make logs ENV=<id> |
Tail env app.log (live) |
make health |
Show all env health statuses |
make simulate ENV=<id> MODE=<mode> |
Run outage simulation |
make status |
List envs via API (JSON) |
make clean |
Wipe all state, logs, archives |
| Mode | Effect | Recovery |
|---|---|---|
crash |
docker kill β hard stop |
MODE=recover |
pause |
docker pause β freeze process |
MODE=recover |
network |
Disconnect from Docker networks | MODE=recover |
recover |
Unpause / restart / reconnect as needed | β |
stress |
CPU spike via stress-ng or Python burner (60s) | Self-resolving |
Nginx is the front door for all environments. Each create_env.sh call writes a config to nginx/conf.d/<ENV_ID>.conf and runs nginx -s reload. On destroy, the file is removed and Nginx is reloaded again.
Routing strategy: Host-header based. Each env gets a virtual server name <ENV_ID>.sandbox.local. For local testing, hit by port directly (each env gets a random host port). For proper hostname routing, add entries to /etc/hosts or use a wildcard DNS entry.
Network: Nginx runs in sandbox-nginx-net. App containers are also joined to this network at creation time, so Nginx can upstream to sandbox-app-<ENV_ID>:5000 by container name.
Approach A (implemented): At container creation, docker logs -f <container> >> logs/<ENV_ID>/app.log & is run and the PID saved to logs/<ENV_ID>/log_shipper.pid. On destroy, this PID is killed before container removal to prevent zombie processes.
Logs are archived to logs/archived/<ENV_ID>/ on destroy and remain queryable.
Netdata is an optional add-on that gives you a live dashboard of every container's CPU, memory, network I/O, and disk β with zero configuration. It auto-discovers all sandbox containers via the Docker socket the moment they start.
Start:
make monitoring-up
# Dashboard: http://localhost:19999Stop:
make monitoring-downWhat you get instantly, with no setup:
- Per-container CPU and memory graphs β including each
sandbox-app-<id>as it's created - Host-level system metrics (load, disk, network)
- Built-in alerts for memory pressure and high CPU
- Live log of containers appearing and disappearing as you create/destroy envs
Metrics are retained in a Docker volume (netdata-lib, netdata-cache) so they survive restarts. Configuration is in monitor/netdata/netdata.conf β the defaults are fine for local use.
devops-sandbox/
βββ platform/
β βββ create_env.sh # Spin up environment
β βββ destroy_env.sh # Tear down environment
β βββ cleanup_daemon.sh # TTL auto-expire loop
β βββ simulate_outage.sh # Chaos injection
β βββ api.py # Flask REST API
β βββ lib/
β βββ common.sh # Shared functions (state, docker, nginx helpers)
βββ apps/
β βββ demo/
β βββ app.py # Demo HTTP server (/ /health /info)
β βββ Dockerfile
βββ nginx/
β βββ nginx.conf # Main config (includes conf.d/)
β βββ conf.d/ # Auto-generated per-env configs (gitignored)
βββ monitor/
β βββ health_monitor.py # 30s health poller β health.log
β βββ netdata/
β βββ netdata.conf # Netdata config (update_every, retention, plugins)
βββ scripts/
β βββ inspect.sh # Pretty-print env runtime state
β βββ list_envs.sh # Formatted table of all active envs
β βββ build_demo_app.sh # Build sandbox-demo-app:latest
β βββ export_logs.sh # Tarball logs for any env
β βββ prune_archives.sh # Remove archived logs older than N days
β βββ reset_platform.sh # Nuclear wipe with confirmation
βββ tests/
β βββ test_api.sh # 12 API integration assertions
β βββ test_lifecycle.sh # Full createβcrashβrecoverβdestroy cycle
β βββ test_outage.sh # All outage modes tested end-to-end
β βββ test_cleanup_daemon.sh # TTL auto-expiry test (~2 min)
βββ logs/ # gitignored
β βββ cleanup.log
β βββ <env_id>/
β β βββ app.log
β β βββ health.log
β βββ archived/
βββ envs/ # gitignored β runtime state JSONs
βββ .env.example
βββ .gitignore
βββ docker-compose.yml # Core platform (nginx, api, daemon, monitor)
βββ docker-compose.monitoring.yml # Optional Netdata (make monitoring-up)
βββ Dockerfile.api
βββ Makefile
βββ requirements.txt
βββ CONTRIBUTING.md
βββ README.md
- Single VM only. No distributed scheduling β everything runs on one host. This is by design.
- Port allocation. Env ports are random (8100β9000). High concurrency could exhaust this range. For >900 envs, widen the range.
- No auth on the API. The API is unauthenticated. Do not expose port 7000 publicly without adding auth middleware.
- Demo app is ephemeral. The bundled Python HTTP server is not production-grade β it's a placeholder to satisfy
/health. Swap it for your own image viacreate_env.sh. - Nginx config reload is not atomic. Between
rmandnginx -s reload, Nginx may briefly serve a 502 for that env. For production, usenginx -tvalidation before reloading. - Log shipper depends on
docker logs. On high-throughput containers this can lag. For production use Approach B (Loki/Fluentd via Docker socket). - Cleanup daemon requires bash + python3 in the daemon container. The
docker:24-cliimage installs these at startup, which adds ~5s cold start. - No TLS. All traffic is plain HTTP. Add Certbot + nginx SSL termination for production use.