<p>Most of us live in two worlds at once. Some projects run natively — a Laravel app served straight off the filesystem, fast and easy to step through. Others live in Docker — a tidy <code>docker compose up</code> with nginx, a database, a queue worker, maybe ClickHouse. Both are great. What's not great is the seam between them.</p><p>Your native sites answer at <code>myapp.test</code> with a trusted green padlock. Your containers answer at something like <code>something.orb.local</code>, or a raw <code>localhost:8000</code>, or a port you can never quite remember. Two naming schemes, two ways to get HTTPS, two places to look. It's a small tax, but you pay it every day.</p><p>Grove 0.4 removes the seam. It becomes one front door for both worlds — native sites and Docker/OrbStack containers, under the same <code>*.test</code> names, the same trusted local HTTPS, and the same dashboard.</p><h2>Why: the container was always the odd one out</h2><p>Grove already knew how to be a good local host: DNS for <code>*.test</code>, a reverse proxy, TLS terminated with its own trusted CA, a dashboard. Containers were the one thing standing outside that. So we taught Grove to bring them in.</p><p>The idea: any running container becomes a first-class <code>&lt;name&gt;.test</code> site. Grove terminates TLS with its trusted cert and reverse-proxies to the container. No per-container certificate dance, no remembering ports, no context-switching. And because a container is now just another Grove site, everything else Grove does comes along for free — including public tunnels.</p><h2>How: it just shows up</h2><p>There's no new command to learn. Start your containers as you always do:</p><pre><code class="language-text">$ docker compose up -d
</code></pre><p>Grove notices. Within a few seconds it discovers the web container and starts serving it:</p><pre><code class="language-text">$ grove list
SITE              DRIVER   PHP   HTTPS   URL
elyra-web.test    laravel  8.4   yes     https://elyra-web.test
inside2.test      proxy          yes     https://inside2.test    🐳
</code></pre><p>There's your dockerized app, <code>inside2.test</code>, sitting right next to a native Laravel site — with a trusted certificate:</p><pre><code class="language-text">$ curl -sI https://inside2.test | head -1
HTTP/1.1 200 OK
</code></pre><p>Grove finds the right container in one of two ways:</p><ol><li><p><strong>A label you (or OrbStack) already set.</strong> If a container carries <code>dev.orbstack.domains</code> or an explicit <code>grove.host</code>, Grove reuses it:</p></li></ol><pre><code class="language-text">$ docker ps --format '{{.Names}}\t{{.Labels}}' | grep orbstack
inside2-nginx   ...,dev.orbstack.domains=inside2.local,...
</code></pre><p>Grove reads that label, serves <code>inside2.test</code>, and quietly ignores the database, cache and queue containers — you don't want <code>mysql.test</code>.</p><ol start="2"><li><p><strong>No labels at all.</strong> Just a plain <code>docker compose</code> project. Grove picks the web container by service name and published port and serves <code>&lt;project&gt;.test</code>. Nothing to configure.</p></li></ol><p>In the daemon log it's a single, calm line:</p><pre><code class="language-text">INFO docker sites updated  containers=1
</code></pre><p>Stop a container and the site slips away; start it and it comes back. It's live.</p><h2>Controls, right where you're already looking</h2><p>Because containers are real sites now, they live in the same Sites table — with a 🐳 badge and their own controls:</p><pre><code class="language-text">🖥  Sites
┌──────────────────────────────────────────────────────────────────────┐
│ HOST                DRIVER      HTTPS   ACTIONS                        │
│ elyra-web.test      laravel      🟢     ↗  🌍  📁  🗑                  │
│ inside2.test        🐳 docker    🔒     ↗  🌍  ↻  ⏹                   │
│   🐳 http://inside2.local                                             │
│ legacy-api.test     🐳 docker    🔒     ▶   (stopped)                  │
└──────────────────────────────────────────────────────────────────────┘
</code></pre><p>Restart (↻) and stop (⏹) a running container, or start (▶) a stopped one — without leaving the dashboard. A stopped site even serves a friendly little "start it from the Sites list" page instead of a cryptic connection error.</p><p>And that 🌍 button? Yes — you can tunnel a container publicly too:</p><pre><code class="language-text">$ grove share inside2
  🌿  Tunnel online
     Public   https://7f3a2c9k.grove.elyracode.com
     Local    http://inside2.test
</code></pre><p>A dockerized app, shared to the internet over HTTPS, in one command. That's the payoff of treating containers as first-class sites: features you built for native apps just work.</p><h2>The honest engineering bit</h2><p>The first thing I tried was the obvious one: proxy <code>inside2.test</code> to the container's published host port, <code>127.0.0.1:8000</code>.</p><pre><code class="language-text">$ curl -sI http://127.0.0.1:8000
   (hangs, then nothing)
</code></pre><p>Dead end. The nginx inside listens on 8080 for <code>server_name inside2.local</code>; the published 8000 was a decoy. The app was reachable at the container's IP on 8080, and — crucially — at <code>inside2.local</code> through OrbStack's own routing:</p><pre><code class="language-text">$ curl -s -o /dev/null -w '%{http_code}\n' http://inside2.local
200
</code></pre><p>So Grove reuses that: for OrbStack-labelled containers it proxies to the container's own domain and lets OrbStack handle the internal port heuristics. Grove adds the <code>.test</code> name, the trusted certificate, the dashboard and the tunnels; OrbStack keeps doing the part it's already great at. Standing on shoulders, not toes.</p><p>There was also a delightfully sneaky bug: talking to the Docker socket with a plain HTTP request, the Engine kept the connection open, so our "read the response" step waited… forever. The fix was a one-liner (<code>Connection: close</code>) plus a hard timeout, so container discovery can never, ever hang the daemon.</p><h2>Getting it</h2><p>Grove auto-updates. After it relaunches:</p><pre><code class="language-text"># pick up the new daemon (one click in Tools → Restart daemon)
$ grove status | head -1
Grove 0.4.1
</code></pre><p>Then just… start your containers. They show up.</p><p>Two worlds, one front door. Native sites and Docker containers, the same names, the same trusted HTTPS, the same dashboard, the same one-command public sharing. The seam is gone — and honestly, that's the nicest kind of feature: the one you stop noticing, because everything is finally in the same place. 🌳🐳</p>