DEV Community

abdelouahed el kasri
abdelouahed el kasri

Posted on

How I built self-hosted dev sandboxes with Docker and Go (and why I open-sourced it)

The problem

I'm building an AI platform where coding agents build web apps for users. Each agent needs its own isolated Linux environment with dev tools, and the user needs a live preview URL to see the result.

I looked at the obvious options:

  • Gitpod / Codespaces — cloud-only, per-minute pricing adds up fast when you're spinning up environments for every user
  • Devcontainers — great for local dev, but no preview URLs and no multi-tenant isolation
  • Kubernetes-based solutions — I needed sandboxes, not a cluster management degree

I needed something that:

  • Runs on a single Docker host (no K8s)
  • Gives each sandbox an instant preview URL
  • Stops idle sandboxes to save RAM
  • Wakes them on the next request
  • Is actually secure (agents run untrusted code)

Nothing fit. So I built it.


What I ended up with

sandboxed is a small Go control plane that drives the Docker daemon, fronted by Traefik.

The whole architecture looks like this:

           ┌──────────── your host (just needs Docker) ────────────┐
browser ──▶│  Traefik  ──▶  sandbox container (dev server :3000)    │
           │     ▲              ▲   ▲   ▲                            │
API/CLI ──▶│  sandboxd ─────────┘   │   │  (docker run/stop/exec)   │
           │     │  SQLite state     │   └─ workspace dir (persists) │
           └─────┼───────────────────┼──────────────────────────────┘
                 └─ idle reaper ─────┘  stop-on-idle / wake-on-request
Enter fullscreen mode Exit fullscreen mode

That’s the whole stack. No external database, no message bus, no orchestrator.


The key decisions

SQLite as the source of truth

Every sandbox's desired state lives in SQLite. On boot, a reconciler compares SQLite to what Docker actually has running and converges.

  • Host reboots? Reconciler brings everything back.
  • Docker daemon restarted? Same thing.
  • Manual docker rm? Reconciler recreates it.

No distributed state to worry about. One file. WAL mode for concurrent reads.


Stop-on-idle, wake-on-request

Most sandboxes sit idle 90% of the time. Instead of keeping them running (and eating RAM), the idle reaper stops them after a configurable timeout.

When someone hits the preview URL of a stopped sandbox, Traefik routes the request to the control plane, which wakes the container and proxies through once it's ready.

The workspace directory persists on disk, so everything is exactly where the user left it.

This means you can run 50+ sandboxes on a host that could only run ~10 simultaneously.


Real security, not just Docker

Docker alone isn't enough isolation for running untrusted code. Each sandbox gets:

  • All capabilities dropped — no NET_RAW, no SYS_ADMIN, nothing
  • no-new-privileges — can't escalate via setuid binaries
  • Read-only rootfs — container filesystem can't be modified
  • Memory + PID limits — no fork bombs or memory exhaustion
  • Isolated network — sandboxes can't talk to each other

Preview URLs that just work

Each sandbox registers itself with Traefik via Docker labels. Any port the user runs a dev server on gets a URL like:

http://s-<sandbox-id>-3000.preview.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

For local development, *.localhost resolves to 127.0.0.1 in modern browsers — zero DNS config needed.

For production, point a wildcard DNS record and let Traefik handle TLS with Let's Encrypt.


The batteries-included image

The base image comes with everything a dev environment needs:

  • Node.js + pnpm + bun
  • Python + uv
  • git, ripgrep, fd
  • Claude Code CLI and OpenCode CLI (for AI agent use cases)

Building on top of it is just a Dockerfile.


Quick start

git clone https://github.com/tastyeffectco/sandboxes
cd sandboxes
./install.sh
Enter fullscreen mode Exit fullscreen mode

Create your first sandbox:

ID=$(curl -s -XPOST http://127.0.0.1:9090/sandbox \
  -H 'content-type: application/json' \
  -d '{"ports":[3000]}' | sed -E 's/.*"id":"([^"]+)".*/\1/')

curl -s -XPOST http://127.0.0.1:9090/sandbox/$ID/exec \
  -H 'content-type: application/json' \
  -d '{"cmd":["bash","-lc","cd ~/workspace && echo hello > index.html && python3 -m http.server 3000"]}'

open "http://s-$ID-3000.preview.localhost"
Enter fullscreen mode Exit fullscreen mode

Why open source it

I'm moving my platform to a different architecture, and this sandbox layer is solid enough to be useful on its own.

It’s the kind of thing I wish I’d found instead of building from scratch.

If you need isolated dev environments with preview URLs on a single host — no Kubernetes, no cloud dependency — this might save you a few months of work.

Repo:

https://github.com/tastyeffectco/sandboxes

MIT licensed. Stars, issues, and contributions welcome.

Top comments (0)