Try it locally
Run the three production images together on your machine to catch problems before a host does.
Start it
Section titled “Start it”The API refuses the AUTH_SECRET from .env.example in production, so put a real one in
.env first:
openssl rand -base64 32Then:
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.
Check it
Section titled “Check it”bun run deploy:check http://localhost:8080 --site http://localhost:8081It 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.
Stop it
Section titled “Stop it”bun run production:down # keeps the datadocker compose -f docker-compose.production.yml down -v # deletes itA 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.