Skip to content

Docker Setup

Doable's Docker setup uses Compose v2 with a multi-stage Dockerfile. Everything you need is in docker/.

Three deployment modes (one script)

docker/setup.sh handles secrets, nginx, SSL, builds, and firewall in one shot. Pick a mode:

Mode Command Use when
Localhost ./docker/setup.sh Trying it out on your laptop
Private network / LAN HOST=192.168.1.50 ./docker/setup.sh Air-gapped server, Tailscale, home lab
Public domain + Let's Encrypt DOMAIN=app.example.com ./docker/setup.sh Real production
Public domain, behind another proxy DOMAIN=app.example.com ./docker/setup.sh --skip-ssl Cloudflare proxy, ingress controller, etc.

What it does in each mode:

  1. Generates docker/.env with random JWT_SECRET, ENCRYPTION_KEY, INTERNAL_SECRET.
  2. Sets NEXT_PUBLIC_* URLs to match your domain/IP.
  3. Installs nginx if missing.
  4. Obtains Let's Encrypt cert (domain mode) or generates a self-signed cert (IP/localhost mode).
  5. Writes nginx config from nginx.conf.template.
  6. Configures UFW: ports 22, 80, 443 only.
  7. Builds and starts all containers.

Compose services

Defined in docker/docker-compose.yml:

Service Image Port (host) Notes
postgres pgvector/pgvector:pg16 127.0.0.1:5432 Volume: postgres_data
migrate built locally, target migrate Runs once, exits 0
api built locally, target api 127.0.0.1:4000 Hono REST API
ws built locally, target ws 127.0.0.1:4001 WebSocket / Yjs
web built locally, target web 127.0.0.1:3000 Next.js
redis redis:7-alpine 127.0.0.1:6379 Profile redis (opt-in)

All host ports bind to 127.0.0.1 — never to 0.0.0.0. The only public-facing process is the host's nginx (installed by setup.sh).

Manual Compose usage

If you'd rather run Compose directly without setup.sh:

cp docker/.env.example docker/.env
# Edit docker/.env — set the three secrets:
#   JWT_SECRET, ENCRYPTION_KEY, INTERNAL_SECRET (each: openssl rand -hex 32)

docker compose -f docker/docker-compose.yml up --build

Then put your own reverse proxy (nginx, Caddy, Traefik) in front of the three services — or access them directly on 127.0.0.1:3000, :4000, :4001 for development.

With Redis (multi-instance scaling)

echo "REDIS_URL=redis://redis:6379" >> docker/.env
docker compose -f docker/docker-compose.yml --profile redis up --build

The @doable/shared/kv-store.js abstraction auto-switches from in-memory to Redis when REDIS_URL is set.

Image structure (docker/Dockerfile)

The Dockerfile is multi-stage. Targets:

  • base — Node 22 alpine + pnpm.
  • deps — installs prod + dev deps for the workspace.
  • build — runs pnpm build for all packages.
  • migrate — minimal image that runs pnpm db:migrate and exits.
  • api, ws, web — slim runtime images, one per service. Each runs as the unprivileged node user.

This keeps individual service images small (~150–300 MB) while sharing all build steps.

Volumes

Volume Mounted in Purpose
postgres_data postgres Database files
api_projects api User projects (filesystem-backed)
api_thumbnails api Generated preview screenshots
ws_projects ws Shared with API for collab access
redis_data redis Redis persistence (when enabled)

Back these up — see Backups.

Common operations

# Tail logs
docker compose -f docker/docker-compose.yml logs -f
docker compose -f docker/docker-compose.yml logs -f api

# Restart a service
docker compose -f docker/docker-compose.yml restart api

# Run migrations on demand
docker compose -f docker/docker-compose.yml run --rm migrate

# Open psql against the running database
docker compose -f docker/docker-compose.yml exec postgres \
  psql -U doable -d doable

# Stop everything (data preserved)
docker compose -f docker/docker-compose.yml down

# Stop and DELETE all data
docker compose -f docker/docker-compose.yml down -v

Updating

cd doable
git pull
docker compose -f docker/docker-compose.yml up --build -d
docker compose -f docker/docker-compose.yml run --rm migrate

Why nginx in front?

  • Single TLS termination point.
  • Routes /, /api/, /auth/, /ws to the right backend.
  • Lets you mix Doable with other apps on the same host.
  • Required by browsers for HTTPS-only features.

The nginx template is at docker/nginx.conf.template. Edit and re-run setup.sh to regenerate.

→ Prefer non-Docker? See Script-based Setup. Want to install everything manually? See Manual Setup.