Skip to content

Try it locally

Run the three production images together on your machine to catch problems before a host does.

The API refuses the AUTH_SECRET from .env.example in production, so put a real one in .env first:

Terminal window
openssl rand -base64 32

Then:

Terminal window
bun run production:up # builds the images and starts them
What Where
Web app, API http://localhost:8080
Marketing site http://localhost:8081
Caught email http://localhost:8026

It has its own database and ports, so it never touches bun dev or your development data. Migrations run first, and the API starts only if they succeed. Billing is off here: your Stripe and Polar keys are not passed on.

The browser only talks to the web image. Caddy there serves the app and passes /api to the API over the private network, so the API has no public address and there is no CORS to set up.

Terminal window
bun run deploy:check http://localhost:8080 --site http://localhost:8081

It changes nothing, and exits with an error if a check fails. Add --email you@example.com to have the app send you a sign-in link. Check a deployment lists what it looks at.

Terminal window
bun run production:down # keeps the data
docker compose -f docker-compose.production.yml down -v # deletes it

A change you made is only in the images after another production:up. To read a container’s output: docker compose -f docker-compose.production.yml logs api.

Next: Railway or another host.