This FAQ collects common deployment failures and the checks that resolve them.
Use placeholders such as open.example.com and never paste production secrets
into GitHub issues, pull requests, or documentation.
.env.production is not automatically loaded by Docker Compose. Add the env
file to every Compose command that reads the production file:
docker compose \
--env-file deploy/.env.production \
-f deploy/docker-compose.production.yml \
psThis also applies to logs, config, up, and exec.
The runtime image must install the production dependency that starts the API. Pull the latest code and rebuild the API image without using an old image:
git pull --ff-only
docker compose \
--env-file deploy/.env.production \
-f deploy/docker-compose.production.yml \
up -d --build apiThe current image keeps tsx in runtime dependencies and installs it with
npm ci --omit=dev.
Check the values used by the running Compose project:
docker compose \
--env-file deploy/.env.production \
-f deploy/docker-compose.production.yml \
configDo not print or share the resulting secret values. If the PostgreSQL volume was already initialized with a different password, changing the environment file alone does not change the existing database password. Back up the data and use a deliberate PostgreSQL password-rotation procedure instead of deleting the volume.
This means an obsolete UUID-to-numeric migration is being applied to a
baseline that already creates numeric IDs. The current 006_numeric_app_user_ids.sql
and 007_numeric_company_ids.sql are ordering-preserving no-op migrations for
fresh databases.
Pull the latest code and retry without deleting database volumes:
git pull --ff-only
docker compose \
--env-file deploy/.env.production \
-f deploy/docker-compose.production.yml \
up -d --build apiIf a historical database has not applied those migrations and still uses the old UUID schema, stop and take a backup before attempting any manual migration.
Check which process owns the ports:
ss -ltnp | grep -E ':(80|443|8080)\b'If Hostinger Traefik owns 80 and 443, do not stop it if other applications use it. Map the OpenBcon Caddy service to loopback HTTP instead:
ports:
- "127.0.0.1:8080:80"Then configure a Traefik router for the public hostname and point it to
http://127.0.0.1:8080 when Traefik uses host networking. The project Caddy
must use HTTP-only routing in this architecture. The Bootstrap Setup wizard
creates the proxy override and mounts deploy/Caddyfile.http automatically.
The primary deploy/Caddyfile is reserved for direct Caddy HTTPS mode.
This means DNS resolved, but the public HTTPS connection could not complete a certificate handshake. Check which proxy owns port 443 and inspect its recent logs:
ss -ltnp | grep -E ':(80|443|8080)\\b'
docker compose --env-file deploy/.env.production \
-f deploy/docker-compose.production.yml logs --tail=200 caddyFor direct Caddy mode, the site address should be the public hostname, for
example www.example.com {, not http://www.example.com {. For Traefik mode,
Caddy should listen only on 127.0.0.1:8080 and Traefik should terminate TLS
with its Let’s Encrypt resolver. Do not use curl -k as the final fix; it
bypasses certificate verification.
A self-signed certificate usually means the public request reached a default Traefik certificate instead of a router with the Let’s Encrypt resolver.
Check:
dig +short A open.example.com
dig +short AAAA open.example.com
docker compose logs --tail=200 traefikConfirm that the hostname router has:
tls:
certResolver: letsencryptDo not treat curl -k as a solution. It skips certificate verification.
When a bind-mount source file does not exist, Docker can create a directory at that path. Remove only the accidental empty directory and create a regular file:
file /etc/traefik/dynamic.yml
if [ -d /etc/traefik/dynamic.yml ]; then rmdir /etc/traefik/dynamic.yml; fiUse an absolute mount to remove ambiguity:
- /etc/traefik/dynamic.yml:/etc/traefik/dynamic.yml:roRecreate Traefik after correcting the mount:
docker compose up -d --force-recreate traefikDo not use down -v; the ACME certificate volume must be preserved.
Test each layer separately:
curl -i http://127.0.0.1:8080/api/health
curl -i https://open.example.com/api/healthIf the private request succeeds and the public request fails, inspect Traefik router and service logs. If both fail, inspect the project API and Caddy logs:
docker compose \
--env-file deploy/.env.production \
-f deploy/docker-compose.production.yml \
logs --tail=200 api caddyCheck the Python container logs and health endpoint:
docker compose \
--env-file deploy/.env.production \
-f deploy/docker-compose.production.yml \
logs --tail=200 pythonThe Python service needs the database and MongoDB connection values from the
production env file. Live generation also requires a valid server-side
OPENBCON_OPENAI_API_KEY; never put that key in frontend variables.
Check API and Caddy health first, then force-refresh the browser. For a local development server, restart Vite after moving or renaming a source module:
npm run devA successful production build is also useful:
npm run buildInclude:
- the OpenBcon commit or image tag
- the operating system and Docker version
- the sanitized Compose command
- relevant redacted logs
- the output of
docker compose ps - whether the failure is local, VPS-internal, or public HTTPS
Never include passwords, API keys, cookies, acme.json, database dumps, or
customer data.