Skip to content

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-1gb Postgres (bump to db-s-2vcpu-4gb for more than 50 users).
  • basic-xs for api (1 GB, OOMs on basic-xxs under load).
  • basic-xxs for ws and web.

Operator action items

  1. Click the Deploy button or doctl apps create --spec .do/app.yaml.
  2. Replace every REPLACE_ME in the spec with output of openssl rand -hex 32 (or -base64 32 for DOABLE_KEK).
  3. Set INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT to ISO8601 = now + 24h.
  4. After first deploy, enable required Postgres extensions:
    doctl databases sql <db-id> --command \
      "CREATE EXTENSION IF NOT EXISTS vector; \
       CREATE EXTENSION IF NOT EXISTS pg_trgm; \
       CREATE EXTENSION IF NOT EXISTS pgcrypto;"
    
  5. 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

  1. Connect the repo via Dashboard, New, then Blueprint. Render reads render.yaml.
  2. Render auto-generates the six secrets marked generateValue: true. You still need to fill the variables marked sync: false:
  3. The four NEXT_PUBLIC_* plus CORS_ORIGINS once the Render hostnames are known. Render assigns https://<service>-<hash>.onrender.com.
  4. INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT.
  5. Optional AI provider keys, OAuth, Stripe.
  6. The api service runs node /app/services/api/dist/db/migrate.js as preDeployCommand before 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

  1. Click the Railway deploy button.
  2. Railway generates the six shared secrets automatically (the manifest's shared block, lines 125-133 of railway.json).
  3. Set INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT to ISO8601 = now + 24h.
  4. 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.json line 17).
  5. 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

  1. Click the Heroku deploy button. Heroku's prompt requests every required env var (JWT_SECRET, ENCRYPTION_KEY, INTERNAL_SECRET, DOABLE_KEK, INSTALL_BOOTSTRAP_TOKEN are generated by the secret generator).
  2. Fill in the URL contract pointing at https://<your-app>.herokuapp.com.
  3. Set INSTALL_BOOTSTRAP_TOKEN_EXPIRES_AT manually.
  4. Optional: fill any AI provider key.
  5. The postdeploy script runs the migrations (app.json line 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