For the fastest setup, see the Quick Start.
Prerequisites
Requirements
- VPS with at least 2 GB RAM, 2 CPU cores, 20 GB storage, and a supported Linux distribution
- Domain name pointed to your VPS IP
- SSH access to your VPS
Step-by-Step VPS Setup
1. Prepare Your VPS
Connect to your VPS and install:- Docker Engine: Follow the official guide and the Post installation steps
- Node.js v24: Follow the official guide (required for the setup CLI and source installation)
2. Setup and Configure
The easiest way to get started is with the Inbox Zero CLI. You can either use it standalone or from within the cloned repo. Option A: Standalone (no clone needed).env template automatically.
Recommended choices for first-time self-hosting:
- PostgreSQL/Redis: Docker Compose
- Full stack: Yes, everything in Docker (especially when running via standalone
npx)
inbox-zero setup-google with the same flags.
The command enables required APIs, creates the Pub/Sub topic and subscription, and guides you through OAuth credential creation. It prints environment variables for you to add to your .env; it does not require a repo checkout.
You can also copy .env.example to .env and set the values yourself.
If you configure the deployment manually, set:
- Google OAuth:
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET - LLM Provider: Uncomment one provider block and add your API key
- Optional: Microsoft OAuth, external Redis, etc.
pnpm dev never start apps/unsubscribe-worker. Leave UNSUBSCRIBE_WORKER_URL unset to keep HTTP unsubscribe (one-click POST, then a simple HTML form, then GET). If you do want isolated browser confirmation for JavaScript preference centers, run that app on a dedicated Linux host with gVisor as described in its README, then set UNSUBSCRIBE_WORKER_URL and UNSUBSCRIBE_WORKER_SECRET on the web app. The web app still tries one-click POST and simple HTML forms first.
Note: If you only use Microsoft OAuth, set GOOGLE_CLIENT_ID=skipped and GOOGLE_CLIENT_SECRET=skipped.
Note: When both the web app and its database/Redis run in Docker, remove or comment out DATABASE_URL, DIRECT_URL, and REDIS_HTTP_URL copied from .env.example. Those example URLs use localhost for running the app on your host; Compose supplies internal Docker hostnames when they are unset. Existing files that still set UPSTASH_REDIS_URL behave the same way. Keep explicit connection URLs when using external services.
3. Deploy
Pull and start the services with your domain:ghcr.io/elie222/inbox-zero:latest and will be automatically pulled.
Important: Compose-time ${...} values are resolved before service-level env_file entries are loaded. If you’re running from a cloned repo, make sure Compose can see the same values as the app by linking or copying apps/web/.env to the repo root as .env. The CLI setup does this automatically for the default configuration and preserves existing user-managed root files. Copied files are refreshed only while their contents match the last CLI-written version; changed copies and retargeted links are preserved with a warning. For manual setup, run ln -s apps/web/.env .env once from the repo root before docker compose up. If a root .env already exists, review it before replacing it, or use docker compose --env-file apps/web/.env --profile all up -d. Shell environment values still take precedence.
For named CLI configurations, use the exact Compose command printed by setup. The generated INBOX_ZERO_ENV_FILE setting selects the same named file for container settings. Existing named files created by older CLI versions also need this setting (for example, INBOX_ZERO_ENV_FILE=./apps/web/.env.staging in a cloned repo, or ./.env.staging for a standalone install). Named configs use the same Compose project and do not create isolated deployments.
Using External Database Services (Optional)
Thedocker-compose.yml supports different deployment modes using profiles:
For external services, set the appropriate environment variables in
apps/web/.env:
- External Postgres: Set
DATABASE_URLandDIRECT_URL - External Redis: Set
REDIS_HTTP_URLandREDIS_HTTP_TOKEN.UPSTASH_REDIS_URLandUPSTASH_REDIS_TOKENstill work.
4. Check Logs
Wait for the containers to start:5. Access Your Application
Your application should now be accessible at:http://your-server-ip:3000(if accessing directly)https://yourdomain.com(if you’ve set up a reverse proxy with SSL)
Scheduled Tasks
The Docker Compose setup includes acron container that handles these scheduled tasks automatically. It runs interval loops rather than a system cron daemon, so the first request is sent when the container starts and subsequent requests use the intervals below.
The bundled Compose service does not run the two daily cleanup endpoints. Schedule
/api/cron/reasoning-retention when REASONING_RETENTION_DAYS or DRAFT_SENT_TEXT_RETENTION_DAYS is configured. Schedule /api/cron/draft-cleanup when users enable automatic cleanup of unused AI-generated drafts.
If you’re not using Docker Compose you need to set up cron jobs manually:
YOUR_CRON_SECRET with the value of CRON_SECRET from your .env file.
These endpoints accept the secret as a
GET header (Authorization: Bearer YOUR_CRON_SECRET, as above) or in a POST JSON body ({ "CRON_SECRET": "YOUR_CRON_SECRET" }).Optional: Background Job Backends
Inbox Zero supports multiple background job backends for self-hosted deployments:- QStash: best when you want a managed queue and are already using Upstash.
- BullMQ worker: best when you’re running Docker or another long-lived environment and want a local durable queue backed by Redis.
- Internal fallback: no extra services, but limited durability and throttling.
*Early access features - available on the Early Access page.
Option 1: QStash
Cost: QStash has a generous free tier and scales to zero when not in use. See QStash pricing. Add your QStash credentials to.env:
Option 2: BullMQ Worker
If you’re already running Docker Compose with Redis, you can use the built-in worker service instead of QStash:WORKER_QUEUES in your environment.
Building from Source (Optional)
If you prefer to build the image yourself instead of using the pre-built one:Auto-Join Organization
For self-hosted instances where all users should belong to a single organization, set:<YOUR_ORG_ID> with your organization’s ID from the Organization table.