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).
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:
- Go to github.com/settings/keys
- Click New SSH key
- Title:
Hitler Server (or whatever you like)
- Key type: Authentication
- 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...
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):
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:
| Type | Name | Value |
|---|
| A | api.hitler | YOUR_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:
Certbot will auto-detect your Nginx server blocks and ask which domain to enable HTTPS for. Select api.mako.devinagiffy.xyz.
Certbot will:
- Verify domain ownership via HTTP challenge
- Generate a Let’s Encrypt SSL certificate
- Automatically modify the Nginx config to add SSL
- 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
- Go to vercel.com/new
- Import your Git repository
- 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
- In Vercel project settings → Domains
- Add
mako.devinagiffy.xyz
- 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.
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
# === 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 |
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 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)
cd hitler
git pull
docker compose -f docker-compose.deploy.yml up -d --build
Web App (Vercel)
Push to main — Vercel auto-deploys:
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