Deployment: Docker + nginx + Let's Encrypt¶
The recommended production path. One script, three modes.
TL;DR¶
git clone https://github.com/doable-me/doable.git
cd doable
# Public domain with auto-SSL:
DOMAIN=app.example.com EMAIL=you@example.com sudo ./docker/setup.sh
# Private network — server's LAN IP, self-signed SSL:
HOST=192.168.1.50 sudo ./docker/setup.sh
# Localhost only:
sudo ./docker/setup.sh
# Behind another TLS proxy (Cloudflare, ingress controller, …):
DOMAIN=app.example.com sudo ./docker/setup.sh --skip-ssl
What gets installed¶
| Component | Where |
|---|---|
| Docker images: postgres, api, ws, web, (redis) | Containers — see docker/docker-compose.yml |
| nginx | Host (apt) — config in /etc/nginx/sites-enabled/doable |
| certbot | Host (apt) — only when domain mode + Let's Encrypt |
| UFW | Host — opens 22, 80, 443 only |
What docker/setup.sh does (step by step)¶
- Pre-flight — checks Docker + Compose v2 are installed.
- Mode detection — domain → Let's Encrypt; HOST/localhost → self-signed.
- Generates
docker/.envwith randomJWT_SECRET,ENCRYPTION_KEY,INTERNAL_SECRET, and the rightNEXT_PUBLIC_*URLs. - Installs nginx + certbot if not present.
- Issues SSL cert — Let's Encrypt for domains, OpenSSL self-signed otherwise.
- Renders
nginx.conf.templatewith your hostname → writes to/etc/nginx/sites-enabled/doable. docker compose up --build -d— pulls/builds images, starts everything.docker compose run --rm migrate— applies DB migrations.- Configures UFW — deny incoming, allow 22/80/443.
nginx routing¶
The template at docker/nginx.conf.template maps:
| Path | Backend |
|---|---|
/api/* |
127.0.0.1:4000 (api) |
/auth/* |
127.0.0.1:4000 (api) |
/ws (Upgrade: websocket) |
127.0.0.1:4001 (ws) |
/preview/* |
127.0.0.1:4000 (api → reverse-proxies to per-project Vite) |
| everything else | 127.0.0.1:3000 (web / Next.js) |
Edit the template and re-run setup.sh to regenerate the live config.
After install¶
# Containers status
docker compose -f docker/docker-compose.yml ps
# Tail all logs
docker compose -f docker/docker-compose.yml logs -f
# Reload nginx after editing the conf
sudo nginx -t && sudo systemctl reload nginx
Adding/changing AI keys¶
Edit docker/.env, then:
Renewing SSL¶
certbot installs a systemd timer that renews automatically. Force a renewal:
Updating Doable¶
cd doable
git pull
docker compose -f docker/docker-compose.yml up --build -d
docker compose -f docker/docker-compose.yml run --rm migrate
Behind Cloudflare proxy (orange cloud)¶
Use --skip-ssl. nginx serves on port 443 with a self-signed cert; Cloudflare's "Full (strict)" mode validates against the CA — switch Cloudflare to "Full" (non-strict) instead, or upload the cert to Cloudflare → Origin Certificates.
In the Cloudflare dashboard, set:
- SSL/TLS mode: Full
- Always Use HTTPS: On
- WebSockets: On (under Network)
Behind a Kubernetes ingress controller¶
You typically run Doable in Kubernetes by not using the host nginx at all. Instead, scale the three services as Deployments and put your ingress (e.g. nginx-ingress, Traefik) in front. The bundled docker/setup.sh is host-oriented; for k8s, use the Compose file as a reference and write your own manifests / Helm chart.
Troubleshooting¶
See Troubleshooting. Highlights:
Set JWT_SECRET …on container start —docker/.envis missing required secrets. Re-runsetup.shor paste them in by hand.- Browser shows nginx default page —
setup.shcouldn't replace the default site.sudo rm /etc/nginx/sites-enabled/default && sudo systemctl reload nginx. 502 Bad Gateway— the API is still booting afterup; wait 30s. Checkdocker logs doable-api.