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
gitandopenssl. - 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
.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 Or print all six at once:
.env: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
6
Sign up
Open Set up Resend before inviting anyone: Resend.
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: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.
Line by line
Line by line
name: agentsdrnames the Compose project, so containers and thedb-datavolume are prefixedagentsdr.dbusespostgres:18-alpinewith database and useragentsdr.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 volumedb-data(mounted at/var/lib/postgresql), so it survivesdocker compose down. The healthcheck runspg_isreadyevery 5 seconds, up to 20 tries; other services wait for it.setupbuilds the image from theDockerfile(build: .) and tags itagentsdr:local. Its command isbun scripts/db/setup.ts --if-empty.environment: &db-envdefinesDATABASE_URL(built fromPOSTGRES_PASSWORD, hostdb) andDATABASE_SSL: disable, and theappreuses them with<<: *db-env. It starts only afterdbis healthy.restart: "no"means it runs once perdocker compose upand exits.appuses the same image, restarts unless you stop it, and reads all your settings from.env(env_file).environmentis applied afterenv_file, so the composeDATABASE_URLwins over any in.env. It publishes${PORT:-3000}on the host to port 3000 in the container. It starts only aftersetupfinished successfully.cronisalpine:3.22.APP_URLis the internal addresshttp://app:3000, so jobs never leave the Docker network.CRON_SECRETandOUTREACH_TICK_SECRETare required (:?). It mountsdocker/cron/crontab(the schedules) anddocker/cron/entrypoint.shread-only. The entrypoint installscurl, writes the three variables to a file each job sources (busyboxcronddoes not pass the environment to jobs), loads the crontab and runscrondin the foreground.volumes: db-data:declares the named volume.
What the cron sidecar runs
What the cron sidecar runs
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 bundleddb service is the default. To use a PostgreSQL 16+ you already run (a hosted one, for example) instead, add to .env:
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.
What docker-compose.external-db.yml does
What docker-compose.external-db.yml does
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
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
Set POSTGRES_PASSWORD in .env
Set POSTGRES_PASSWORD in .env
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.Port 3000 is already in use
Port 3000 is already in use
Set
PORT=3001 (any free port) in .env and run docker compose up -d again.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.