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 Minimum 2GB RAM, 2 CPU cores, 20GB storage and linux distribution with minimum security
  • 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: Follow the official guide (required for the setup CLI)

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)
npx @inbox-zero/cli setup
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
git clone https://github.com/elie222/inbox-zero.git
cd inbox-zero
npm install
npm run setup
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:
npx inbox-zero setup-google --project-id YOUR_PROJECT_ID --domain yourdomain.com
This command enables required APIs, creates the Pub/Sub topic and subscription, and guides you through OAuth credential creation. You can also copy .env.example to .env and set the values yourself. If doing this manually edit then you’ll need to configure:
  • 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. Note: If you only use Microsoft OAuth, set GOOGLE_CLIENT_ID=skipped and GOOGLE_CLIENT_SECRET=skipped. Note: The first section of .env.example variables that are commented out. If you’re using Docker Compose leave them commented - Docker Compose sets these automatically with the correct internal hostnames.

3. Deploy

Pull and start the services with your domain:
NEXT_PUBLIC_BASE_URL=https://yourdomain.com docker compose --profile all up -d
The pre-built Docker image is hosted at ghcr.io/elie222/inbox-zero:latest and will be automatically pulled. Important: The NEXT_PUBLIC_BASE_URL must be set as a shell environment variable when running docker compose up (as shown above). Setting it in apps/web/.env will not work because docker-compose.yml overrides it.

Using External Database Services (Optional)

The docker-compose.yml supports different deployment modes using profiles:
ProfileDescriptionUse when
--profile allIncludes Postgres and Redis containersDefault, simplest setup
--profile local-redisLocal Redis onlyUsing managed Postgres (RDS, Neon, Supabase)
--profile local-dbLocal Postgres onlyUsing managed Redis (Upstash, ElastiCache)
(no profile)No local databasesUsing managed services for both (production recommended)
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:
# Check that containers are running (STATUS should show "Up")
docker ps
# Check logs. This can take 30 seconds to complete
docker logs inbox-zero-services-web-1 -f

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 scheduled tasks automatically:
TaskFrequencyEndpointCron ExpressionDescription
Scheduled actionsEvery minute/api/cron/scheduled-actions* * * * *Executes delayed/scheduled actions when QStash is not configured
Email watch renewalEvery 6 hours/api/watch/all0 */6 * * *Renews Gmail/Outlook push notification subscriptions
Meeting briefsEvery 15 minutes/api/meeting-briefs*/15 * * * *Sends pre-meeting briefings to users with the feature enabled
Follow-up remindersEvery hour/api/follow-up-reminders0 * * * *Processes follow-up reminder notifications
If you’re not using Docker Compose you need to set up cron jobs manually:
# Scheduled actions - every minute (only needed when QStash is not configured)
* * * * * curl -s -X GET "https://yourdomain.com/api/cron/scheduled-actions" -H "Authorization: Bearer YOUR_CRON_SECRET"

# Email watch renewal - every 6 hours
0 */6 * * * curl -s -X GET "https://yourdomain.com/api/watch/all" -H "Authorization: Bearer YOUR_CRON_SECRET"

# Meeting briefs - every 15 minutes (optional, only if using meeting briefs feature)
*/15 * * * * curl -s -X GET "https://yourdomain.com/api/meeting-briefs" -H "Authorization: Bearer YOUR_CRON_SECRET"

# Follow-up reminders - every hour (optional, only if using follow-up reminders feature)
0 * * * * curl -s -X GET "https://yourdomain.com/api/follow-up-reminders" -H "Authorization: Bearer YOUR_CRON_SECRET"
Replace YOUR_CRON_SECRET with the value of CRON_SECRET from your .env file.

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:
FeatureWithout QStashWith QStash
Email digest✅ Works (sync, no retries)✅ Full support
Delayed/scheduled email actions✅ Works via cron fallback✅ Full support
AI categorization of senders*✅ Works (sync)✅ Works (async with retries)
Bulk inbox cleaning*✅ Works (sync, no throttling)✅ Full support
*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:
QUEUE_BACKEND=qstash
QSTASH_TOKEN=your-qstash-token
QSTASH_CURRENT_SIGNING_KEY=your-signing-key
QSTASH_NEXT_SIGNING_KEY=your-next-signing-key

Option 2: BullMQ Worker

If you’re already running Docker Compose with Redis, you can use the built-in worker service instead of QStash:
QUEUE_BACKEND=bullmq
REDIS_URL=redis://redis:6379
Then start the worker profile:
docker compose --profile queue-worker up -d
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:
# Clone the repository
git clone https://github.com/elie222/inbox-zero.git
cd inbox-zero

# Install dependencies and configure environment (auto-generates secrets)
npm install
npm run setup
nano apps/web/.env

# Build and start
docker compose build
NEXT_PUBLIC_BASE_URL=https://yourdomain.com docker compose --profile all up -d
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:
AUTO_JOIN_ORGANIZATION_ENABLED=true
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:
INSERT INTO "Member" ("id", "emailAccountId", "organizationId", "role", "createdAt")
SELECT gen_random_uuid(), ea.id, '<YOUR_ORG_ID>', 'member', now()
FROM "EmailAccount" ea
WHERE NOT EXISTS (
  SELECT 1 FROM "Member" m WHERE m."emailAccountId" = ea.id
)
ON CONFLICT ("emailAccountId") DO NOTHING;
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:
AUTO_ENABLE_ORG_ANALYTICS=true
This only affects new memberships created after the setting is enabled. Existing members keep their current stored value.