What you’ll need
- A host that runs an OCI container — or a Heroku app.
- A PostgreSQL database (managed is recommended).
- An SMTP sender for transactional mail (registration, password reset, magic links).
-
A domain you control, so auth can live at
auth.yourdomain.com.
Five things every deploy needs
Whichever path you pick below, do these:
- Terminate TLS upstream — Caddy, Traefik, nginx, Cloudflare, or your platform’s load balancer. ManyRows speaks plain HTTP behind the proxy.
- Forward
X-Forwarded-Proto: httpsso session cookies get theSecureflag and redirect targets are built correctly. - Set
MANYROWS_BASE_URLto your canonical hostname (or let the first/admin/registerpin it). - Use durable Postgres — managed in production; if you keep the bundled compose Postgres, back the volume up.
- Wire a custom domain + cookie scope so auth cookies are first-party with your app (see the callout below).
Use your own domain forMANYROWS_BASE_URL— a subdomain of your app’s registrable domain (auth.yourdomain.com), not the platform’s default host.*.herokuapp.com,*.fly.dev, and*.onrender.comare on the Public Suffix List, so session cookies set there can’t be shared first-party with your app. Every example below assumesauth.yourdomain.com.
Docker Compose
The bundled docker-compose.yml is production-capable, not just
a local demo. Two ways to take it live:
- Managed Postgres (recommended) — drop the
dbservice and pointDATABASE_URLat your managed instance (RDS, Cloud SQL, Neon, Supabase…). ManyRows holds no local state, so thewebservice is stateless and trivially restartable. - Bundled Postgres — keep the
dbservice for a small single-host install, but change the defaultPOSTGRES_PASSWORDand back themanyrows-dbvolume up.
Either way, set a real MANYROWS_FROM_EMAIL and put the web service behind one of the proxies below.
Standalone container
Any platform that runs an OCI image works — docker run,
Kubernetes, ECS, Cloud Run:
docker build -t manyrows .
docker run -d -p 8080:8080 \
-e DATABASE_URL="postgres://user:pass@host:5432/manyrows?sslmode=require" \
-e MANYROWS_FROM_EMAIL="[email protected]" \
-e MANYROWS_BASE_URL="https://auth.yourdomain.com" \
manyrows
The binary binds $PORT when the platform sets it, falling back
to 8080 — so most platforms wire the port with no extra
config.
Heroku
The image is Heroku-ready: it honours $PORT and defaults to the
prod profile. Heroku’s router terminates TLS and sets
X-Forwarded-Proto, so steps 1–2 are handled for you.
Container registry — simplest, reuses the Dockerfile:
heroku create your-manyrows
heroku addons:create heroku-postgresql:essential-0 # provisions DATABASE_URL
heroku config:set \
MANYROWS_FROM_EMAIL="[email protected]" \
MANYROWS_BASE_URL="https://auth.yourdomain.com"
heroku stack:set container
heroku container:push web && heroku container:release web Binary slug via the Platform API — no Docker; build
a Linux binary locally and push it as a slug. It releases to an app you’ve
already created, and needs jq plus Heroku credentials in ~/.netrc (written by heroku login). The UI bundles come from the committed
build-ui.sh:
bash ./build-ui.sh || { echo "build-ui failed"; exit 1; }
cd manyrows-core
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo dev)
GOARCH=amd64 GOOS=linux go build -ldflags="-X main.Version=${VERSION}" \
-o ../app/web start.go
cd ..
tar czf slug.tgz ./app # Heroku expects a top-level ./app dir -> /app/web Then create, upload, and release the slug:
AppID='your-heroku-app'
slug=$(curl -s -X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/vnd.heroku+json; version=3' \
-d '{"process_types":{"web":"./web"}}' \
-n "https://api.heroku.com/apps/$AppID/slugs")
curl -X PUT -H 'Content-Type:' --data-binary @slug.tgz "$(jq -r '.blob.url' <<< "$slug")"
curl -X POST \
-H 'Accept: application/vnd.heroku+json; version=3' \
-H 'Content-Type: application/json' \
-d "{\"slug\":$(jq '.id' <<< "$slug")}" \
-n "https://api.heroku.com/apps/$AppID/releases" Render
Render builds straight from the Dockerfile, sets $PORT, and terminates TLS + forwards X-Forwarded-Proto at its edge.
Commit a render.yaml blueprint that provisions Postgres and wires
DATABASE_URL for you:
databases:
- name: manyrows-db
plan: basic-256mb
services:
- type: web
name: manyrows
runtime: docker
plan: starter
healthCheckPath: /health
envVars:
- key: DATABASE_URL
fromDatabase:
name: manyrows-db
property: connectionString
- key: MANYROWS_FROM_EMAIL
value: [email protected]
- key: MANYROWS_BASE_URL
sync: false # set to your custom auth domain (auth.yourdomain.com)
Then New → Blueprint in the dashboard and point it at your repo.
The Dockerfile EXPOSEs 8080 and the binary honours $PORT, so the port wires up with no extra config.
Fly.io
fly launch reads the Dockerfile, picks up its EXPOSE 8080 as the internal port, and writes a fly.toml with force_https = true. Fly terminates TLS and forwards X-Forwarded-Proto, so the
proxy checklist is covered.
fly launch --no-deploy # detects the Dockerfile, writes fly.toml
fly postgres create # or point DATABASE_URL at Supabase/Neon/Fly MPG
fly postgres attach <pg-app-name> # sets the DATABASE_URL secret
fly secrets set [email protected] \
MANYROWS_BASE_URL=https://auth.yourdomain.com
fly deploy Put it behind a proxy
Self-hosting the container on your own host? Terminate TLS with a reverse proxy. Caddy is the least config — automatic Let’s Encrypt certificates:
auth.yourdomain.com {
reverse_proxy localhost:8080
}
That’s the whole Caddyfile; Caddy adds X-Forwarded-For,
X-Forwarded-Proto, and X-Forwarded-Host automatically.
nginx, Traefik, or a cloud load balancer work too — just forward X-Forwarded-Proto.
Upgrades and backups
- Upgrades — pull the new image (or push a new slug)
and restart. Schema migrations run automatically on boot. For rollouts that
apply schema separately from the binary, run migrations once out-of-band
and set
MANYROWS_DB_SKIP_MIGRATIONS=trueon the new release. - Backups — managed Postgres gives you automated snapshots.
The auto-generated signing and encryption keys live in the database (
system_secrets), so a Postgres backup captures everything — there’s no separate keystore to save. - Health checks — point your platform’s liveness
probe at
/health(it also reports the running build version).
Going further
Custom-domain setup, cookie scope, and the full environment-variable reference live in the project README and the API docs. Something missing or unclear? Get in touch.