Self-host

Run ManyRows in production

ManyRows ships as a single static binary with the admin UI and end-user auth bundled in — no sidecars, no asset server. Run the container (or a Heroku slug) behind a TLS-terminating proxy, point it at Postgres, and you’re live. Here’s the whole playbook.

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:

  1. Terminate TLS upstream — Caddy, Traefik, nginx, Cloudflare, or your platform’s load balancer. ManyRows speaks plain HTTP behind the proxy.
  2. Forward X-Forwarded-Proto: https so session cookies get the Secure flag and redirect targets are built correctly.
  3. Set MANYROWS_BASE_URL to your canonical hostname (or let the first /admin/register pin it).
  4. Use durable Postgres — managed in production; if you keep the bundled compose Postgres, back the volume up.
  5. Wire a custom domain + cookie scope so auth cookies are first-party with your app (see the callout below).
Use your own domain for MANYROWS_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.com are on the Public Suffix List, so session cookies set there can’t be shared first-party with your app. Every example below assumes auth.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 db service and point DATABASE_URL at your managed instance (RDS, Cloud SQL, Neon, Supabase…). ManyRows holds no local state, so the web service is stateless and trivially restartable.
  • Bundled Postgres — keep the db service for a small single-host install, but change the default POSTGRES_PASSWORD and back the manyrows-db volume 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=true on 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.