Skip to content

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): A record pointing to the server, plus api. and ws. 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:

  1. The PostgreSQL databasepg_dump.
  2. The project filesystemPROJECTS_ROOT/ (default: services/api/projects/ or the api_projects Docker volume).

See Operations → Backups.

Scaling

A single 4-vCPU / 8 GB box handles ~100–500 active users. Beyond that, see Scaling.