> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getinboxzero.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker/VPS Deployment Guide

> Production deployment on your own VPS with Docker and Docker Compose

<Info>
  For the fastest setup, see the [Quick Start](/hosting/quick-start).
</Info>

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](https://docs.docker.com/engine/install) and the [Post installation steps](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
2. **Node.js v24**: Follow [the official guide](https://nodejs.org/en/download) (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)**

```bash theme={null}
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**

```bash theme={null}
git clone https://github.com/elie222/inbox-zero.git
cd inbox-zero
corepack enable
pnpm install
pnpm 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](https://cloud.google.com/sdk/docs/install) installed, you can automate API enabling and Pub/Sub setup:

```bash theme={null}
npx @inbox-zero/cli setup-google --project-id YOUR_PROJECT_ID --domain yourdomain.com
```

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](/hosting/environment-variables).

If you want remote email images to load through a separate privacy-preserving proxy, see the [Image Proxy guide](/hosting/image-proxy).

**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 `UPSTASH_REDIS_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. Keep explicit connection URLs when using external services.

### 3. Deploy

Pull and start the services with your domain:

```bash theme={null}
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**: 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)

The `docker-compose.yml` supports different deployment modes using profiles:

| Profile                 | Description                            | Use when                                                 |
| ----------------------- | -------------------------------------- | -------------------------------------------------------- |
| `--profile all`         | Includes Postgres and Redis containers | Default, simplest setup                                  |
| `--profile local-redis` | Local Redis only                       | Using managed Postgres (RDS, Neon, Supabase)             |
| `--profile local-db`    | Local Postgres only                    | Using managed Redis (Upstash, ElastiCache)               |
| *(no profile)*          | No local databases                     | Using 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:

```bash theme={null}
# 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 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.

| Task                            | Interval         | Endpoint                         | Description                                                                          |
| ------------------------------- | ---------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
| **Scheduled mail actions**      | Every 15 minutes | `/api/cron/scheduled-actions`    | Executes delayed actions when QStash is not configured, and restores snoozed threads |
| **Automation jobs**             | Every 15 minutes | `/api/cron/automation-jobs`      | Processes due automation jobs                                                        |
| **Email digests**               | Every 30 minutes | `/api/resend/digest/all`         | Sends due email digests                                                              |
| **Email watch renewal**         | Every 6 hours    | `/api/watch/all`                 | Renews Gmail/Outlook push notification subscriptions                                 |
| **Meeting briefs**              | Every 15 minutes | `/api/meeting-briefs`            | Sends pre-meeting briefings to users with the feature enabled                        |
| **Meeting recorder scheduling** | Every 5 minutes  | `/api/meeting-recorder/schedule` | Schedules Recall.ai meeting bots when Meeting Recorder is configured                 |
| **Follow-up reminders**         | Every hour       | `/api/follow-up-reminders`       | Processes follow-up reminder notifications                                           |

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:

```bash theme={null}
# Scheduled actions (without QStash) and snooze fallback (always) - every 15 minutes
*/15 * * * * curl -s -X GET "https://yourdomain.com/api/cron/scheduled-actions" -H "Authorization: Bearer YOUR_CRON_SECRET"

# Automation jobs - every 15 minutes
*/15 * * * * curl -s -X GET "https://yourdomain.com/api/cron/automation-jobs" -H "Authorization: Bearer YOUR_CRON_SECRET"

# Email digests - every 30 minutes (optional, only if using digests)
*/30 * * * * curl -s -X GET "https://yourdomain.com/api/resend/digest/all" -H "Authorization: Bearer YOUR_CRON_SECRET"

# Reasoning retention - daily (optional, only if REASONING_RETENTION_DAYS is set)
0 3 * * * curl -s -X GET "https://yourdomain.com/api/cron/reasoning-retention" -H "Authorization: Bearer YOUR_CRON_SECRET"

# Unused AI draft cleanup - daily (optional, only if users enable automatic draft cleanup)
0 4 * * * curl -s -X GET "https://yourdomain.com/api/cron/draft-cleanup" -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"

# Meeting recorder scheduling - every 5 minutes (optional, only if using Meeting Recorder)
*/5 * * * * curl -s -X GET "https://yourdomain.com/api/meeting-recorder/schedule" -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.

<Info>
  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" }`).
</Info>

## 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:**

| Feature                             | Without QStash                | With 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](https://upstash.com/pricing/qstash).

Add your QStash credentials to `.env`:

```bash theme={null}
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:

```bash theme={null}
QUEUE_BACKEND=bullmq
REDIS_URL=redis://redis:6379
```

Then start the worker profile:

```bash theme={null}
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:

```bash theme={null}
# Clone the repository
git clone https://github.com/elie222/inbox-zero.git
cd inbox-zero

# Install dependencies and configure environment (auto-generates secrets)
corepack enable
pnpm install
pnpm 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](/hosting/troubleshooting) page for solutions to common problems.

## Auto-Join Organization

For self-hosted instances where all users should belong to a single organization, set:

```env theme={null}
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:

```sql theme={null}
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.

## Default Organization Analytics Consent

If you want newly created organization members to start with organization analytics enabled, set:

```env theme={null}
AUTO_ENABLE_ORG_ANALYTICS=true
```

This only affects new memberships created after the setting is enabled. Existing members keep their current stored value.
