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

# Deployment

> Deploy Hitler — Vercel for the web app, Docker Compose for API, bot, and databases

# Deployment Guide

Hitler uses a split deployment architecture:

* **Web app** (Next.js) → deployed on **Vercel** at `mako.devinagiffy.xyz`
* **API + Bot + Databases** → deployed via **Docker Compose** on a VPS at `api.mako.devinagiffy.xyz`

***

## Architecture Overview

```
┌─────────────────────┐          ┌──────────────────────────────────────┐
│    Vercel (Web)      │          │   VPS / Server (Docker Compose)      │
│                      │          │                                      │
│  hitler.devinagiffy.  │  HTTPS   │  api.mako.devinagiffy.xyz           │
│        xyz           │ ──────── │  ┌──────────┐  ┌──────────────────┐ │
│                      │          │  │  API      │  │  Slack Bot       │ │
│  Next.js 15          │          │  │  :3001    │  │  (Socket Mode)   │ │
│                      │          │  └────┬─────┘  └────────┬─────────┘ │
│                      │          │       │                  │           │
│                      │          │  ┌────┴─────┐  ┌────────┴─────────┐│
│                      │          │  │ Postgres  │  │  Redis           ││
│                      │          │  │  :5432    │  │  :6379           ││
│                      │          │  └──────────┘  └──────────────────┘│
└─────────────────────┘          └──────────────────────────────────────┘
```

***

## Part 1: VPS Deployment (API + Bot + Databases)

### Prerequisites

* A GCP Compute Engine instance running Ubuntu (22.04 or 24.04 LTS recommended) with at least 2 GB RAM
* `gcloud` CLI installed and authenticated on your local machine
* A domain pointing to the instance (`api.mako.devinagiffy.xyz`)

### Step 1: SSH into Your Instance

```bash theme={null}
# SSH via gcloud (uses your Google account, no manual key setup needed)
gcloud compute ssh YOUR_INSTANCE_NAME --zone=YOUR_ZONE

# Or with project specified
gcloud compute ssh YOUR_INSTANCE_NAME --zone=YOUR_ZONE --project=YOUR_PROJECT_ID
```

<Info>
  GCP automatically creates a user matching your local username and manages SSH keys for you. You
  land as a non-root user with sudo access — no need to create a separate user.
</Info>

### Step 2: Initial Server Setup

```bash theme={null}
# Update system packages
sudo apt update && sudo apt upgrade -y

# Install essential tools
sudo apt install -y curl git make ufw
```

### Step 3: Create a Deploy User

Create a dedicated `hitler` user to keep application files separate from your personal account:

```bash theme={null}
# Create user with home directory
sudo adduser hitler

# Give sudo access
sudo usermod -aG sudo hitler
```

You can switch to the `hitler` user anytime:

```bash theme={null}
# Switch to hitler user
sudo su - hitler

# Or SSH directly as hitler via gcloud
gcloud compute ssh hitler@YOUR_INSTANCE_NAME --zone=YOUR_ZONE
```

From now on, all remaining steps are done as the `hitler` user (`sudo su - hitler`).

### Step 4: Configure Firewall

**GCP VPC firewall** (run from your local machine):

```bash theme={null}
# Allow HTTP and HTTPS traffic to your instance
gcloud compute firewall-rules create allow-http \
  --allow=tcp:80 --target-tags=http-server --description="Allow HTTP"

gcloud compute firewall-rules create allow-https \
  --allow=tcp:443 --target-tags=https-server --description="Allow HTTPS"

# Tag your instance (if not already tagged)
gcloud compute instances add-tags YOUR_INSTANCE_NAME \
  --zone=YOUR_ZONE --tags=http-server,https-server
```

**OS-level firewall** (on the instance, as `hitler` user):

```bash theme={null}
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

# Verify
sudo ufw status
```

### Step 5: Install Docker

```bash theme={null}
# Install Docker using the official script
curl -fsSL https://get.docker.com | sudo sh

# Add hitler user to the docker group (run docker without sudo)
sudo usermod -aG docker hitler

# Apply group changes — log out and back in
exit
```

Reconnect as `hitler` (re-login to pick up the docker group):

```bash theme={null}
# From your local machine
gcloud compute ssh hitler@YOUR_INSTANCE_NAME --zone=YOUR_ZONE

# Verify Docker works without sudo
docker --version        # Should be 24+
docker compose version  # Should be v2+
docker run hello-world
```

### Step 6: Install Node.js and pnpm (optional, for running migrations from host)

```bash theme={null}
# Install Node.js 20 via NodeSource
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Install pnpm
sudo npm install -g pnpm

# Verify
node --version   # Should be 20+
pnpm --version
```

### Step 7: Set Up GitHub SSH Key

Generate an SSH key on the server and add it to GitHub so you can clone private repos:

```bash theme={null}
# Generate a new SSH key (press Enter for all prompts — no passphrase needed)
ssh-keygen -t ed25519 -C "hitler-server"

# Copy the public key
cat ~/.ssh/id_ed25519.pub
```

Copy the output, then add it to GitHub:

1. Go to [github.com/settings/keys](https://github.com/settings/keys)
2. Click **New SSH key**
3. Title: `Hitler Server` (or whatever you like)
4. Key type: **Authentication**
5. Paste the public key and click **Add SSH key**

Verify the connection:

```bash theme={null}
ssh -T git@github.com
# Should print: Hi <username>! You've successfully authenticated...
```

### Step 8: Clone and Configure

```bash theme={null}
cd ~
git clone git@github.com:dev-inagiffy/mako.git && cd hitler
cp .env.example .env
```

Edit `.env` with your production values (see [Environment Variables](#environment-variables) below):

```bash theme={null}
nano .env
```

### Step 9: Deploy with Docker Compose

```bash theme={null}
# Build and start all backend services
docker compose -f docker-compose.deploy.yml up -d --build

# Check status
docker compose -f docker-compose.deploy.yml ps
```

Or use the Makefile:

```bash theme={null}
make deploy        # Build + start
make deploy-logs   # View logs
make deploy-down   # Stop
```

<Info>
  The `docker-compose.deploy.yml` starts **API, Slack Bot, Postgres (pgvector/pgvector:pg16), and Redis**. The web service is
  deployed separately on Vercel (see Part 2). The Postgres image uses `pgvector/pgvector:pg16` instead of standard `postgres:16-alpine`
  to support the pgvector extension required by the context memory system.
</Info>

### Step 10: Run Database Migrations

```bash theme={null}
# From inside the API container (recommended)
docker compose -f docker-compose.deploy.yml exec api \
  node packages/db/dist/migrate.js

# Or from host (requires Node.js + pnpm installed in Step 6)
pnpm db:migrate
```

<Warning>
  **Existing deployments:** If you previously used `postgres:16-alpine` and switched to `pgvector/pgvector:pg16`,
  run `REINDEX DATABASE hitler;` inside the Postgres container to avoid index corruption. The `must_change_password`
  column on the `users` table is added automatically by migration — no manual steps needed.
</Warning>

### Step 11: Point DNS to VPS

Add an **A record** at your domain registrar:

| Type | Name         | Value         |
| ---- | ------------ | ------------- |
| A    | `api.hitler` | `YOUR_VPS_IP` |

Verify it resolves:

```bash theme={null}
dig api.mako.devinagiffy.xyz +short
# Should return your VPS IP
```

### Step 12: Set Up Nginx Reverse Proxy

Install Nginx:

```bash theme={null}
sudo apt update && sudo apt install -y nginx
```

Create the site config:

```bash theme={null}
sudo nano /etc/nginx/sites-available/hitler-api
```

Paste:

```nginx theme={null}
server {
    listen 80;
    server_name api.mako.devinagiffy.xyz;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;

        # Timeout settings for long-running LLM requests
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }
}
```

Enable the site and reload:

```bash theme={null}
sudo ln -s /etc/nginx/sites-available/hitler-api /etc/nginx/sites-enabled/
sudo nginx -t          # Verify config is valid
sudo systemctl reload nginx
```

### Step 13: Set Up SSL with Certbot

Install Certbot via snap (recommended by Certbot/EFF):

```bash theme={null}
sudo snap install --classic certbot

# Ensure the certbot command is available
sudo ln -s /snap/bin/certbot /usr/bin/certbot
```

Generate and install the SSL certificate:

```bash theme={null}
sudo certbot --nginx
```

Certbot will auto-detect your Nginx server blocks and ask which domain to enable HTTPS for. Select `api.mako.devinagiffy.xyz`.

Certbot will:

1. Verify domain ownership via HTTP challenge
2. Generate a Let's Encrypt SSL certificate
3. Automatically modify the Nginx config to add SSL
4. Set up auto-renewal (runs twice daily via systemd timer)

Verify auto-renewal is active:

```bash theme={null}
sudo certbot renew --dry-run
```

After Certbot runs, your Nginx config will automatically be updated to:

```nginx theme={null}
server {
    listen 443 ssl;
    server_name api.mako.devinagiffy.xyz;

    ssl_certificate /etc/letsencrypt/live/api.mako.devinagiffy.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.mako.devinagiffy.xyz/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }
}

server {
    listen 80;
    server_name api.mako.devinagiffy.xyz;
    return 301 https://$host$request_uri;
}
```

### Step 14: Verify

```bash theme={null}
# Health check over HTTPS
curl https://api.mako.devinagiffy.xyz/api/health

# Should return: {"status":"ok","database":"connected","redis":"connected"}

# Verify SSL certificate
curl -vI https://api.mako.devinagiffy.xyz 2>&1 | grep "subject:"
# Should show: subject: CN=api.mako.devinagiffy.xyz
```

***

## Part 2: Vercel Deployment (Web App)

### Step 1: Import to Vercel

1. Go to [vercel.com/new](https://vercel.com/new)
2. Import your Git repository
3. Set the following:
   * **Root Directory**: `apps/web`
   * **Framework Preset**: Next.js
   * **Build Command**: `cd ../.. && pnpm install && pnpm --filter @hitler/web build`
   * **Output Directory**: `.next`

### Step 2: Set Environment Variables

In Vercel project settings → Environment Variables:

| Variable              | Value                                  |
| --------------------- | -------------------------------------- |
| `NEXT_PUBLIC_API_URL` | `https://api.mako.devinagiffy.xyz/api` |

### Step 3: Configure Custom Domain

1. In Vercel project settings → Domains
2. Add `mako.devinagiffy.xyz`
3. Add the DNS records Vercel provides to your domain registrar:
   * **A record**: `76.76.21.21` (or the IP Vercel gives you)
   * **CNAME**: `cname.vercel-dns.com` (for `www` subdomain, optional)

### Step 4: Deploy

Push to your main branch — Vercel auto-deploys on every push.

```bash theme={null}
git push origin main
```

***

## Environment Variables

### Required (VPS `.env`)

| Variable            | Description                                         | How to Generate                                                                         |
| ------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `JWT_SECRET`        | Auth token signing key                              | `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`              |
| `API_KEY`           | Service-to-service auth (must match in bot and API) | `node -e "console.log('hitler_' + require('crypto').randomUUID() + '_dev_secret_key')"` |
| `POSTGRES_PASSWORD` | Database password (change from default)             | Use a strong random password                                                            |

### Slack Integration

| Variable               | Description                                             |
| ---------------------- | ------------------------------------------------------- |
| `SLACK_BOT_TOKEN`      | Bot token (`xoxb-...`) — only for single-workspace mode |
| `SLACK_APP_TOKEN`      | App-level token for Socket Mode (`xapp-...`)            |
| `SLACK_SIGNING_SECRET` | Signing secret from Slack app settings                  |
| `SLACK_CLIENT_ID`      | OAuth client ID (for "Sign in with Slack" on web)       |
| `SLACK_CLIENT_SECRET`  | OAuth client secret                                     |

### LLM Provider

| Variable            | Description                       |
| ------------------- | --------------------------------- |
| `ANTHROPIC_API_KEY` | Anthropic API key (recommended)   |
| `OPENAI_API_KEY`    | OpenAI API key (alternative)      |
| `LLM_PROVIDER`      | `anthropic` (default) or `openai` |

### Secrets Storage (Production)

| Variable                     | Description                                |
| ---------------------------- | ------------------------------------------ |
| `CLOUDFLARE_ACCOUNT_ID`      | Cloudflare account ID                      |
| `CLOUDFLARE_KV_NAMESPACE_ID` | KV namespace for encrypted tokens          |
| `CLOUDFLARE_API_TOKEN`       | Cloudflare API token with KV access        |
| `SECRETS_ENCRYPTION_KEY`     | 32-byte hex key for AES-256-GCM encryption |

### URLs and CORS

| Variable              | Value for Production                   |
| --------------------- | -------------------------------------- |
| `API_HTTPS_URL`       | `https://api.mako.devinagiffy.xyz`     |
| `NEXT_PUBLIC_API_URL` | `https://api.mako.devinagiffy.xyz/api` |
| `WEB_APP_URL`         | `https://mako.devinagiffy.xyz`         |
| `CORS_ORIGINS`        | `https://mako.devinagiffy.xyz`         |

### Full `.env` Example

```env theme={null}
# === Auth ===
JWT_SECRET=your-64-char-hex-secret-here
JWT_EXPIRES_IN=7d
API_KEY=hitler_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx_dev_secret_key

# === Database ===
POSTGRES_USER=hitler
POSTGRES_PASSWORD=your-strong-password-here
POSTGRES_DB=hitler

# === LLM ===
ANTHROPIC_API_KEY=sk-ant-...
LLM_PROVIDER=anthropic

# === Slack (Production App) ===
SLACK_APP_TOKEN=xapp-1-...
SLACK_SIGNING_SECRET=...
SLACK_CLIENT_ID=...
SLACK_CLIENT_SECRET=...

# === URLs ===
API_HTTPS_URL=https://api.mako.devinagiffy.xyz
NEXT_PUBLIC_API_URL=https://api.mako.devinagiffy.xyz/api
WEB_APP_URL=https://mako.devinagiffy.xyz
CORS_ORIGINS=https://mako.devinagiffy.xyz

# === Secrets (Cloudflare KV) ===
CLOUDFLARE_ACCOUNT_ID=...
CLOUDFLARE_KV_NAMESPACE_ID=...
CLOUDFLARE_API_TOKEN=...
SECRETS_ENCRYPTION_KEY=...
```

***

## DNS Configuration

Set up these DNS records at your domain registrar (`devinagiffy.xyz`):

| Type  | Name         | Value                  | Purpose                          |
| ----- | ------------ | ---------------------- | -------------------------------- |
| A     | `api.hitler` | `YOUR_VPS_IP`          | API + Slack Bot (Nginx → Docker) |
| CNAME | `hitler`     | `cname.vercel-dns.com` | Web app on Vercel                |

<Warning>
  DNS propagation can take up to 48 hours, but usually completes within minutes. Run `dig
      api.mako.devinagiffy.xyz +short` to verify before running Certbot — Certbot will fail if DNS
  hasn't propagated yet.
</Warning>

***

## Slack App Setup (Dev + Production)

We recommend **two separate Slack apps** — one for development and one for production. See [Slack Setup](/integrations/slack-setup) for details.

|                  | Development App                                 | Production App                                             |
| ---------------- | ----------------------------------------------- | ---------------------------------------------------------- |
| **Name**         | `Hitler Dev`                                    | `Hitler`                                                   |
| **Socket Mode**  | Enabled                                         | Enabled                                                    |
| **Redirect URL** | `http://localhost:3001/api/auth/slack/callback` | `https://api.mako.devinagiffy.xyz/api/auth/slack/callback` |
| **Used by**      | Local dev                                       | Docker deployment                                          |

***

## Services Overview

| Service       | Container          | Port            | Health Check      |
| ------------- | ------------------ | --------------- | ----------------- |
| API (NestJS)  | `hitler-api`       | 3001            | `GET /api/health` |
| Web (Next.js) | Vercel             | —               | Auto-managed      |
| Slack Bot     | `hitler-bot-slack` | *(socket mode)* | Process check     |
| Postgres      | `hitler-postgres`  | 5432            | `pg_isready`      |
| Redis         | `hitler-redis`     | 6379            | `redis-cli ping`  |

***

## Updating

### API / Bot (VPS)

```bash theme={null}
cd hitler
git pull
docker compose -f docker-compose.deploy.yml up -d --build
```

### Web App (Vercel)

Push to main — Vercel auto-deploys:

```bash theme={null}
git push origin main
```

***

## Viewing Logs

```bash theme={null}
# All services
docker compose -f docker-compose.deploy.yml logs -f

# Specific service
docker compose -f docker-compose.deploy.yml logs -f api
docker compose -f docker-compose.deploy.yml logs -f bot-slack
```

***

## Stopping and Cleaning Up

```bash theme={null}
# Stop services (keeps data)
docker compose -f docker-compose.deploy.yml down

# Stop and delete ALL data (database, Redis) — destructive!
docker compose -f docker-compose.deploy.yml down -v
```

***

## Using an External Database

If you already have Postgres and/or Redis:

```env theme={null}
DATABASE_URL=postgres://user:pass@your-db-host:5432/hitler
REDIS_URL=redis://your-redis-host:6379
```

Then start only app services:

```bash theme={null}
docker compose -f docker-compose.deploy.yml up -d api bot-slack
```

***

## Production Checklist

* [ ] Strong `JWT_SECRET` (32+ random bytes)
* [ ] Unique `API_KEY` (matching between API and bot `.env`)
* [ ] `POSTGRES_PASSWORD` changed from default
* [ ] `NEXT_PUBLIC_API_URL` set to `https://api.mako.devinagiffy.xyz/api`
* [ ] `WEB_APP_URL` set to `https://mako.devinagiffy.xyz`
* [ ] `CORS_ORIGINS` set to `https://mako.devinagiffy.xyz`
* [ ] At least one LLM key configured (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`)
* [ ] Slack app credentials configured (production app)
* [ ] Nginx reverse proxy installed and configured for `api.mako.devinagiffy.xyz`
* [ ] SSL certificate via Certbot (`sudo certbot --nginx`)
* [ ] DNS records for `api.mako.devinagiffy.xyz` and `mako.devinagiffy.xyz`
* [ ] Automated backups for the `postgres_data` Docker volume
* [ ] Cloudflare KV configured for per-org secret storage
