One config file with paths and hostnames. roost generates the Dockerfiles, Compose stack, Caddy routing, and Cloudflare Tunnel — and creates every DNS record via API. No files added to your repos, ever. Run it on your laptop or a small always-on box, and drive the whole fleet from a browser.
You supply where each app lives and what hostname it answers on. Framework, port, database, start command, and memory cap are inferred — every one overridable, none required.
# ~/.roost/config.yml — minimal domain: rajan.dev apps: - ~/code/blog # → blog.rajan.dev - ~/code/shop # → shop.rajan.dev
# …or with every knob control_host: control.rajan.dev # route this host to the web panel tunnel: name: roost access: emails: [rajan@rajan.dev] include: apps/*.yml # split a big fleet across files apps: - path: ~/code/trackaru domain: trackaru.com # bare apex, own zone category: main # panel grouping: main | utility profile: extras # start on demand seed: true # migrate + seed on up (once) migrate: false # image self-migrates at boot redis: true # shared redis + REDIS_URL (auto-detected) build_env: # build-time env SKIP_ENV_VALIDATION: "1" - path: ~/code/trackaru # a background worker over the same code worker: true # no domain, no route command: bundle exec sidekiq
One tunnel, one wildcard DNS record per suffix, host-header routing inside. That architecture is why adding an app is a purely local change with zero API calls.
| Signal in the folder | Framework | Port | Start command |
|---|---|---|---|
Gemfile + config/application.rb | rails | 3000 | puma, bound to 0.0.0.0 |
Gemfile + config.ru + sinatra | sinatra | 4567 | rackup |
package.json with next | next | 3000 | npm run start |
package.json with vite | static | 80 | built, served by Caddy |
package.json with express | node | 3000 | npm run start |
manage.py + requirements/pyproject | django | 8000 | gunicorn |
index.html, no manifest | static | 80 | served by Caddy |
Also inferred: runtime version, database need (one shared MySQL/Postgres, one database per app), and memory caps. Detection is explainable — roost detect prints the exact signal — and never guesses silently.
roost web serves a control panel — start, stop, add, and remove apps and watch health at a glance. It runs as a host process outside the stack, so stopping the apps can never take down the thing that restarts them. Set control_host: to route a hostname through the tunnel, put Cloudflare Access in front, and reach it from anywhere.
Apps grouped into Main / Utilities / Workers, each with a live status pill and a colour-coded memory bar. A needs attention strip surfaces anything that isn't running, over metric cards for running, memory, and stopped.
Start or stop the whole stack, or a single app. Add one from a path — the panel runs roost doctor first, then builds and starts just that app, streaming each step. Remove drops it and keeps it for one-click re-add; shared databases are never touched.
Search, a status filter, list or grid views, light and dark mode, and it's mobile-responsive. The dashboard you'd expect — generated for you, no front-end to maintain.
Loopback (127.0.0.1:4600) by default, with a --token bearer check on the actions. Behind Cloudflare Access it's a private control plane for your fleet — the Add form builds real code, so it never faces the open internet.
Prerequisites: Docker, and a domain on Cloudflare. You create one API token — roost automates every other Cloudflare step, including all DNS records. There is no "add this CNAME in your dashboard" step.
$ brew install cdrrazan/tap/roost $ roost init # picks your domain from your live zone list $ roost auth login # paste the API token (init links the page + scopes) $ roost doctor # every failure comes with a specific fix $ roost tunnel setup # tunnel + ALL DNS records via API $ roost up # build, start, route — live on HTTPS $ roost enable # come back up at every login
A day in the life after setup: check what's running, tail a log, park heavy apps in a profile, and let the doctor explain anything odd.
| Command | What it does |
|---|---|
roost init | interactive setup; picks your domain from your live Cloudflare zone list, scans a folder for apps, writes explicit hostnames |
roost add / remove | edit the app list — your config comments are preserved |
roost list / detect | resolved apps and URLs; detection with the signal that triggered it |
roost generate | write ~/.roost/build/* without starting anything |
roost up [--profile] [--reseed] [--no-seed] / down | start the whole stack (staggered, memory-capped), migrate + seed DB apps (--no-seed skips seeding for a clean start) / stop it |
roost start / stop / restart | bring one app up, take it down, or bounce it — without touching the rest of the stack |
roost status / logs | state, health, memory vs cap, public URL; tail one app's logs |
roost doctor | preflight everything — Docker, token scopes, zones, DNS shadowing, SSL depth — with a fix per failure |
roost tunnel setup | create the tunnel + every DNS record via API; idempotent, refuses to clobber records it didn't create |
roost tunnel access | apply the Cloudflare Access policy across every suffix |
roost auth login | verify and store the API token (~/.roost/credentials, 0600) |
roost enable / disable | run at login via launchd (macOS) / systemd --user (Linux); fully removable |
Full schema and hostname rules: configuration reference · runnable configs: examples, including a fully-populated demo.
The failure modes that cost hours are encoded, tested, and checked by roost doctor.
Cloudflare terminates TLS and forwards plain HTTP; roost sets RAILS_ASSUME_SSL so force_ssl apps don't redirect forever.
Free Universal SSL covers one subdomain level. Doctor flags hostnames nested deeper — before you hit an unexplainable browser TLS error.
An exact DNS record silently beats a wildcard. roost pre-flights every record and names the collision and both ways out.
Databases and apps never publish host ports; everything rides the internal network. One shared MySQL/Postgres, one database per app.
Hostnames leak via Certificate Transparency within hours. Set access.emails and every suffix gets an auth wall before first up.
Memory caps per app, single-worker Rails defaults, staggered starts so six apps don't spike your CPU at once.
A per-app database user so apps that connect as their own username and use Solid Cache/Queue/Cable (sibling <app>_* databases) just work.
After (re)starting containers Caddy is reloaded and cloudflared refreshed on new routes — no empty responses from a dead upstream, no 404 on a just-added zone.
Database apps are migrated every up; add seed: true (or a command) and they seed once — tracked in state.json, --reseed to repeat, --no-seed to skip for a clean start. No manual db:prepare after a fresh start.
Label-driven tools automate tunnel routing for containers you already maintain. roost starts one step earlier: from source folders.
| roost | DockFlare / TunnelDock | Coolify | |
|---|---|---|---|
| Input | a list of source folders | running containers + labels | git repos, web UI |
| Dockerfiles & Compose | generated for you | you write them | buildpacks / you |
| DNS strategy | one wildcard per suffix | per-hostname API records | your server's DNS |
| Adding an app | local change only | labels + API calls | UI / git push |
| Runs on | your laptop | your Docker host | a server you operate |
Every database runs in the shared MySQL/Postgres container, and its files live in a named Docker volume (roost-mysql-data) — decoupled from the containers. Rebuilding images or recreating containers never touches it; only deleting the volume does.
Yes — the engine stops, so apps, Caddy, MySQL, and cloudflared all stop and every hostname goes unreachable. Your data is safe on disk. When Docker restarts, containers carry restart: unless-stopped and come back automatically if they were running when it stopped. Enable Docker Desktop's Start on login for hands-off recovery — roost has no daemon; Docker's restart policy is the supervisor.
No. down removes the containers, not the named volume; up recreates the containers and reattaches the same volume. Every database, user, and row is intact — no migrate, no re-seed. Same for an image rebuild after you change app code.
| Action | Databases |
|---|---|
roost down / up / image rebuild | kept |
docker system prune -f | kept — named volumes untouched |
docker compose down -v | dropped |
docker volume rm roost-mysql-data | dropped |
docker system prune --volumes / --all --volumes | dropped |
| Docker Desktop → Troubleshoot → Clean / Purge data | everything gone |
To reclaim space without losing data, use docker system prune -f — it keeps named volumes. Clean / Purge data is the nuclear option: it wipes the entire Docker VM (all containers, images, and volumes), so every database and all demo data is gone and the next roost up rebuilds every image, recreates empty databases from mysql-init.sql, and needs a fresh migrate + re-seed. Back the volume up first if you want it reversible.
Yes — roost web serves a control panel: a live status table with whole-stack Start apps / Stop apps buttons and per-row Start / Stop for each app. It runs as a host process outside the stack, so stopping the apps can't take down the thing that starts them back up. Per-app actions are name-checked against your config, so the panel can never toggle an infra container. It binds 127.0.0.1:4600 by default — expose it only behind Cloudflare Access (control_host: routes a hostname to it), with --token as defense-in-depth. An unprotected panel lets anyone who reaches it stop and start your stack.
Yes. An Add an app form takes a host path (+ optional hostname); it runs roost doctor first and, only if preflight passes, edits the config, regenerates, and builds + starts just the new app — the Processing pane streams each step. Per-row Remove (with a free disk option to drop the image) stops the container, removes it from the config, and lists it under Removed apps for one-click re-add. Shared DB volumes are never touched, so data survives. The Add form builds any path you give it — keep the panel behind Cloudflare Access.
No. Stop stops only the app containers; Caddy and cloudflared — the proxy and tunnel — keep running, so the panel stays reachable and can bring the apps back. Start resumes the stopped containers rather than rebuilding, so a full stack returns in seconds. Only roost down in the terminal removes the whole stack, tunnel included.
Yes — nothing ties it to a laptop. It's a Go binary driving Docker, so it runs on any always-on VPS or cloud VM if you want the apps up 24/7. To move: install roost + Docker, copy ~/.roost/config.yml and credentials, restore your DB dumps, then roost up. The tunnel is outbound — no ports to open. Run cloudflared from one machine at a time; two connectors on the same token split traffic.
Push to the repo, then run roost deploy <app> on the host. It does a fast-forward-only git pull on that app's clone and rebuilds + restarts only that container, leaving the rest up. Diverged clone? The deploy fails loudly instead of silently merging. roost still only reads your repo — never writes to it.
Yes — roost deploy is built to run from CI over SSH. A GitHub Actions workflow on push: branches: [main] SSHes to the box with a deploy key and runs roost deploy <app>. Fast-forward-only pull means a force-push or diverged branch surfaces as a failed deploy, not a bad merge.
roost enable installs a boot unit — launchd on macOS, systemd --user on Linux — that runs roost up at login. On a headless Linux box, also run loginctl enable-linger <user> so units start at boot without a login. Docker must start on boot too (default on a server install); its restart: unless-stopped policy is the supervisor — roost has no daemon.
Your apps roost while the laptop is open and leave when it closes. Lid shut — apps down; lid open — cloudflared reconnects in seconds. For demos, previews, and personal projects, that's the whole point — and it says so plainly. Want 24/7? The same config runs unchanged on a small always-on box: copy config.yml and credentials, restore your database dumps, roost up, and drive the fleet from the web panel. Same tool, your call on where it lives.
Creator & maintainer of roost — Ruby and Go developer building tools that keep side projects fun. roost exists because putting ten local apps online should be one config file, not ten YAML rituals.