Self-host
Self-host OfferKit with the published container image on Docker or Railway, configure Postgres and Redis, and verify the application and worker.
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=…
# WEBHOOK_SECRET_ENCRYPTION_KEY=… # openssl rand -base64 32
# ADMIN_EMAIL=…
# ADMIN_PASSWORD=…
# OFFERKIT_PUBLIC_URL=http://localhost:3000
docker compose up -dweb 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
Authentication requests must come from the exact origin configured in OFFERKIT_PUBLIC_URL. To open OfferKit from another computer on your network, set the public URL in .env to the address used in that computer’s browser:
OFFERKIT_PUBLIC_URL=http://192.168.1.108:3000If the same instance should accept sign-ins through additional URLs, list those exact origins as a comma-separated allowlist:
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.1.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.1.0 and 0.1.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.1.0 instead of latest.
Set these variables on web:
DATABASE_URL=postgres://...
REDIS_URL=redis://...
ADMIN_EMAIL=…
ADMIN_PASSWORD=…
BETTER_AUTH_SECRET=…
WEBHOOK_SECRET_ENCRYPTION_KEY=…
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://...
WEBHOOK_SECRET_ENCRYPTION_KEY=…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=…
WEBHOOK_SECRET_ENCRYPTION_KEY=…
OFFERKIT_PUBLIC_URL=https://your-diploi-domain
ADMIN_EMAIL=…
ADMIN_PASSWORD=…Required worker environment variables:
DATABASE_URL=postgres://...
REDIS_URL=redis://...
WEBHOOK_SECRET_ENCRYPTION_KEY=…
WORKER_HEALTH_PORT=9091Use 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.