> ## 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 Minimum 2GB RAM, 2 CPU cores, 20GB storage and linux distribution with [minimum security](https://help.ovhcloud.com/csm/en-gb-vps-security-tips?id=kb_article_view\&sysparm_article=KB0047706)
* 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**: Follow [the official guide](https://nodejs.org/en/download) (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)**

```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
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](https://cloud.google.com/sdk/docs/install) installed, you can automate API enabling and Pub/Sub setup:

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

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

| 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 scheduled tasks automatically:

| Task                    | Frequency        | Endpoint                        | Cron Expression | Description                                                                   |
| ----------------------- | ---------------- | ------------------------------- | --------------- | ----------------------------------------------------------------------------- |
| **Scheduled actions**   | Every minute     | `/api/cron/scheduled-actions`   | `* * * * *`     | Executes delayed/scheduled actions when QStash is not configured              |
| **Reasoning retention** | Daily            | `/api/cron/reasoning-retention` | `0 3 * * *`     | Redacts only stale AI reasoning fields when `REASONING_RETENTION_DAYS` is set |
| **Email watch renewal** | Every 6 hours    | `/api/watch/all`                | `0 */6 * * *`   | Renews Gmail/Outlook push notification subscriptions                          |
| **Meeting briefs**      | Every 15 minutes | `/api/meeting-briefs`           | `*/15 * * * *`  | Sends pre-meeting briefings to users with the feature enabled                 |
| **Follow-up reminders** | Every hour       | `/api/follow-up-reminders`      | `0 * * * *`     | Processes follow-up reminder notifications                                    |

**If you're not using Docker Compose** you need to set up cron jobs manually:

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

# Reasoning retention - daily (optional, only if REASONING_RETENTION_DAYS is set)
# Call /api/cron/reasoning-retention with the same cron authorization header.

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

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