Docker Compose
The repo ships docker-compose.prod.yml, which runs the full stack on one box: Postgres, the
API, the click router and the portal, behind Caddy, which gets TLS
certificates from Let’s Encrypt on its own.
The plain docker-compose.yml in the repo root is for local development. It runs only
Postgres, on host port 5433, for the quickstart. Every command on this page
names the production file with -f docker-compose.prod.yml.
What runs
| Service | What it does | Port |
|---|---|---|
postgres | Postgres 16. Data lives in the postgres-data volume. | 5432, internal only |
api | The JSON API, webhooks and scheduled jobs. Runs migrations on start. | 4601, internal only |
router | Partner link redirects and the _cref attribution cookie. | 4701, internal only |
portal | The admin and partner UI. Proxies /api/* to the API. | 8080, internal only |
caddy | TLS and routing for the three public hostnames. | 80, 443 and 443/udp on the host |
Only Caddy is reachable from outside the box. It serves three hostnames:
| Hostname | Default | Serves |
|---|---|---|
PORTAL_HOST | portal.<OPENPARTNER_DOMAIN> | The portal |
API_HOST | api.<OPENPARTNER_DOMAIN> | The API |
ROUTER_HOST | go.<OPENPARTNER_DOMAIN> | Partner links, e.g. https://go.yourdomain.com/r/<linkKey> |
Partners put router links in their content, so keep that hostname short.
Requirements
- Docker Engine 24+ with the Compose plugin (
docker compose) - 2GB RAM minimum (4GB+ recommended for production)
- DNS records for the three hostnames, pointing at the box
- Ports 80 and 443 open to the internet, so Caddy can get certificates (443/udp too if you want HTTP/3)
Configure
git clone https://github.com/getcoherence/openpartner.gitcd openpartnercp .env.example .env.env.example is written for local development, so edit .env before the first start. The
production compose file reads these variables from it:
| Variable | Needed | Notes |
|---|---|---|
OPENPARTNER_DOMAIN | Yes | Your domain, e.g. yourdomain.com. Not in .env.example, so add it. |
CADDY_EMAIL | Yes | Email for Let’s Encrypt registration. Not in .env.example, so add it. |
ADMIN_API_KEY | Yes | Bootstrap admin key. Replace the placeholder with node -e "console.log('op_' + require('crypto').randomBytes(24).toString('hex'))". |
SECRETS_ENCRYPTION_KEY | Yes | 32 bytes, hex or base64 (openssl rand -hex 32). The API won’t start in production without it. |
COOKIE_DOMAIN | Yes, if you copied .env.example | Set it to .yourdomain.com, or delete the line. .env.example sets localhost, which the router would use as-is. When it’s unset, the router uses .<OPENPARTNER_DOMAIN>. |
POSTGRES_PASSWORD | Recommended | Defaults to openpartner. Set it before the first start: Postgres reads it only when it creates the database. POSTGRES_USER and POSTGRES_DB also default to openpartner. |
METRICS_TOKEN | Recommended | The API host is public, so set this to require a bearer token on /metrics. |
PORTAL_HOST, API_HOST, ROUTER_HOST | No | Override the default hostnames above. |
OPENPARTNER_ENABLE_SCHEDULER | No | On unless set to 0. See Scheduled jobs. |
OPENPARTNER_MODE | No | Leave it at selfhost. |
POSTMARK_SERVER_TOKEN, MAIL_FROM | No | Mail fallback. You can set up mail in the install wizard instead. |
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET_PLATFORM, STRIPE_WEBHOOK_SECRET_CONNECT | No | Your own Stripe account, to pay partners over Stripe Connect and take in Stripe revenue. Leave them blank to pay partners by hand. |
VELOCITY_MAX, VELOCITY_WINDOW_MS | No | How many clicks one IP can make on one link per window before the rest are flagged for fraud review. Default: 20 per 60,000 ms. |
Only these variables reach the containers. The compose file gives each service an explicit
list rather than the whole .env. Anything else in .env, such as the SMTP_* settings or the
OPENPARTNER_STORAGE_* upload settings, does nothing until you add it to that service’s
environment: block in docker-compose.prod.yml.
Start
docker compose -f docker-compose.prod.yml up -ddocker compose -f docker-compose.prod.yml logs -f apiThe first start builds the API, router and portal images from source. The API runs database migrations before it starts, and won’t start if they fail, so watch its log.
Once Caddy has its certificates, open https://<PORTAL_HOST>. With no admin yet, the portal
sends you to the install wizard at /install, which collects your admin account, program name
and support email, and mail transport (SMTP or Postmark). Accept the magic-link email and you’re
signed in as the first admin. From then on, configuration lives in the admin UI.
ADMIN_API_KEY keeps working as a bootstrap and headless key: paste it into the API key tab on
/login, or send it to the API as a bearer token. Rotate it once you have human admins.
Prebuilt images
Every push to main publishes images to GitHub Container Registry:
ghcr.io/getcoherence/openpartner-api:edge, openpartner-router:edge and
openpartner-portal:edge. To skip the build, replace each service’s build: block in
docker-compose.prod.yml with image: ghcr.io/getcoherence/openpartner-<service>:edge, then:
docker compose -f docker-compose.prod.yml pulldocker compose -f docker-compose.prod.yml up -dScheduled jobs
The API has an in-process scheduler, and this compose file turns it on unless .env sets
OPENPARTNER_ENABLE_SCHEDULER=0. With it on, the API does this unattended:
- Approves commissions after their holdback, daily at 05:15 UTC. Commissions with no holdback wait for you in Admin → Review queue.
- Pays partners, Mondays at 09:00 UTC, on the cadence set in Settings → Payouts → Payout
cadence (weekly unless you change it). Partners with a connected, payouts-enabled Stripe
account get a real Stripe transfer from the account behind
STRIPE_SECRET_KEY, and no funding check stands in front of it. Transfers that don’t complete are retried every 15 minutes. For everyone else, the run records a payout for you to send yourself. - Housekeeping: reminder and cleanup sweeps, and Network sync if you’ve connected to the Network.
With the scheduler off, nothing runs on a timer: you approve commissions in the Review queue and run payouts from the Payouts page by hand. To pay partners by hand while the other jobs run, set Payout cadence to Manual.
Run one API container. Don’t docker compose up --scale api=N with the scheduler on, or a
job can run twice. Run any extra API replicas as a separate service with
OPENPARTNER_ENABLE_SCHEDULER=0.
Updating
git pulldocker compose -f docker-compose.prod.yml up -d --buildWith prebuilt images, run pull and then up -d instead. The API runs any new migrations when
it restarts.
Upgrading from an earlier release turns the scheduler on. Earlier compose files didn’t pass
OPENPARTNER_ENABLE_SCHEDULER, so it was always off. If you aren’t ready for automatic approvals
and payouts, set OPENPARTNER_ENABLE_SCHEDULER=0 in .env before you upgrade.
Backups
The bundled Postgres keeps its data in the postgres-data volume. Dump it at least daily:
docker compose -f docker-compose.prod.yml exec -T postgres pg_dump -U openpartner openpartner > openpartner-$(date +%F).sqlUse your own POSTGRES_USER and POSTGRES_DB if you changed them. You can also keep the volume
on block storage that you snapshot.
Production hardening
- Set up upload storage. Brand logos and creator avatars go to local disk by default
(
/var/lib/openpartner/uploadsinside the API container), but this compose file mounts no volume for them and passes none of the storage settings. Before your brand uploads anything, add one of these to theapiservice’senvironment:block:- S3-compatible storage:
OPENPARTNER_STORAGE_KIND: s3plus the bucket settings in the environment reference. - Local disk: mount a volume, point
OPENPARTNER_STORAGE_FS_DIRat it, and make it writable by the API container’snodeuser (UID 1000). Also setOPENPARTNER_STORAGE_FS_PUBLIC_BASEtohttps://<API_HOST>/uploads. Otherwise stored URLs point athttp://localhost:4601/uploads.
- S3-compatible storage:
- Use managed Postgres for real traffic.
DATABASE_URLis written into theapiandrouterservices in the compose file, not read from.env. Change it in both, then remove thepostgresservice and thedepends_onentries that name it. - Protect
/metricswithMETRICS_TOKEN. - Rotate
SECRETS_ENCRYPTION_KEYonly with a planned re-encrypt of stored secrets.
See the environment reference for all settings.