Skip to main content
For the fastest setup, see the Quick Start.
This guide covers production deployment on a VPS using Docker and Docker Compose.

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:
  1. Docker Engine: Follow the official guide and the Post installation steps
  2. 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)
This downloads the Docker Compose file and .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)
Option B: From the cloned repo
The setup wizard will walk you through configuring Google and/or Microsoft OAuth, choosing an AI provider, and generating secrets. Optional: Automated Google Cloud Setup If you have the gcloud CLI installed, you can automate API enabling and Pub/Sub setup:
This works whether or not you have cloned the repository. If you installed the CLI globally, use 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_ID and GOOGLE_CLIENT_SECRET
  • LLM Provider: Uncomment one provider block and add your API key
  • Optional: Microsoft OAuth, external Redis, etc.
For detailed configuration instructions, see the Environment Variables Reference. If you want remote email images to load through a separate privacy-preserving proxy, see the Image Proxy guide. Note: If you only use Microsoft OAuth, set GOOGLE_CLIENT_ID=skipped and GOOGLE_CLIENT_SECRET=skipped. Note: Leave the commented connection variables at the beginning of .env.example unset when using Docker Compose. Compose supplies the correct internal hostnames automatically.

3. Deploy

Pull and start the services with your domain:
The pre-built Docker image is hosted at ghcr.io/elie222/inbox-zero:latest and will be automatically pulled. Important: docker compose reads NEXT_PUBLIC_BASE_URL from your shell environment or from a root .env file next to docker-compose.yml. Setting it only in apps/web/.env will not work because Compose resolves that override before the container loads apps/web/.env. If you use a custom port, set WEB_PORT the same way.

Using External Database Services (Optional)

The docker-compose.yml supports different deployment modes using profiles: For external services, set the appropriate environment variables in apps/web/.env:
  • External Postgres: Set DATABASE_URL and DIRECT_URL
  • External Redis: Set UPSTASH_REDIS_URL and UPSTASH_REDIS_TOKEN

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)
Note: For production deployments, you should set up a reverse proxy (like Nginx, Caddy, or use a cloud load balancer) to handle SSL/TLS termination and route traffic to your Docker container.

Scheduled Tasks

The Docker Compose setup includes a cron 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:
Replace 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.
When neither QStash nor BullMQ is configured, we fall back to internal API calls and cron for scheduled actions. This works without extra infrastructure, but lacks built-in retries, deduping, and queue-level backpressure. Features that benefit from QStash: *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:
Then start the worker profile:
This runs a separate worker process that pulls jobs from Redis and forwards them to the web app over the internal network. By default the worker subscribes to the built-in queues. If you need to tune which queues it listens to, or adjust concurrency, you can override WORKER_QUEUES in your environment.

Building from Source (Optional)

If you prefer to build the image yourself instead of using the pre-built one:
Note: Building from source requires significantly more resources (4GB+ RAM recommended) and takes longer than pulling the pre-built image. Having issues? See the Troubleshooting page for solutions to common problems.

Auto-Join Organization

For self-hosted instances where all users should belong to a single organization, set:
New users will automatically join the organization when they sign in. This requires exactly one organization to exist — create one first via the app before enabling this. To retroactively add all existing users to your organization, run this SQL query:
Replace <YOUR_ORG_ID> with your organization’s ID from the Organization table. If you want newly created organization members to start with organization analytics enabled, set:
This only affects new memberships created after the setting is enabled. Existing members keep their current stored value.