Production Deployment¶
This page is the decision tree. Pick one of the deployment paths, then follow the matching detailed guide.
Decide¶
┌──────────────────────────────┐
│ Do you want Docker? │
└──────┬───────────────┬───────┘
│ yes │ no
▼ ▼
Docker + nginx Bare-metal Ubuntu
(recommended) + Caddy + Cloudflare Tunnel
| Path | Use when | Guide |
|---|---|---|
| Docker + nginx + Let's Encrypt | You have a public IP and a domain | Docker + nginx |
| Docker + nginx, behind another proxy | Cloudflare proxy / k8s ingress in front | Docker + nginx (--skip-ssl) |
| Docker on a private network / LAN | No domain, internal use | Docker + nginx (HOST=…) |
| Bare-metal Ubuntu + Caddy + Cloudflare Tunnel | No public IP / want zero open ports | Bare-metal |
| Bare-metal Ubuntu + Caddy | Public IP, prefer no Docker | Bare-metal (skip the Tunnel step) |
All paths give you the same hardened result:
- All app services bind to
127.0.0.1. - One TLS-terminating reverse proxy in front.
- UFW configured to deny everything except SSH/HTTP/HTTPS.
- Strong random secrets generated at install time.
- systemd (or Docker) auto-starts everything on reboot.
Pre-flight checklist¶
Before you start any of the install scripts:
- [ ] Server: fresh Ubuntu 22.04 or 24.04 with sudo/root, 4 GB+ RAM recommended.
- [ ] DNS (if using a domain):
Arecord pointing to the server, plusapi.andws.subdomains for the bare-metal/three-domain layout — the Docker/single-domain layout doesn't need them. - [ ] AI key: at least one of
ANTHROPIC_API_KEY,OPENAI_API_KEY,COPILOT_CLI_PATH. - [ ] Optional: GitHub & Google OAuth client IDs/secrets if you want social login.
- [ ] Optional: Stripe keys if you'll charge users.
- [ ] Cloudflare account (for the Tunnel path).
Post-deploy verification¶
Whichever path you take, verify:
# Only SSH (and your reverse proxy) should be listening publicly
sudo ss -tlnp | grep -v 127.0.0.1 | grep -v '\[::1\]'
# Health check
curl -fsSL https://<host>/api/health
# DB reachable from the API container/process
docker compose -f docker/docker-compose.yml logs api | tail
# or for bare-metal:
journalctl -u doable -n 100
Then open your browser, sign up, and create a project.
Updating¶
cd /path/to/doable
git pull
# Docker:
docker compose -f docker/docker-compose.yml up --build -d
docker compose -f docker/docker-compose.yml run --rm migrate
# Bare-metal:
pnpm install
pnpm db:migrate
sudo systemctl restart doable
See Operations → Upgrading for migrations that need special handling.
Backups¶
You must back up two things separately:
- The PostgreSQL database —
pg_dump. - The project filesystem —
PROJECTS_ROOT/(default:services/api/projects/or theapi_projectsDocker volume).
See Operations → Backups.
Scaling¶
A single 4-vCPU / 8 GB box handles ~100–500 active users. Beyond that, see Scaling.