Skip to main content
This is the recommended install. One command starts PostgreSQL, creates the schema, runs the app and runs the scheduler for periodic jobs.
What you need
  • A Linux, macOS or Windows machine with Docker and Docker Compose (Compose 2.24 or newer only if you use your own database).
  • A terminal and git and openssl.
  • About 15 minutes. The first start builds the image, which takes a few minutes.
  • For a public server: a domain pointing at it (see Going to production).

Overview

docker compose up -d starts four services:
1

Clone the repository

2

Copy the example environment file

Compose reads .env from this folder. It is ignored by git and excluded from the Docker build, so secrets never enter the image.
3

Generate the secrets

Each secret is a random 64-character hex string. Run this once per secret and paste each result into .env:
Or print all six at once:
Keep a copy of INTEGRATION_CREDENTIALS_KEY somewhere outside the server (a password manager). It encrypts every saved integration credential; if you lose it or change it, every integration has to be reconnected. BETTER_AUTH_SECRET signs sessions; changing it signs everyone out. Changing UNSUBSCRIBE_SECRET breaks unsubscribe links in email already sent.
4

Edit .env

Open .env and set at least the values below. Replace everything in the block that says PASTE. Leave the other lines as they are.
DATABASE_URL and DATABASE_SSL in .env are ignored in this mode: docker-compose.yml sets them for the app and setup services. The bundled db service does not use TLS, so Compose sets DATABASE_SSL=disable for you. Every variable is described in the configuration reference.Optional: PORT (the host port the app is published on, default 3000) if port 3000 is taken on the host. BETTER_AUTH_URL is read at runtime, so one image works for any domain.
5

Start everything

The first run builds the image (Bun install and a Next.js build), starts the database, creates the schema, then starts the app and the scheduler.
6

Sign up

Open http://localhost:3000 (or your BETTER_AUTH_URL) and go to /sign-up. Create the first account, verify it, and create your organization at /onboarding (a name and a URL slug). You become its owner. The first account on a fresh install can always be created; after that sign-up is invite-only by default. See First sign-up and invite-only.Without a Resend account yet, the verification email is written to the app log. Read the link with:
Set up Resend before inviting anyone: Resend.
7

Connect your services

In Settings, open the Connection page of each channel you want and connect the services you need: see Integrations.

The full docker-compose.yml

This is the file in the repository root. You normally do not edit it; configuration goes in .env.
  • name: agentsdr names the Compose project, so containers and the db-data volume are prefixed agentsdr.
  • db uses postgres:18-alpine with database and user agentsdr. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} makes Compose stop with that message if the value is missing. The data lives in the named volume db-data (mounted at /var/lib/postgresql), so it survives docker compose down. The healthcheck runs pg_isready every 5 seconds, up to 20 tries; other services wait for it.
  • setup builds the image from the Dockerfile (build: .) and tags it agentsdr:local. Its command is bun scripts/db/setup.ts --if-empty. environment: &db-env defines DATABASE_URL (built from POSTGRES_PASSWORD, host db) and DATABASE_SSL: disable, and the app reuses them with <<: *db-env. It starts only after db is healthy. restart: "no" means it runs once per docker compose up and exits.
  • app uses the same image, restarts unless you stop it, and reads all your settings from .env (env_file). environment is applied after env_file, so the compose DATABASE_URL wins over any in .env. It publishes ${PORT:-3000} on the host to port 3000 in the container. It starts only after setup finished successfully.
  • cron is alpine:3.22. APP_URL is the internal address http://app:3000, so jobs never leave the Docker network. CRON_SECRET and OUTREACH_TICK_SECRET are required (:?). It mounts docker/cron/crontab (the schedules) and docker/cron/entrypoint.sh read-only. The entrypoint installs curl, writes the three variables to a file each job sources (busybox crond does not pass the environment to jobs), loads the crontab and runs crond in the foreground.
  • volumes: db-data: declares the named volume.
From docker/cron/crontab (UTC): build-queue at 00:05 daily, mailboxes/watch at 03:30 daily, LinkedIn run-outreach every 15 minutes, run-search-queue and replay-webhooks every 10 minutes, reset-daily-limits at 00:00. Adjust the schedules to your sending windows by editing that file and running docker compose up -d --force-recreate cron. See the jobs table.

Using your own PostgreSQL

The bundled db service is the default. To use a PostgreSQL 16+ you already run (a hosted one, for example) instead, add to .env:
and run docker compose up -d as usual. The db service is not started, and the setup service creates the schema in your database on first start, so the database must be empty then. POSTGRES_PASSWORD is unused in this mode; leave the example value. This needs Docker Compose 2.24 or newer (the override file uses !reset). If your database has no TLS, set DATABASE_SSL=disable. If it is a managed database, leave DATABASE_SSL unset or empty.
It puts db behind a profile (bundled-db) that nobody passes, so it never starts; clears setup’s dependency on db; and replaces DATABASE_URL and DATABASE_SSL on setup and app with the values from your .env (failing with “Set DATABASE_URL in .env to your PostgreSQL” if missing).

Check that it works

/sign-in loads in the browser and docker compose ps shows app as running. The app has no dedicated health endpoint; a page loading is the check.

Stop, restart and update

To update to a newer version, back up first, pull the code, run any migration the release notes name, then rebuild:
Updating an existing install is not done with db:setup. The migration steps are in Upgrading and backups in Backups and restore.
Do not scale app to more than one replica: the outreach sender loop has no distributed lock, and two instances would each send.

Troubleshooting

Compose stops with this when the variable is empty or missing. Set it in .env (not in .env.local). The same applies to CRON_SECRET and OUTREACH_TICK_SECRET.
Set PORT=3001 (any free port) in .env and run docker compose up -d again.
More: Troubleshooting.

Next

Going to production

Domain, HTTPS, email, backups, upgrades.

Integrations

Connect Gmail, Unipile, R2, OpenRouter.

Configuration reference

Every environment variable.

Troubleshooting

Real error messages and fixes.