Self-host - Docker Compose
Self-host OpenQuok with Docker Compose — env setup, Supabase, and bring-up of API, web, Redis, and BullMQ workers.
Connect your agent today
Draft from chat, review in your calendar, and publish only what you approve.
Overview
The canonical self-host path builds images from this monorepo and runs API, web, Redis, and BullMQ workers via Compose under infra/self-host/. Use it when you want OpenQuok on your own computer or private network — not the hosted product at openquok.com.
Two Compose files
infra/docker-compose.yml is for contributors developing the hosted OpenQuok product: local Redis and optional CLI-auth Postgres only (API/web still via pnpm). The full app stack for operators is infra/self-host/docker-compose.yml. See System requirements and Docker (local services).
Self-host defaults skip outbound email and Stripe billing so you can sign up and enter the app without a verify inbox or First Billing paywall. Details: email and Stripe.
Environment variables
Copy the example file, then edit infra/self-host/.env. Compose loads it into the API and workers (and agent when using the cli profile) via env_file; the web service does not load the full file. You can also export the same names in the shell before docker compose.
# From repo root
cp infra/self-host/.env.example infra/self-host/.env Required: Supabase
Fill these before first start (hosted project or local supabase start). Apply OpenQuok migrations to that project — see Database & migrations and Supabase.
| Variable | Role |
|---|---|
| PUBLIC_SUPABASE_URL | API / Auth URL |
| PUBLIC_SUPABASE_PUBLISHABLE_KEY | Publishable key |
| SUPABASE_SECRET_KEY | Secret key (server only) |
| VITE_PUBLIC_SUPABASE_URL | Same URL for the web build |
| VITE_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Same publishable key for the web build |
Optional: Social provider apps
Leave these empty until you connect a channel. Fill only the pairs for networks you use, then recreate the API (and workers) so they pick up the new env. Per-provider setup (redirect URIs, app dashboards): Social integration.
| Variable | Channel |
|---|---|
| FACEBOOK_APP_ID / FACEBOOK_APP_SECRET | Facebook Page |
| INSTAGRAM_APP_ID / INSTAGRAM_APP_SECRET | |
| THREADS_APP_ID / THREADS_APP_SECRET | Meta Threads |
| YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET | YouTube |
| TIKTOK_CLIENT_ID / TIKTOK_CLIENT_SECRET | TikTok |
| LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET | LinkedIn / LinkedIn Page |
| X_API_KEY / X_API_SECRET | X |
Dev.to needs no operator app
Dev.to (devto) connects with a personal API key the user pastes in Add Channel. Do not add operator ID/secret env vars for it. Setup: Dev.to.
Self-host friendly defaults
EMAIL_ENABLED=false
STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
DISABLE_REGISTRATION=false
NOT_SECURED=true - EMAIL_ENABLED
=false— no outbound mail; signup auto-verified. - Empty STRIPE_PUBLISHABLE_KEY —
billingEnabled=false; no First Billing paywall. - NOT_SECURED
=truefor plain HTTP on localhost; setfalsebehind an HTTPS reverse proxy. Withtrue, API rate limiting is off by default so the web container’s SSR proxy does not exhaust a shared per-IP limit (see RATE_LIMIT_ENABLED in infra/self-host/.env.example).
Also set SECURITY_SECRET (for example openssl rand -hex 32) and align public URLs:
FRONTEND_DOMAIN_URL=http://localhost:4007
BACKEND_DOMAIN_URL=http://localhost:3000
VITE_FRONTEND_DOMAIN_URL=http://localhost:4007
VITE_API_BASE_URL= Leave VITE_API_BASE_URL empty for this Compose stack. The web container proxies same-origin /api and /uploads to the api service (OPENQUOK_API_PROXY_TARGET in Compose). Setting http://localhost:3000 breaks server-side rendering inside the web container.
Supabase on the host
When API and workers run in Docker but Supabase runs on the host (supabase start), use the API URL from supabase status (typically port 54321, not Postgres 5432) and reach the host from containers via http://host.docker.internal:54321 for PUBLIC_SUPABASE_URL and matching VITE_PUBLIC_SUPABASE_*.
Changing env after build
VITE_* values are baked into the web image at build time. After changing them, rebuild the web service. Other vars usually need a recreate: docker compose -f infra/self-host/docker-compose.yml up -d --force-recreate.
Install and start
Meet system requirements
Confirm Docker / Compose, disk, and a Supabase project per System requirements.
Configure .env
Copy infra/self-host/.env.example to infra/self-host/.env, fill Supabase keys (and matching VITE_*), and set SECURITY_SECRET.
Bring the stack up
From the repository root:
docker compose -f infra/self-host/docker-compose.yml up --build Detach with -d when you want background containers. The web service waits until api passes its /health check so SSR does not call the API before it is listening.
Open the UI
- Web UI: http://localhost:4007
- API: http://localhost:3000
Override the web host port with OPENQUOK_WEB_HOST_PORT if 4007 is taken; then open that port instead.
Sign up with email/password; with the defaults above you should enter the app without email verification or a billing paywall.
Optional: CLI device-flow profile
Postgres + agent server for openquok auth:login device flow:
docker compose -f infra/self-host/docker-compose.yml --profile cli up --build Register an OAuth app (Settings → Developers → Apps) with redirect http://localhost:3111/device/callback, and fill the OPENQUOKOAUTH* / CLI_AUTH_SERVER_URL fields documented in the example env. See Configuration - Agent and Admin — OAuth Server.
Stop the stack
# From repo root
docker compose -f infra/self-host/docker-compose.yml down If you started with —profile cli, include the same profile so Compose tears down those services too:
docker compose -f infra/self-host/docker-compose.yml --profile cli downServices in the stack
| Compose service | Role |
|---|---|
redis | Cache + BullMQ |
api | Express backend |
web | SvelteKit Node adapter |
worker-integration-refresh | BullMQ worker |
worker-notification-email | BullMQ worker |
worker-scheduled-social-post | BullMQ worker |
postgres / agent-server | Only with —profile cli |
Images build from the monorepo root (context: ../..). No registry is required for v1.
Security and exposure
This stack is designed for trusted local / private-network operators, not a multi-tenant public SaaS edge. Keep that in mind before publishing ports to the internet.
| Topic | Default / behavior | Guidance |
|---|---|---|
| Audience | Single operator machine | Prefer localhost; for LAN/VPN only after you understand the defaults below |
| NOT_SECURED | true (HTTP cookies) | Set false behind HTTPS; never leave true on a public origin |
| Registration | DISABLE_REGISTRATION=false | Open signup; with EMAIL_ENABLED=false accounts skip email verify |
| Secrets file | infra/self-host/.env | Gitignored; never commit. Compose loads it into API and workers (and agent when using cli) |
| Web container | No full env_file | Only runtime vars + VITE_* baked at build; do not pass SUPABASE_SECRET_KEY into web |
| Published ports | Web, API (and optional agent / worker health) on the host | Bind to localhost or firewall if the host is multi-user / cloud VPS |
| Redis | No password; not published | Keep unpublished; do not point at a shared production Redis |
| CLI Postgres | Default openquok / openquok; profile cli only | Change credentials if the host is not solely yours |
Danger
Do not treat Compose as a public cloud deploy.Published ports default to all host interfaces. Combined with NOT_SECURED=true, open registration, and secrets in .env, a cloud VM with no firewall is an easy compromise. Use a reverse proxy with TLS, restrict listen addresses or firewall rules, set NOT_SECURED=false, and rotate SECURITY_SECRET / Supabase keys if they ever leaked.
Production notes
- Put a reverse proxy (TLS) in front of web and API when exposing beyond localhost; set NOT_SECURED to
falseand update public URL vars to your HTTPS origins. - SaaS-style deploys on Vercel / Railway remain documented under Production deployment; this page is the self-host Compose path.