Skip to content

Other hosts

Any host that runs containers and gives you Postgres and Redis can run the kit. docker-compose.production.yml is a complete working example. This page is for setting one up by hand; to make a host part of the kit, as code, like Railway and AWS, see Add a host.

The apps share the contracts package, so build from the root:

Terminal window
docker build -f apps/api/Dockerfile -t saas-stack-api .
docker build -f apps/web/Dockerfile -t saas-stack-web .
docker build -f apps/site/Dockerfile -t saas-stack-site \
--build-arg SITE_URL=https://example.com \
--build-arg PUBLIC_APP_URL=https://app.example.com .

The site’s two addresses are baked into its pages, so change them and you rebuild the site.

Container Command Needs
api bun src/main.ts DATABASE_URL, REDIS_URL and its variables
web Caddy API_URL: the API’s private address, such as http://api:3000
site Caddy API_URL: the API’s private address, for the contact form
worker bun src/worker.ts Same as the API. Optional: set JOBS_WORKER=false on the API

A host that gives the database in parts can set PGHOST, PGPORT, PGUSER, PGPASSWORD and PGDATABASE instead of DATABASE_URL. Redis must not evict keys: set maxmemory-policy noeviction, or jobs can be lost.

Every image listens on PORT. Give api no public address: browsers reach it through web under /api. Put HTTPS in front of web and site (Caddy’s own HTTPS is off), and set the API’s WEB_URL to the https:// address.

Run this in the API image with the same DATABASE_URL, before the new version starts:

Terminal window
bun src/db/migrate.ts

If it fails, don’t start the new version. The old version keeps serving while it runs, so migrations must work with the running code: add a column in one release, stop using the old one in the next.

Container Path Healthy when
api /api/health 200; 503 if the database is down
web, site /healthz 200

The worker has no health check. Restart it when it stops. What the API answers, and how to watch it from outside, is on Health checks.

Sign-in is rate limited per visitor address. Caddy in the web image trusts X-Forwarded-For only from private addresses (private_ranges and 100.64.0.0/10) and passes the API a single visitor address. If your host’s proxy reaches the container from a public address, add its range to trusted_proxies in apps/web/Caddyfile and apps/site/Caddyfile, or every visitor shares one limit.

Then check it with bun run deploy:check.