Skip to main content

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

# 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
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.

Step 2: Initial Server Setup

# 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:
# Create user with home directory
sudo adduser hitler

# Give sudo access
sudo usermod -aG sudo hitler
You can switch to the hitler user anytime:
# 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):
# 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):
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

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

# 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:
# 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
  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:
ssh -T git@github.com
# Should print: Hi <username>! You've successfully authenticated...

Step 8: Clone and Configure

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 below):
nano .env

Step 9: Deploy with Docker Compose

# 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:
make deploy        # Build + start
make deploy-logs   # View logs
make deploy-down   # Stop
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.

Step 10: Run Database Migrations

# 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
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.

Step 11: Point DNS to VPS

Add an A record at your domain registrar:
TypeNameValue
Aapi.hitlerYOUR_VPS_IP
Verify it resolves:
dig api.mako.devinagiffy.xyz +short
# Should return your VPS IP

Step 12: Set Up Nginx Reverse Proxy

Install Nginx:
sudo apt update && sudo apt install -y nginx
Create the site config:
sudo nano /etc/nginx/sites-available/hitler-api
Paste:
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:
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):
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:
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:
sudo certbot renew --dry-run
After Certbot runs, your Nginx config will automatically be updated to:
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

# 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
  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:
VariableValue
NEXT_PUBLIC_API_URLhttps://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.
git push origin main

Environment Variables

Required (VPS .env)

VariableDescriptionHow to Generate
JWT_SECRETAuth token signing keynode -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
API_KEYService-to-service auth (must match in bot and API)node -e "console.log('hitler_' + require('crypto').randomUUID() + '_dev_secret_key')"
POSTGRES_PASSWORDDatabase password (change from default)Use a strong random password

Slack Integration

VariableDescription
SLACK_BOT_TOKENBot token (xoxb-...) — only for single-workspace mode
SLACK_APP_TOKENApp-level token for Socket Mode (xapp-...)
SLACK_SIGNING_SECRETSigning secret from Slack app settings
SLACK_CLIENT_IDOAuth client ID (for “Sign in with Slack” on web)
SLACK_CLIENT_SECRETOAuth client secret

LLM Provider

VariableDescription
ANTHROPIC_API_KEYAnthropic API key (recommended)
OPENAI_API_KEYOpenAI API key (alternative)
LLM_PROVIDERanthropic (default) or openai

Secrets Storage (Production)

VariableDescription
CLOUDFLARE_ACCOUNT_IDCloudflare account ID
CLOUDFLARE_KV_NAMESPACE_IDKV namespace for encrypted tokens
CLOUDFLARE_API_TOKENCloudflare API token with KV access
SECRETS_ENCRYPTION_KEY32-byte hex key for AES-256-GCM encryption

URLs and CORS

VariableValue for Production
API_HTTPS_URLhttps://api.mako.devinagiffy.xyz
NEXT_PUBLIC_API_URLhttps://api.mako.devinagiffy.xyz/api
WEB_APP_URLhttps://mako.devinagiffy.xyz
CORS_ORIGINShttps://mako.devinagiffy.xyz

Full .env Example

# === 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):
TypeNameValuePurpose
Aapi.hitlerYOUR_VPS_IPAPI + Slack Bot (Nginx → Docker)
CNAMEhitlercname.vercel-dns.comWeb app on Vercel
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.

Slack App Setup (Dev + Production)

We recommend two separate Slack apps — one for development and one for production. See Slack Setup for details.
Development AppProduction App
NameHitler DevHitler
Socket ModeEnabledEnabled
Redirect URLhttp://localhost:3001/api/auth/slack/callbackhttps://api.mako.devinagiffy.xyz/api/auth/slack/callback
Used byLocal devDocker deployment

Services Overview

ServiceContainerPortHealth Check
API (NestJS)hitler-api3001GET /api/health
Web (Next.js)VercelAuto-managed
Slack Bothitler-bot-slack(socket mode)Process check
Postgreshitler-postgres5432pg_isready
Redishitler-redis6379redis-cli ping

Updating

API / Bot (VPS)

cd hitler
git pull
docker compose -f docker-compose.deploy.yml up -d --build

Web App (Vercel)

Push to main — Vercel auto-deploys:
git push origin main

Viewing Logs

# 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

# 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:
DATABASE_URL=postgres://user:pass@your-db-host:5432/hitler
REDIS_URL=redis://your-redis-host:6379
Then start only app services:
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