One-Click PaaS Deploys¶
Doable ships manifests for every major full-stack PaaS. Pick the platform that matches where you already host things. The same prebuilt images from ghcr.io/doable-me/doable-* back every path, so each deploy is about 30 seconds of work once the secrets are filled in.
| Platform | Manifest | Button | Postgres |
|---|---|---|---|
| DigitalOcean App Platform | .do/app.yaml |
Deploy | Managed PG 16 |
| Render | render.yaml |
Deploy | Managed PG 16 |
| Railway | railway.json |
Deploy | pgvector/pgvector:pg16 |
| Heroku | app.json |
Deploy | Heroku Postgres essential-0 |
| GitHub Codespaces | .devcontainer/devcontainer.json |
Open | Local Postgres in dev container |
All PaaS paths use the prebuilt ghcr.io images intentionally. The Next.js source build peaks at about 2 GB of RAM and OOMs on most free tiers. See the platform-specific notes for the sizing each manifest assumes.
Common secrets¶
Every path needs the same six secrets. Most PaaS UIs auto-generate them when the manifest marks them as generator: secret.
| Variable | Purpose | Generate with |
|---|---|---|
JWT_SECRET |
Signs JWT access + refresh tokens | openssl rand -hex 32 |
ENCRYPTION_KEY |
Symmetric encryption for legacy column-encryption paths | openssl rand -hex 32 |
INTERNAL_SECRET |
HMAC for internal api to ws auth on loopback | openssl rand -hex 32 |
DOABLE_KEK |
Key-encryption-key for envelope-encrypted wizard secrets. Never roll, rolling equals data loss. | openssl rand -base64 32 |
INSTALL_BOOTSTRAP_TOKEN |
Single-use signup token that grants platform-owner on first signup | openssl rand -hex 32 |
INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT |
ISO8601 timestamp, recommended install time + 24h | Manual, e.g. 2099-01-01T00:00:00Z |
URL contract on every platform (substitute the host the PaaS assigns to web):
NEXT_PUBLIC_API_URL=https://<host>/api
NEXT_PUBLIC_WS_URL=wss://<host>/ws
NEXT_PUBLIC_APP_URL=https://<host>
CORS_ORIGINS=https://<host>
DigitalOcean App Platform¶
Spec file: .do/app.yaml. It declares a Managed Postgres 16 cluster, a PRE_DEPLOY migrate job, and three services (api, ws, web), all sourced from ghcr.io/doable-me/doable-*:latest.
Sizes the manifest assumes
db-s-1vcpu-1gbPostgres (bump todb-s-2vcpu-4gbfor more than 50 users).basic-xsfor api (1 GB, OOMs onbasic-xxsunder load).basic-xxsfor ws and web.
Operator action items
- Click the Deploy button or
doctl apps create --spec .do/app.yaml. - Replace every
REPLACE_MEin the spec with output ofopenssl rand -hex 32(or-base64 32forDOABLE_KEK). - Set
INSTALL_BOOTSTRAP_TOKEN_EXPIRES_ATto ISO8601 = now + 24h. - After first deploy, enable required Postgres extensions:
- Optional: fill any AI provider key (any one is enough to pre-configure step 2 of the wizard).
The migrate job is wired as kind: PRE_DEPLOY, so it runs before api, ws, and web come up on every deploy. The migration image is idempotent.
Render¶
Spec file: render.yaml. It declares a Render-managed Postgres, an api web service, a private ws service (type: pserv), and a web service, all on the starter plan.
Operator action items
- Connect the repo via Dashboard, New, then Blueprint. Render reads
render.yaml. - Render auto-generates the six secrets marked
generateValue: true. You still need to fill the variables markedsync: false: - The four
NEXT_PUBLIC_*plusCORS_ORIGINSonce the Render hostnames are known. Render assignshttps://<service>-<hash>.onrender.com. INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT.- Optional AI provider keys, OAuth, Stripe.
- The api service runs
node /app/services/api/dist/db/migrate.jsaspreDeployCommandbefore every deploy. The script is idempotent.
Sizing
Starter plan is 512 MB. Bump api to Standard (2 GB) once you have more than about 25 users.
Railway¶
Template file: railway.json. It declares api, ws, web, and a pgvector/pgvector:pg16 Postgres service, all pulling from ghcr.io/doable-me/doable-*:latest.
Operator action items
- Click the Railway deploy button.
- Railway generates the six shared secrets automatically (the manifest's
sharedblock, lines 125-133 ofrailway.json). - Set
INSTALL_BOOTSTRAP_TOKEN_EXPIRES_ATto ISO8601 = now + 24h. - The api service's start command runs the migration first:
node /app/services/api/dist/db/migrate.js && exec tmux-entrypoint api npx tsx services/api/src/index.ts(railway.jsonline 17). - Railway wires
${{web.RAILWAY_PUBLIC_DOMAIN}}into the NEXT_PUBLIC URLs automatically, so the URL contract self-configures.
Sizing
Railway gives you an 8 GB sandbox by default. The source-build path still OOMs on it, which is why the template pulls from ghcr.io.
Heroku¶
App spec: app.json. It declares a container stack with the ghcr.io/doable-me/doable-api:latest image and a heroku-postgresql:essential-0 addon.
Operator action items
- Click the Heroku deploy button. Heroku's prompt requests every required env var (
JWT_SECRET,ENCRYPTION_KEY,INTERNAL_SECRET,DOABLE_KEK,INSTALL_BOOTSTRAP_TOKENare generated by thesecretgenerator). - Fill in the URL contract pointing at
https://<your-app>.herokuapp.com. - Set
INSTALL_BOOTSTRAP_TOKEN_EXPIRES_ATmanually. - Optional: fill any AI provider key.
- The
postdeployscript runs the migrations (app.jsonline 92):node /app/services/api/dist/db/migrate.js.
Limitations of the Heroku path
app.json deploys only the api image. For a complete Doable instance with ws and web you need to add the two extra services with the Heroku CLI or use a different PaaS. The Heroku button is best for evaluating the api in isolation.
GitHub Codespaces¶
Dev container: .devcontainer/devcontainer.json. It pulls mcr.microsoft.com/devcontainers/typescript-node:22-bookworm, installs Docker-in-Docker, GitHub CLI, Node 22, and pnpm, then runs .devcontainer/post-create.sh.
Resource requirements (declared in the manifest)
- 4 CPUs
- 16 GB RAM
- 32 GB disk
Forwarded ports
- 3000 web (Next.js), auto-opens browser on forward
- 4000 api (Hono)
- 4001 ws (Yjs)
- 5432 Postgres
Click the Codespaces button on the README, wait for the container to build, then run pnpm dev inside. Codespaces is intended for development, not production hosting.
Smoke test (any platform)¶
curl https://<your-host>/api/health
# {"status":"healthy","timestamp":"...","checks":{"database":{"status":"up"}}}
curl -I https://<your-host>/
# HTTP/2 200, content-type: text/html
Then open https://<your-host>/auth/register. The first signup becomes the platform owner. You complete the five-step setup wizard at /setup (welcome, sign-in, AI provider, Cloudflare, plans and billing).
Reference¶
- Source manifests:
.do/app.yaml,render.yaml,railway.json,app.json,.devcontainer/devcontainer.json - Image registry:
ghcr.io/doable-me/doable-{api,ws,web,migrate}:latest - Related: Coolify, Fly.io, Kubernetes, Custom Domains