Skip to main content
Find the message you see, then follow cause and fix. Messages are quoted exactly as the code prints them. For the first look at any problem, read the logs: docker compose logs app (or the Logs tab in Dokploy).

Database setup

Symptom: bun run db:setup fails with This database already has tables (…). db:setup only initialises an empty database; an existing install is upgraded with the migrations in scripts/.Cause: db:setup refuses any database that already contains tables in the public schema, so it can never overwrite an install.Fix: For a first install, point DATABASE_URL at an empty database (create a new one). To upgrade an existing install, run the migrations instead: Upgrading. In Docker Compose the setup service passes --if-empty, which prints Database already initialised — nothing to do. and succeeds.
Cause: db:setup checks the server version and refuses anything below 16.Fix: Use PostgreSQL 16, 17 or 18. With Compose the bundled database is PostgreSQL 18; this error appears only with your own server.
Cause: The process cannot see DATABASE_URL. From source the file is .env.local in the repository root, and you must run the command from there. On Dokploy the variable is missing from the Environment tab or the app was not redeployed.Fix: Set it and restart. With Compose .env is used and the URL is built for you from POSTGRES_PASSWORD.
Cause: Compose requires these three and stops with Set POSTGRES_PASSWORD in .env, Set CRON_SECRET in .env or Set OUTREACH_TICK_SECRET in .env.Fix: Put them in .env next to docker-compose.yml (generate with openssl rand -hex 32). With your own database you also need Set DATABASE_URL in .env to your PostgreSQL.
Cause: The app connects with TLS unless DATABASE_SSL=disable. A database without TLS (a local one, Dokploy’s Postgres) fails the handshake.Fix: Set DATABASE_SSL=disable for a database without TLS. For a hosted database leave it unset or empty. Compose sets it for the bundled db.
Symptom: bun run db:seed:demo prints Refusing to seed demo data: NODE_ENV=production. or Refusing to seed demo data into "host": it is not localhost/127.0.0.1. …Cause: The demo data is fictional and must not land in a real database.Fix: Run it only against a local database, without NODE_ENV=production. Pass --allow-remote only if you are sure.

Database connections

Symptom: every page and every query fails at once.Cause: PostgreSQL’s max_connections is used up. It is shared by every deployment pointing at the server, and idle connections from other deployments count. A rolling deploy briefly runs two containers, each with its own pool of DB_POOL_MAX (default 8). This is capacity, not a code bug.Fix: Check select count(*) from pg_stat_activity on the server, stop what is holding connections, lower DB_POOL_MAX, or raise the server’s max_connections. Do not raise DB_POOL_MAX before checking.

Sign-in and sign-up email

Cause: Without RESEND_API_KEY nothing is sent. The log shows [auth/email] RESEND_API_KEY is not set: verification, password-reset and invitation emails are written to this log instead of being sent. Set RESEND_API_KEY and AUTH_EMAIL_FROM before inviting anyone. and the message itself as [auth/email] (not sent — no RESEND_API_KEY) to=… subject="…".Fix: For the first account, copy the link from docker compose logs app. For everyone else set RESEND_API_KEY and AUTH_EMAIL_FROM (an address on a domain verified in Resend) and restart. See Resend. If both are set and mail still does not arrive, check the Resend dashboard for rejected sends and the domain’s verification status.
Cause: AUTH_SIGNUP defaults to invite-only. After the first account, only an email address with a pending, unexpired invitation (they expire after 7 days) gets an account; anyone else gets the normal “check your email” response and nothing is created.Fix: Invite the person from Settings, Members, and have them sign up with that exact address. For a public deployment set AUTH_SIGNUP=open. See Who may sign up.
Cause: BETTER_AUTH_SECRET changed or is not set to the same value as before.Fix: Restore the previous value. Keep it stable.

Secrets

Cause: The variable is empty. Saving an integration needs it.Fix: Set a 64-character secret (openssl rand -hex 32) and restart.
Cause: A stored credential could not be decrypted, usually because INTEGRATION_CREDENTIALS_KEY differs from the key used when it was saved (changed, lost or different between instances).Fix: Restore the original key. If it is lost, reconnect each integration. To change the key on purpose, re-encrypt with bun run rotate:integration-credentials (new key in INTEGRATION_CREDENTIALS_KEY, old in LEGACY_INTEGRATION_CREDENTIALS_KEYS).
Cause: Sending email builds a signed unsubscribe link and fails without the secret.Fix: Set UNSUBSCRIBE_SECRET and restart. Changing it later breaks links in email already sent.

Sending and scheduled jobs

Cause: The daily /api/outreach/build-queue job never ran. It fills each mailbox’s queue and resets daily counters; the in-process sender has nothing to send without it. Other causes: the app is not running with NODE_ENV=production (the sender runs only in production, so not under bun run dev), or PAUSE_EMAIL_OUTBOUND / PEOPLE_MIGRATION_MODE is on.Fix: Check that the cron service is running (docker compose logs cron should show agentsdr cron: schedules loaded) or that your external cron calls it. Run it once by hand: curl -fsS -X POST -H "x-tick-secret: $OUTREACH_TICK_SECRET" "$APP/api/outreach/build-queue". See Scheduled jobs.
Cause: The secret sent does not match the app’s. OUTREACH_TICK_SECRET is unset in the app (it answers 401 then), or the caller sends a different value. For /api/linkedin/jobs/* without a valid Authorization: Bearer $CRON_SECRET the request is treated as an ordinary browser call and redirected to sign-in or refused.Fix: Use the same value in the app and the caller. Outreach endpoints take x-tick-secret (or ?secret=); LinkedIn endpoints take Authorization: Bearer. After editing .env, recreate the containers: docker compose up -d.
Cause: PEOPLE_MIGRATION_MODE or PAUSE_CAMPAIGN_MUTATIONS is on, so POST, PUT, PATCH and DELETE calls to campaign, LinkedIn job and mailbox routes answer 503, including your scheduled jobs.Fix: Set them to false (or remove them) and restart. See PAUSE switches.
Cause: Gmail push watches expire after about 7 days and /api/outreach/mailboxes/watch is not scheduled.Fix: Schedule it once a day. See Gmail reply sync.
Cause: The app runs more than one instance, or /api/outreach/tick is also scheduled. The outreach sender loop has no distributed lock.Fix: Run exactly one app replica and do not schedule /api/outreach/tick.

Integrations and webhooks

Symptom: a feature or page shows Unipile is not connected — connect it in Settings → LinkedIn → Connection, Google Workspace is not connected — connect it in Settings → Email → Connection or the Cloudflare R2 equivalent (Settings → WhatsApp → Integrations). API calls answer 409.Cause: Integrations are per organization and stored in the database. There is no environment fallback, so putting keys in .env does nothing. The organization you are signed in to has not connected the service, or its stored credentials could not be read (see “Stored integration credential is invalid” above). Background jobs for that feature return early.Fix: Open the named Settings page and connect the service. See Integrations.
Symptom: the Unipile settings show … is a local address that Unipile's servers cannot reach. Webhooks are registered automatically once the app runs at its public address. or BETTER_AUTH_URL is not set, so there is no public address for Unipile to call.Cause: Unipile’s servers cannot reach localhost, so registration is skipped.Fix: Run the app at a public HTTPS address, set BETTER_AUTH_URL to it, and save the Unipile integration again. For local development use a tunnel and set BETTER_AUTH_URL to the tunnel address. See Unipile.
Cause: The extension only talks to AgentSDR addresses it knows.Fix: Add your address in the extension’s Options and reload the open AgentSDR tab. See WhatsApp call recorder extension.

Next

Going to production

The full go-live checklist.

Configuration reference

Every environment variable.