Self-hosting with Docker
Self-host OpenPromoter on your server with Docker Compose and PostgreSQL 16. It has every feature with no limits under AGPL-3.0. See the steps.
Self-hosted OpenPromoter has every feature of Cloud with no revenue or partner limits. One container runs the dashboard, partner portal, REST API, tracking endpoints and background worker. It needs PostgreSQL 16 and an SMTP server or a Resend key for email.
Quick start
git clone https://github.com/openpromoter/openpromoter
cd openpromoter
docker compose up
That starts three services:
| Service | URL | What it is |
|---|---|---|
app |
http://localhost:3000 | OpenPromoter |
postgres |
localhost:5432 | PostgreSQL 16 |
mailpit |
http://localhost:8025 | Catches every email the app sends |
The compose file seeds a demo workspace. Sign in with:
- Email:
demo@openpromoter.test - Password:
openpromoter-demo
Set SEED_DEMO=false before the first start to skip the demo data.
Before you go to production
The defaults in docker-compose.yml are for trying OpenPromoter on your laptop. For a real deployment:
- Set
APP_URLto the public URL (for examplehttps://partners.example.com). Tracking links, emails and webhook URLs are built from it. - Generate two long random secrets for
BETTER_AUTH_SECRETandENCRYPTION_KEY(for example withopenssl rand -base64 32). KeepENCRYPTION_KEYsafe: it encrypts Stripe, PayPal and Wise credentials and tax IDs at rest, and they cannot be decrypted without it. - Point
SMTP_URLat a real mail server or setRESEND_API_KEY. Then setEMAIL_FROM. - Use a managed or backed-up Postgres and change the default database password.
- Put the app behind HTTPS.
Environment variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Postgres connection string such as postgres://user:pass@host:5432/db |
APP_URL |
Yes | Public URL of this instance. Default http://localhost:3000 |
BETTER_AUTH_SECRET |
Yes | Secret for sessions and auth tokens. 32+ random characters |
ENCRYPTION_KEY |
Yes | Key for AES-256-GCM encryption of stored secrets. 32+ random characters |
SMTP_URL |
One of these two | SMTP server such as smtp://user:pass@host:587 |
RESEND_API_KEY |
One of these two | Send email through Resend instead of SMTP |
EMAIL_FROM |
Recommended | Sender such as Acme Partners <partners@acme.com> |
GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET |
No | Adds Continue with Google. Callback URL {APP_URL}/api/auth/callback/google |
GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET |
No | Adds Continue with GitHub. Callback URL {APP_URL}/api/auth/callback/github |
ANTHROPIC_API_KEY |
No | Turns on AI features with Anthropic (default model claude-opus-5-5) |
OPENAI_API_KEY |
No | Use OpenAI instead |
OPENAI_BASE_URL |
No | Any OpenAI-compatible endpoint such as Ollama or LM Studio |
AI_MODEL |
No | Override the model name |
AI_PROVIDER |
No | anthropic, openai or none. Chosen automatically from the keys if unset |
HOSTED_MODE |
No | true turns on Cloud plans, limits and Stripe Connect OAuth. Leave false when self-hosting |
STRIPE_CONNECT_CLIENT_ID |
No | Cloud only: lets founders connect Stripe with OAuth |
PLATFORM_STRIPE_SECRET_KEY |
No | Cloud only: OpenPromoter's own Stripe account for plans and OAuth |
PLATFORM_STRIPE_WEBHOOK_SECRET |
No | Cloud only: webhook secret for that account |
RUN_WORKER |
No | true (default) runs the job worker inside the web process |
SEED_DEMO |
No | true seeds the demo workspace on start (compose file only) |
Without any AI variable, every AI feature still works in a reduced form. See AI features.
Each sign-in button shows only when both of its values are set. A Google or GitHub login joins an existing account only when the provider confirms the email. Accounts with two-factor on must sign in with email and password.
Background jobs
The worker sends emails and webhooks, matures commissions after the hold period, ends campaigns, runs fraud scoring and Autopilot, and prepares scheduled payouts. It keeps jobs in a Postgres table and needs no Redis.
With RUN_WORKER=true it runs inside the app process. That suits a single container. If you run several web replicas, the queue is safe to share because jobs are claimed with FOR UPDATE SKIP LOCKED.
Railway
- Create a project from the GitHub repo. Railway builds the
Dockerfileat the root. - Add the PostgreSQL plugin and set
DATABASE_URLto its connection string. - Set
APP_URLto the generated domain (or your custom domain). Also setBETTER_AUTH_SECRET,ENCRYPTION_KEY,EMAIL_FROMand eitherRESEND_API_KEYorSMTP_URL. - Expose port 3000.
Render
- Create a Web Service from the repo with the Docker runtime.
- Create a PostgreSQL instance and copy its internal connection string into
DATABASE_URL. - Add the same variables as for Railway. Use a paid instance type: free instances sleep, and the in-process worker stops while they sleep.
Upgrading
Pull the new version and restart. Database migrations run on start.
git pull
docker compose up --build -d
Read the changelog before upgrading across minor versions.
Connecting Stripe on a local instance
Stripe will not send webhooks to localhost. Run the Stripe CLI and paste the signing secret it prints into Settings > Integrations:
stripe listen --forward-to localhost:3000/api/webhooks/stripe/<workspaceId>
See Stripe for the full setup.