<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: abdelouahed el kasri</title>
    <description>The latest articles on DEV Community by abdelouahed el kasri (@abdelouahe53334).</description>
    <link>https://dev.to/abdelouahe53334</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3967111%2F93550c57-83e8-4f09-9896-1719efb4416a.jpg</url>
      <title>DEV Community: abdelouahed el kasri</title>
      <link>https://dev.to/abdelouahe53334</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abdelouahe53334"/>
    <language>en</language>
    <item>
      <title>How I built self-hosted dev sandboxes with Docker and Go (and why I open-sourced it)</title>
      <dc:creator>abdelouahed el kasri</dc:creator>
      <pubDate>Wed, 03 Jun 2026 19:55:03 +0000</pubDate>
      <link>https://dev.to/abdelouahe53334/how-i-built-self-hosted-dev-sandboxes-with-docker-and-go-and-why-i-open-sourced-it-1hoe</link>
      <guid>https://dev.to/abdelouahe53334/how-i-built-self-hosted-dev-sandboxes-with-docker-and-go-and-why-i-open-sourced-it-1hoe</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I looked at the obvious options:&lt;/p&gt;

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

&lt;p&gt;I needed something that:&lt;/p&gt;

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

&lt;p&gt;Nothing fit. So I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I ended up with
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;sandboxed&lt;/strong&gt; is a small Go control plane that drives the Docker daemon, fronted by Traefik.&lt;/p&gt;

&lt;p&gt;The whole architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           ┌──────────── 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the whole stack. No external database, no message bus, no orchestrator.&lt;/p&gt;




&lt;h2&gt;
  
  
  The key decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SQLite as the source of truth
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;Host reboots? Reconciler brings everything back.
&lt;/li&gt;
&lt;li&gt;Docker daemon restarted? Same thing.
&lt;/li&gt;
&lt;li&gt;Manual &lt;code&gt;docker rm&lt;/code&gt;? Reconciler recreates it.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No distributed state to worry about. One file. WAL mode for concurrent reads.&lt;/p&gt;




&lt;h3&gt;
  
  
  Stop-on-idle, wake-on-request
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The workspace directory persists on disk, so everything is exactly where the user left it.&lt;/p&gt;

&lt;p&gt;This means you can run &lt;strong&gt;50+ sandboxes&lt;/strong&gt; on a host that could only run ~10 simultaneously.&lt;/p&gt;




&lt;h3&gt;
  
  
  Real security, not just Docker
&lt;/h3&gt;

&lt;p&gt;Docker alone isn't enough isolation for running untrusted code. Each sandbox gets:&lt;/p&gt;

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




&lt;h3&gt;
  
  
  Preview URLs that just work
&lt;/h3&gt;

&lt;p&gt;Each sandbox registers itself with Traefik via Docker labels. Any port the user runs a dev server on gets a URL like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://s-&amp;lt;sandbox-id&amp;gt;-3000.preview.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For local development, &lt;code&gt;*.localhost&lt;/code&gt; resolves to &lt;code&gt;127.0.0.1&lt;/code&gt; in modern browsers — zero DNS config needed.&lt;/p&gt;

&lt;p&gt;For production, point a wildcard DNS record and let Traefik handle TLS with Let's Encrypt.&lt;/p&gt;




&lt;h3&gt;
  
  
  The batteries-included image
&lt;/h3&gt;

&lt;p&gt;The base image comes with everything a dev environment needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js + pnpm + bun
&lt;/li&gt;
&lt;li&gt;Python + uv
&lt;/li&gt;
&lt;li&gt;git, ripgrep, fd
&lt;/li&gt;
&lt;li&gt;Claude Code CLI and OpenCode CLI (for AI agent use cases)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building on top of it is just a Dockerfile.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/tastyeffectco/sandboxes
&lt;span class="nb"&gt;cd &lt;/span&gt;sandboxes
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create your first sandbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-XPOST&lt;/span&gt; http://127.0.0.1:9090/sandbox &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"ports":[3000]}'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/.*"id":"([^"]+)".*/\1/'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-XPOST&lt;/span&gt; http://127.0.0.1:9090/sandbox/&lt;span class="nv"&gt;$ID&lt;/span&gt;/exec &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"cmd":["bash","-lc","cd ~/workspace &amp;amp;&amp;amp; echo hello &amp;gt; index.html &amp;amp;&amp;amp; python3 -m http.server 3000"]}'&lt;/span&gt;

open &lt;span class="s2"&gt;"http://s-&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;-3000.preview.localhost"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why open source it
&lt;/h2&gt;

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

&lt;p&gt;It’s the kind of thing I wish I’d found instead of building from scratch.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/tastyeffectco/sandboxes" rel="noopener noreferrer"&gt;https://github.com/tastyeffectco/sandboxes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Stars, issues, and contributions welcome.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>go</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
