Self-host
Run the published Docker image locally or on Railway. No source build required.
Local: Docker compose
The provided docker-compose.yml boots four services. web and worker use the same published image, ghcr.io/offerkit/offerkit:
web— Next.js dashboard + REST API on:3000worker— long-running Node process pulling jobs out of the queuepostgres— Postgres 17 on:5432redis— Redis 7 on:6379
cp .env.example .env
# Required:
# DATABASE_URL=postgres://offerkit:dev@postgres:5432/offerkit
# BETTER_AUTH_SECRET=…
# ADMIN_EMAIL=…
# ADMIN_PASSWORD=…
docker compose up -dIf you use webhooks, set WEBHOOK_SECRET_ENCRYPTION_KEY to a stable value of at least
32 characters before starting web and worker. Generate one with openssl rand -base64 32.
Deployments without webhooks can omit it.
web runs migrations before starting Next.js, so a fresh DB is fully migrated on first boot. The worker reclaims orphaned jobs on startup and re-seeds the recurring loyalty.points.expire sweep if it’s missing.
Access from another device
With OFFERKIT_PUBLIC_URL unset, OfferKit derives the exact origin of each authentication request. To open OfferKit from another computer on your network, browse directly to the server’s address:
http://192.168.1.108:3000This is still same-origin validation: a page on another origin cannot authenticate against the server. No broad LAN allowlist or disabled CSRF check is used.
Set OFFERKIT_PUBLIC_URL for production deployments, reverse proxies, OAuth clients, or hosted MCP, where OfferKit needs one canonical external origin. If a configured deployment should accept authentication from additional browser origins, list them explicitly:
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000Do not use a broad wildcard or disable Better Auth’s origin check. After changing either variable, recreate the web container with docker compose up -d --force-recreate web.
Hosted MCP with account login
Set this optional variable on the web service to expose a Streamable HTTP MCP server:
OFFERKIT_MCP_ENABLED=trueThe endpoint is ${OFFERKIT_PUBLIC_URL}/mcp and uses OAuth to sign each MCP client into
an OfferKit account. Use HTTPS in production and ensure OFFERKIT_PUBLIC_URL is the exact
external origin, because it is also the OAuth issuer and callback origin. No MCP variable
is needed on the worker service. Authorized clients can be revoked from Settings →
Agent connections.
Pin a release by setting OFFERKIT_IMAGE_TAG, for example:
OFFERKIT_IMAGE_TAG=v0.2.0 docker compose up -dImage channels are intentionally narrow: edge follows the latest main commit, latest follows the newest stable version, and stable versions are published as both v0.2.0 and 0.2.0.
Contributors who need to test local source changes can build the same image shape:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --buildProduction: Railway
Railway should deploy the published image directly. Do not connect the monorepo as a GitHub source.
In one Railway project, create:
- Postgres
- Redis
webDocker Image service usingghcr.io/offerkit/offerkit:latestworkerDocker Image service usingghcr.io/offerkit/offerkit:latest
For production, pin both image services to a version tag such as ghcr.io/offerkit/offerkit:v0.2.0 instead of latest.
Set these variables on web:
DATABASE_URL=postgres://...
REDIS_URL=redis://...
ADMIN_EMAIL=…
ADMIN_PASSWORD=…
BETTER_AUTH_SECRET=…
OFFERKIT_PUBLIC_URL=https://your-domainweb uses the image default command and should expose a public domain. Configure its healthcheck path as /api/v1/ready.
Set these variables on worker:
DATABASE_URL=postgres://...
REDIS_URL=redis://...If you use webhooks, also set WEBHOOK_SECRET_ENCRYPTION_KEY to the same stable value of
at least 32 characters on both services.
Override the worker start command:
node apps/worker/dist/index.jsworker should not expose a public domain. Configure its healthcheck path as /health.
Production: Diploi
Diploi should use the same deployment shape as Docker compose and Railway:
web— public HTTP component fromghcr.io/offerkit/offerkit, exposes:3000worker— private/background component fromghcr.io/offerkit/offerkit, commandnode apps/worker/dist/index.js, exposes health on:9091postgres— managed Postgres service wired into both components asDATABASE_URLredis— Redis service wired into both components asREDIS_URL; BullMQ stores background jobs here
Required web environment variables:
DATABASE_URL=postgres://...
REDIS_URL=redis://...
BETTER_AUTH_SECRET=…
OFFERKIT_PUBLIC_URL=https://your-diploi-domain
ADMIN_EMAIL=…
ADMIN_PASSWORD=…Required worker environment variables:
DATABASE_URL=postgres://...
REDIS_URL=redis://...
WORKER_HEALTH_PORT=9091If you use webhooks, also set WEBHOOK_SECRET_ENCRYPTION_KEY to the same stable value of
at least 32 characters on both components.
Use the README’s Launch with Diploi button if you want to start from Diploi’s project wizard. After the first deploy, verify /api/v1/ready on web, :9091/health on worker, and sign in with the seeded admin account.
Observability
The image is OpenTelemetry-ready out of the box. Set:
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.axiom.co
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <AXIOM_TOKEN>,X-Axiom-Dataset=offerkit…and you’ll get traces and metrics from apps/web and apps/worker in the same dataset, separable by service.name. Set OTEL_SDK_DISABLED=true to opt out entirely.
Multi-environment
The model is “run two deploys” — each environment is its own Railway project with its own Postgres. There’s no concept of a logical environment within a single deployment. Staging and production roll forward independently.