Grove · · 4 min read

One front door for everything — Grove 0.4

Grove 0.4 brings Docker and OrbStack containers into the fold: native sites and containers now share the same *.test names, trusted local HTTPS, dashboard, and one-command public tunnels.

One front door for everything — Grove 0.4

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 docker compose up with nginx, a database, a queue worker, maybe ClickHouse. Both are great. What's not great is the seam between them.

Your native sites answer at myapp.test with a trusted green padlock. Your containers answer at something like something.orb.local, or a raw localhost:8000, 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.

Grove 0.4 removes the seam. It becomes one front door for both worlds — native sites and Docker/OrbStack containers, under the same *.test names, the same trusted local HTTPS, and the same dashboard.

Why: the container was always the odd one out

Grove already knew how to be a good local host: DNS for *.test, 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.

The idea: any running container becomes a first-class <name>.test 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.

How: it just shows up

There's no new command to learn. Start your containers as you always do:

$ docker compose up -d

Grove notices. Within a few seconds it discovers the web container and starts serving it:

$ 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    🐳

There's your dockerized app, inside2.test, sitting right next to a native Laravel site — with a trusted certificate:

$ curl -sI https://inside2.test | head -1
HTTP/1.1 200 OK

Grove finds the right container in one of two ways:

  1. A label you (or OrbStack) already set. If a container carries dev.orbstack.domains or an explicit grove.host, Grove reuses it:

$ docker ps --format '{{.Names}}\t{{.Labels}}' | grep orbstack
inside2-nginx   ...,dev.orbstack.domains=inside2.local,...

Grove reads that label, serves inside2.test, and quietly ignores the database, cache and queue containers — you don't want mysql.test.

  1. No labels at all. Just a plain docker compose project. Grove picks the web container by service name and published port and serves <project>.test. Nothing to configure.

In the daemon log it's a single, calm line:

INFO docker sites updated  containers=1

Stop a container and the site slips away; start it and it comes back. It's live.

Controls, right where you're already looking

Because containers are real sites now, they live in the same Sites table — with a 🐳 badge and their own controls:

🖥  Sites
┌──────────────────────────────────────────────────────────────────────┐
│ HOST                DRIVER      HTTPS   ACTIONS                        │
│ elyra-web.test      laravel      🟢     ↗  🌍  📁  🗑                  │
│ inside2.test        🐳 docker    🔒     ↗  🌍  ↻  ⏹                   │
│   🐳 http://inside2.local                                             │
│ legacy-api.test     🐳 docker    🔒     ▶   (stopped)                  │
└──────────────────────────────────────────────────────────────────────┘

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.

And that 🌍 button? Yes — you can tunnel a container publicly too:

$ grove share inside2
  🌿  Tunnel online
     Public   https://7f3a2c9k.grove.elyracode.com
     Local    http://inside2.test

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.

The honest engineering bit

The first thing I tried was the obvious one: proxy inside2.test to the container's published host port, 127.0.0.1:8000.

$ curl -sI http://127.0.0.1:8000
   (hangs, then nothing)

Dead end. The nginx inside listens on 8080 for server_name inside2.local; the published 8000 was a decoy. The app was reachable at the container's IP on 8080, and — crucially — at inside2.local through OrbStack's own routing:

$ curl -s -o /dev/null -w '%{http_code}\n' http://inside2.local
200

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 .test name, the trusted certificate, the dashboard and the tunnels; OrbStack keeps doing the part it's already great at. Standing on shoulders, not toes.

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 (Connection: close) plus a hard timeout, so container discovery can never, ever hang the daemon.

Getting it

Grove auto-updates. After it relaunches:

# pick up the new daemon (one click in Tools → Restart daemon)
$ grove status | head -1
Grove 0.4.1

Then just… start your containers. They show up.

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. 🌳🐳