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.
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:
A label you (or OrbStack) already set. If a container carries
dev.orbstack.domainsor an explicitgrove.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.
No labels at all. Just a plain
docker composeproject. 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. 🌳🐳