A self-service platform for spinning up isolated, short-lived environments on a single Linux VM. Each environment gets its own Docker network, Nginx route, log stream, and health monitor. When the TTL expires β or you say so β everything is torn down and archived automatically.
Think of it as a stripped-down internal Heroku with a chaos engineering toggle.
You create an environment. A container starts, Nginx opens a route to it, a log shipper attaches, and a health poller begins watching it every 30 seconds. You can crash it, pause it, or cut its network β then recover it. When the TTL runs out, the cleanup daemon destroys it without any input from you.
The demo app inside each environment is a simple Flask server. The platform is the project, not what runs inside it.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EC2 Linux VM β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Control Plane β β
β β β β
β β ββββββββββββββββ ββββββββββββββββββββββββ β β
β β β FastAPI β β Cleanup Daemon β β β
β β β :8000 β β (60s loop) β β β
β β β β β β β β
β β β POST /envs β β reads envs/*.json β β β
β β β GET /envs β β checks TTL expiry β β β
β β β DELETE /envs β β calls destroy_env β β β
β β β GET /logs β ββββββββββββββββββββββββ β β
β β β GET /health β β β
β β β POST /outage β ββββββββββββββββββββββββ β β
β β ββββββββ¬βββββββββ β Health Poller β β β
β β β β (30s loop) β β β
β β β wraps β β β β
β β βΌ β GET /health per env β β β
β β ββββββββββββββββ β 3 failures β DEGRADEDβ β β
β β β Bash Scripts β β writes health.log β β β
β β β β ββββββββββββββββββββββββ β β
β β β create_env β β β
β β β destroy_env β β β
β β β simulate β β β
β β ββββββββ¬βββββββββ β β
β ββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β
β ββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ β
β β Data Plane β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Nginx Container :80 β β β
β β β β β β
β β β nginx/conf.d/env-abc123.conf β auto-written β β β
β β β nginx/conf.d/env-def456.conf β auto-written β β β
β β β β β β
β β β Routes by Host header: env-abc123.sandbox.localβ β β
β β ββββββββ¬ββββββββββββββββββββ¬ββββββββββββββββββββββββ β β
β β β β β β
β β ββββββββΌβββββββ βββββββββΌβββββββ β β
β β βsandbox-net- β βsandbox-net- β (one Docker β β
β β βenv-abc123 β βenv-def456 β network β β
β β β β β β per env) β β
β β β βββββββββββ β β βββββββββββββ β β
β β β βenv-abc123β β β βenv-def456ββ β β
β β β β:5000 β β β β:5000 ββ β β
β β β βFlask appβ β β βFlask app ββ β β
β β β βββββββββββ β β βββββββββββββ β β
β β βββββββββββββββ βββββββββββββββ β β
β β β β
β β State: envs/env-abc123.json (id, ttl, port, status) β
β β Logs: logs/env-abc123/app.log β β
β β logs/env-abc123/health.log β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Request flow:
Browser / curl
β
βΌ
Nginx :80 βββΊ routes by Host header βββΊ env container :5000
β
βΌ (also)
FastAPI :8000 βββΊ wraps bash scripts βββΊ Docker + filesystem
| Requirement | Version |
|---|---|
| Ubuntu | 22.04 LTS |
| Docker | 24+ |
| Python | 3.10+ |
jq |
any |
curl |
any |
make |
any |
Install everything on a fresh Ubuntu 22.04 VM:
# Docker
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER && newgrp docker
# Everything else
sudo apt install -y python3 python3-pip jq curl make git
pip3 install fastapi uvicorn --break-system-packagesFrom zero to a running environment in 5 commands:
git clone https://github.com/YOUR_USERNAME/devops-sandbox.git
cd devops-sandbox
docker build -t sandbox-demo-app:latest ./demo-app
make up
make createThat's it. The output will give you a URL and TTL. Open the URL in your browser.
devops-sandbox/
βββ platform/
β βββ create_env.sh # spins up an environment
β βββ destroy_env.sh # tears one down
β βββ cleanup_daemon.sh # TTL watcher, runs every 60s
β βββ simulate_outage.sh # crash / pause / network / recover
β βββ api.py # FastAPI control plane
βββ nginx/
β βββ nginx.conf # main config, includes conf.d/
β βββ conf.d/ # per-env configs, auto-written on create
βββ monitor/
β βββ poller.sh # health checker, runs every 30s
βββ demo-app/
β βββ app.py # Flask app with GET / and GET /health
β βββ requirements.txt
β βββ Dockerfile
βββ logs/ # gitignored β runtime logs per env
βββ envs/ # gitignored β JSON state per env
βββ Makefile
βββ README.md
make up # start Nginx + cleanup daemon + health poller + API
make down # stop everything, destroy all envs
make create # interactive: prompts for name and TTL
make destroy ENV=env-abc123 # destroy a specific environment
make logs ENV=env-abc123 # tail that environment's app.log
make health # print health status of all active envs
make simulate ENV=env-abc123 MODE=crash # run outage simulation
make clean # wipe all logs, state, and Nginx configsThe API runs on port 8000. Interactive docs at http://YOUR_IP:8000/docs.
| Method | Endpoint | What it does |
|---|---|---|
POST |
/envs |
Create a new environment |
GET |
/envs |
List all active environments + TTL remaining |
DELETE |
/envs/:id |
Destroy an environment |
GET |
/envs/:id/logs |
Last 100 lines of app.log |
GET |
/envs/:id/health |
Last 10 health check results |
POST |
/envs/:id/outage |
Trigger an outage simulation |
Create an environment:
curl -X POST http://localhost:8000/envs \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "ttl": 600}'Trigger an outage:
curl -X POST http://localhost:8000/envs/env-abc123/outage \
-H "Content-Type: application/json" \
-d '{"mode": "pause"}'Valid outage modes: crash pause network recover stress
make upThis starts Nginx (Docker), the cleanup daemon (background), the health poller (background), and the FastAPI server on port 8000.
make create
# Enter name: demo-app
# Enter TTL: 300Output:
β
Environment ready!
ID: env-f9c029f5
URL: http://<YOUR_IP>:8112
TTL: 300s (expires at 2026-05-10 05:00:00)
Health: http://<YOUR_IP>:8112/health
curl http://localhost:8112/
curl http://localhost:8112/healthmake healthAfter 30 seconds the poller will have written its first entry to logs/env-f9c029f5/health.log.
# Pause the container (app becomes unreachable)
make simulate ENV=env-f9c029f5 MODE=pause
# Watch the health poller detect it (within 90s)
make healthAfter 3 consecutive failed health checks, the environment status flips to degraded.
make simulate ENV=env-f9c029f5 MODE=recoverThe container unpauses, the status resets to running, and health checks resume passing.
make logs ENV=env-f9c029f5Or via the API:
curl http://localhost:8000/envs/env-f9c029f5/logsmake destroy ENV=env-f9c029f5This stops the container, removes the Docker network, deletes the Nginx config,
reloads Nginx, archives the logs to logs/archived/, and removes the state file.
If you don't destroy it manually, the cleanup daemon will do it when the TTL expires.
create_env.sh does the following in order:
- Generates a unique ID (
env-+ 8 hex chars from/proc/sys/kernel/random/uuid) - Picks a random port in the range
8080β8179 - Creates a dedicated Docker network (
sandbox-net-$ENV_ID) - Connects Nginx to that network so it can proxy to the container
- Starts the app container with
sandbox.env=$ENV_IDlabel - Writes state to
envs/$ENV_ID.jsonatomically (write to.tmp, thenmv) - Writes an Nginx upstream + server block to
nginx/conf.d/$ENV_ID.conf - Reloads Nginx with
nginx -s reload - Starts
docker logs -fpiped tologs/$ENV_ID/app.login the background, saves PID
destroy_env.sh reverses all of that:
- Kills the log shipper process by PID
- Stops and removes all containers with
label=sandbox.env=$ENV_ID - Disconnects and removes the Docker network
- Archives
logs/$ENV_ID/tologs/archived/$ENV_ID/ - Deletes the Nginx config and reloads Nginx
- Deletes the state file
Runs as a background process. Every 60 seconds it reads every file in envs/,
compares created_at + ttl against the current epoch time, and calls destroy_env.sh
on anything that has expired. All actions are timestamped and written to logs/cleanup.log.
Every 30 seconds it reads all state files, hits GET /health on each environment's port,
and records the HTTP status and latency to logs/$ENV_ID/health.log. After 3 consecutive
non-200 responses it sets the environment's status to degraded in the state file and
prints a warning to stdout.
Nginx runs as a Docker container. Its main config includes conf.d/*.conf. Every create
writes a new file there and every destroy removes it. After each change, the script runs
docker exec sandbox-nginx nginx -s reload. The environment is reachable by direct port
(e.g. :8112) or by setting Host: env-f9c029f5.sandbox.local in the request header.
simulate_outage.sh has a guard at the top that refuses to run against sandbox-nginx,
sandbox-api, or sandbox-cleanup. The modes work as follows:
| Mode | What happens | How to recover |
|---|---|---|
crash |
docker kill β container stops immediately |
docker start via recover |
pause |
docker pause β process frozen, port still open |
docker unpause via recover |
network |
docker network disconnect β container isolated |
docker network connect via recover |
recover |
undoes whichever mode was active | β |
stress |
runs yes > /dev/null inside the container to spike CPU |
kill the process manually |
- Port range is
8080β8179, so a maximum of ~100 concurrent environments per VM - Log shipping uses Approach A (simple
docker logs -fpipe) β no log aggregator - Nginx reload causes ~1 second of disruption per config change
- The
stressmode uses a basic shell trick, notstress-ng, so CPU impact varies - No HTTPS β this is an HTTP-only sandbox platform
- Public IP detection uses
ifconfig.mewhich may be slow or unavailable in some networks; the EC2 metadata endpoint (169.254.169.254) is used as a fallback - State files are stored on disk with no locking β concurrent creates on the same port could theoretically collide (rare given the random port selection)
This platform is designed for internal sandbox use on a trusted network.
Do not expose port 8000 (API) or 8080β8179 (env ports) to the public internet
without adding authentication. The API has no auth layer by design β add an API key
middleware or put it behind a VPN for any real deployment.