roost.

Every app on your laptop, live on your own domain.

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.

The whole configuration

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
tunnel:
  name: roost
  access:
    emails: [[email protected]]
include: apps/*.yml   # split a big fleet across files
apps:
  - path: ~/code/trackaru
    domain: trackaru.com   # bare apex, own zone
    profile: extras       # start on demand
    build_env:            # build-time env
      SKIP_ENV_VALIDATION: "1"

How it works

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.

DetectGemfile? package.json? manage.py? — framework, port, DB
GenerateDockerfiles, compose.yml, Caddyfile → ~/.roost/build
Rundocker compose, staggered starts, memory caps
RouteCaddy matches the Host header → app container
Exposecloudflared + wildcard DNS, TLS at the edge

What gets inferred from a bare path

Signal in the folderFrameworkPortStart command
Gemfile + config/application.rbrails3000puma, bound to 0.0.0.0
Gemfile + config.ru + sinatrasinatra4567rackup
package.json with nextnext3000npm run start
package.json with vitestatic80built, served by Caddy
package.json with expressnode3000npm run start
manage.py + requirements/pyprojectdjango8000gunicorn
index.html, no manifeststatic80served 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.

60-second quickstart

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

Everyday usage

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.

Status & logs

$ roost status
APP STATE HEALTH MEMORY URL
blog running healthy 212MiB / 512MiB https://blog.rajan.dev
shop running healthy 198MiB / 512MiB https://shop.rajan.dev
trackaru running healthy 301MiB / 768MiB https://trackaru.com
 
$ roost logs blog -f
blog | Puma starting in single mode...
blog | * Listening on http://0.0.0.0:3000

Add, remove, profiles

$ roost add ~/code/api --domain api.rajan.dev
added ~/code/api to ~/.roost/config.yml
$ roost up
 
$ roost up --profile extras
# heavy apps only start when you ask
 
$ roost remove api
removed api from ~/.roost/config.yml

When something's off, the doctor says exactly what and how to fix it

$ roost doctor
ok docker: installed and running
ok config: ~/.roost/config.yml parses; 6 app(s) resolve to hostnames
warn zone:app.pending.dev: zone pending.dev is pending: nameservers haven't propagated yet
fix: wait for propagation (usually under 24h); records can be created meanwhile
FAIL ssl-depth: app1.demo.rajan.dev is 2 levels below zone rajan.dev; free Universal SSL
covers only ONE subdomain level, so browsers will get an opaque TLS error
fix: (1) flatten to app1.rajan.dev (free, recommended), (2) use a dedicated
one-level domain, or (3) enable Advanced Certificate Manager on rajan.dev

Command reference

CommandWhat it does
roost initinteractive setup; picks your domain from your live Cloudflare zone list, scans a folder for apps, writes explicit hostnames
roost add / removeedit the app list — your config comments are preserved
roost list / detectresolved apps and URLs; detection with the signal that triggered it
roost generatewrite ~/.roost/build/* without starting anything
roost up [--profile] / downstart the whole stack (staggered, memory-capped) / stop it
roost start / stop / restartbring one app up, take it down, or bounce it — without touching the rest of the stack
roost status / logsstate, health, memory vs cap, public URL; tail one app's logs
roost doctorpreflight everything — Docker, token scopes, zones, DNS shadowing, SSL depth — with a fix per failure
roost tunnel setupcreate the tunnel + every DNS record via API; idempotent, refuses to clobber records it didn't create
roost tunnel accessapply the Cloudflare Access policy across every suffix
roost auth loginverify and store the API token (~/.roost/credentials, 0600)
roost enable / disablerun 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.

Built-in guardrails

The failure modes that cost hours are encoded, tested, and checked by roost doctor.

No Rails redirect loops

Cloudflare terminates TLS and forwards plain HTTP; roost sets RAILS_ASSUME_SSL so force_ssl apps don't redirect forever.

The SSL depth trap

Free Universal SSL covers one subdomain level. Doctor flags hostnames nested deeper — before you hit an unexplainable browser TLS error.

No shadowed wildcards

An exact DNS record silently beats a wildcard. roost pre-flights every record and names the collision and both ways out.

No leaked databases

Databases and apps never publish host ports; everything rides the internal network. One shared MySQL/Postgres, one database per app.

Access before exposure

Hostnames leak via Certificate Transparency within hours. Set access.emails and every suffix gets an auth wall before first up.

Laptop-friendly

Memory caps per app, single-worker Rails defaults, staggered starts so six apps don't spike your CPU at once.

Multi-database Rails

A per-app database user so apps that connect as their own username and use Solid Cache/Queue/Cable (sibling <app>_* databases) just work.

Never a stale route

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.

How it compares

Label-driven tools automate tunnel routing for containers you already maintain. roost starts one step earlier: from source folders.

roostDockFlare / TunnelDockCoolify
Inputa list of source foldersrunning containers + labelsgit repos, web UI
Dockerfiles & Composegenerated for youyou write thembuildpacks / you
DNS strategyone wildcard per suffixper-hostname API recordsyour server's DNS
Adding an applocal change onlylabels + API callsUI / git push
Runs onyour laptopyour Docker hosta server you operate

The honest part

This is not always-on hosting.

Your apps roost while the laptop is open and leave when it closes. Lid shut — apps down; lid open — cloudflared reconnects in seconds. roost is for demos, previews, and personal projects, and it says so plainly.

Developer

Rajan Bhattarai

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.