Self-Hosting
Deploy Dubbl with Coolify or Docker Compose for production use.
Coolify (recommended)
Bykandil production path: Docker Compose on Coolify (app + Postgres in one resource) + Traefik TLS.
Create the resource
Coolify → New Resource → Git repo → Docker Compose.
Compose file: /docker-compose.yaml. Base directory: /.
Domains for web: https://backofficecore.bykandil.com:3000
(Coolify routes bare domains to container port 80; :3000 is required).
Health check: /api/health. See docker/COOLIFY.md.
Environment
Copy docker/coolify.env.example from the repo into Coolify env.
Minimum (no separate Postgres / no DATABASE_URL):
AUTH_SECRET=... # openssl rand -base64 32
AUTH_URL=https://your-domain.com
AUTH_TRUST_HOST=true
NEXT_PUBLIC_APP_URL=https://your-domain.comSet NEXT_PUBLIC_APP_URL as a Build Variable as well — it is baked into the client bundle.
Deploy
Deploy. Compose starts Postgres, the app migrates on boot, then Next.js serves traffic. Register the first user — they become site admin.
Set DUBBL_SKIP_MIGRATE=1 only if you apply migrations out-of-band.
Never use pnpm db:push against production.
Docker Compose
services:
web:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://dubbl:dubbl@postgres:5432/dubbl
AUTH_SECRET: change-me-in-production
AUTH_URL: https://your-domain.com
AUTH_TRUST_HOST: "true"
NEXT_PUBLIC_APP_URL: https://your-domain.com
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: dubbl
POSTGRES_PASSWORD: dubbl
POSTGRES_DB: dubbl
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Production Deployment (Compose)
Build and start
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --buildMigrations run automatically via the container entrypoint.
Seed demo data (optional)
Seed is a local/dev convenience (pnpm db:seed). Prefer creating the first
admin via /sign-up on a fresh production instance.
Environment Variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | PostgreSQL connection string |
AUTH_SECRET | Yes | Random secret for JWT signing |
AUTH_URL | Yes | Canonical Auth.js origin (HTTPS in production) |
AUTH_TRUST_HOST | Yes | true behind Coolify/Traefik |
NEXT_PUBLIC_APP_URL | Yes | Public URL of your instance (also as build arg) |
AUTH_GOOGLE_ID | No | Google OAuth client ID |
AUTH_GOOGLE_SECRET | No | Google OAuth client secret |
AUTH_APPLE_ID | No | Apple Sign In service ID |
AUTH_APPLE_TEAM_ID | No | Apple Developer team ID |
AUTH_APPLE_KEY_ID | No | Apple Sign In key ID |
AUTH_APPLE_KEY_BASE64 | No | Base64-encoded .p8 private key |
STRIPE_SECRET_KEY | No | Stripe API key for billing |
STRIPE_WEBHOOK_SECRET | No | Stripe webhook signing secret |
RESEND_API_KEY | No | Resend API key for platform emails |
TRIGGER_PROJECT_REF | Yes | Trigger.dev project reference for production background jobs |
TRIGGER_SECRET_KEY | Yes | Trigger.dev secret key for scheduled tasks and delayed retries |
S3_BUCKET | No | S3 bucket for file uploads |
S3_REGION | No | S3 region |
S3_ACCESS_KEY_ID | No | S3 access key |
S3_SECRET_ACCESS_KEY | No | S3 secret key |
S3_ENDPOINT | No | Custom S3 endpoint (for MinIO, etc.) |
Always set a strong, unique AUTH_SECRET in production. Generate one with openssl rand -base64 32.
Authentication
Dubbl supports three authentication methods:
- Email & Password - Works out of the box, no additional setup needed
- Google OAuth - Requires
AUTH_GOOGLE_IDandAUTH_GOOGLE_SECRETfrom Google Cloud Console - Apple Sign In - Requires
AUTH_APPLE_ID,AUTH_APPLE_TEAM_ID,AUTH_APPLE_KEY_ID, andAUTH_APPLE_KEY_BASE64from Apple Developer. Encode your.p8key file withbase64 -w0 AuthKey_XXXXXXXXXX.p8
For OAuth providers, set the callback URL to https://your-domain.com/api/auth/callback/google (or /apple).
First User Setup
The first user to register on a new instance automatically becomes a site admin. This applies to both email/password and OAuth registration. Site admins can access the admin panel at /admin and configure site-wide settings at /admin/settings.
Registration Controls
Control who can create accounts on your instance:
| Mode | Description |
|---|---|
open | Anyone can register (default) |
invite_only | Only users with an invitation can register |
disabled | No new registrations (except the first user) |
Set via environment variable (REGISTRATION_MODE) or the admin settings page at /admin/settings.
Domain Restrictions
Restrict registration to specific email domains by setting ALLOWED_EMAIL_DOMAINS (comma-separated) or configuring it in admin settings. When set, only users with matching email domains can register or sign in via OAuth.
ALLOWED_EMAIL_DOMAINS="acme.com,example.com"Self-Hosted Unlimited Mode
Without Stripe configured, all organizations automatically receive unlimited Pro features. No billing, subscriptions, or plan limits apply. This is detected automatically based on the absence of STRIPE_SECRET_KEY.
You can also control this behavior from the admin settings page:
- Auto (default) - Unlimited when Stripe is not configured
- Always on - Force unlimited regardless of Stripe config
- Always off - Enforce plan limits even without Stripe
Admin Settings
Site admins can configure all self-hosting settings from /admin/settings:
- Registration mode and allowed email domains
- Organization creation permissions
- Self-hosted unlimited mode toggle
Billing (Stripe)
Billing is fully optional. Without Stripe configured, all users get unlimited Pro features automatically. Site admins can also override plan limits per organization from /admin/organizations.
To enable paid plans, see the Billing & Stripe Setup guide for full instructions on creating products, prices, and configuring webhooks.
Self-hosted instances don't need Stripe. All features are automatically unlocked.
Platform emails (welcome, invitations, login alerts, notification digests) use Resend. Set RESEND_API_KEY to enable these. Without it, platform emails are silently skipped.
Organization-level emails (invoices, reminders, statements) use per-org SMTP configuration set up in Settings > Email.
Background Jobs
Dubbl uses Trigger.dev for scheduled maintenance and delayed retries. Set TRIGGER_PROJECT_REF and TRIGGER_SECRET_KEY in production before accepting traffic.
Scheduled tasks are defined in trigger/scheduled.ts:
- Notification digests every 15 minutes
- Report schedules hourly
- Stripe retry hourly
- Invoicing, bookkeeping, trash purge, and backups daily
Webhook delivery failures schedule exact delayed retry runs, so retries happen at their intended backoff time without polling. If TRIGGER_SECRET_KEY is missing when a webhook delivery fails, that retry run cannot be scheduled.
For local development:
pnpm trigger:devFor production, deploy the Trigger.dev tasks:
pnpm trigger:deployDatabase
Dubbl uses PostgreSQL. Minimum version: 15.
The schema is managed by Drizzle ORM. To apply schema changes:
pnpm db:pushStorage
File uploads (receipts, attachments) require S3-compatible storage:
Standard cloud storage. Set S3_ENDPOINT to https://s3.amazonaws.com or omit it.
Self-hosted S3-compatible storage. Set S3_ENDPOINT to your MinIO server URL.
Cost-effective alternative. Set S3_ENDPOINT to your R2 endpoint URL.
Without S3 configured, file upload features are disabled but the rest of the app works normally.
Reverse Proxy
For production, place Dubbl behind a reverse proxy:
server {
server_name accounting.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}accounting.example.com {
reverse_proxy localhost:3000
}Backups
Back up your PostgreSQL database regularly:
# Dump database
docker compose exec db pg_dump -U dubbl dubbl > backup.sql
# Restore
docker compose exec -T db psql -U dubbl dubbl < backup.sqlSchedule automated backups. Losing financial data can be catastrophic.