Skip to content

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

ServiceWhat it doesPort
postgresPostgres 16. Data lives in the postgres-data volume.5432, internal only
apiThe JSON API, webhooks and scheduled jobs. Runs migrations on start.4601, internal only
routerPartner link redirects and the _cref attribution cookie.4701, internal only
portalThe admin and partner UI. Proxies /api/* to the API.8080, internal only
caddyTLS 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:

HostnameDefaultServes
PORTAL_HOSTportal.<OPENPARTNER_DOMAIN>The portal
API_HOSTapi.<OPENPARTNER_DOMAIN>The API
ROUTER_HOSTgo.<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

Terminal window
git clone https://github.com/getcoherence/openpartner.git
cd openpartner
cp .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:

VariableNeededNotes
OPENPARTNER_DOMAINYesYour domain, e.g. yourdomain.com. Not in .env.example, so add it.
CADDY_EMAILYesEmail for Let’s Encrypt registration. Not in .env.example, so add it.
ADMIN_API_KEYYesBootstrap admin key. Replace the placeholder with node -e "console.log('op_' + require('crypto').randomBytes(24).toString('hex'))".
SECRETS_ENCRYPTION_KEYYes32 bytes, hex or base64 (openssl rand -hex 32). The API won’t start in production without it.
COOKIE_DOMAINYes, if you copied .env.exampleSet 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_PASSWORDRecommendedDefaults 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_TOKENRecommendedThe API host is public, so set this to require a bearer token on /metrics.
PORTAL_HOST, API_HOST, ROUTER_HOSTNoOverride the default hostnames above.
OPENPARTNER_ENABLE_SCHEDULERNoOn unless set to 0. See Scheduled jobs.
OPENPARTNER_MODENoLeave it at selfhost.
POSTMARK_SERVER_TOKEN, MAIL_FROMNoMail fallback. You can set up mail in the install wizard instead.
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET_PLATFORM, STRIPE_WEBHOOK_SECRET_CONNECTNoYour 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_MSNoHow 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

Terminal window
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml logs -f api

The 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:

Terminal window
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

Scheduled 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

Terminal window
git pull
docker compose -f docker-compose.prod.yml up -d --build

With 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:

Terminal window
docker compose -f docker-compose.prod.yml exec -T postgres pg_dump -U openpartner openpartner > openpartner-$(date +%F).sql

Use 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/uploads inside 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 the api service’s environment: block:
    • S3-compatible storage: OPENPARTNER_STORAGE_KIND: s3 plus the bucket settings in the environment reference.
    • Local disk: mount a volume, point OPENPARTNER_STORAGE_FS_DIR at it, and make it writable by the API container’s node user (UID 1000). Also set OPENPARTNER_STORAGE_FS_PUBLIC_BASE to https://<API_HOST>/uploads. Otherwise stored URLs point at http://localhost:4601/uploads.
  • Use managed Postgres for real traffic. DATABASE_URL is written into the api and router services in the compose file, not read from .env. Change it in both, then remove the postgres service and the depends_on entries that name it.
  • Protect /metrics with METRICS_TOKEN.
  • Rotate SECRETS_ENCRYPTION_KEY only with a planned re-encrypt of stored secrets.

See the environment reference for all settings.