# 06 — Deployment Guide: OpenStack VPS

This guide covers the complete deployment of LexiChain on a **private OpenStack VPS** running Ubuntu 22.04, using Docker, Nginx as a reverse proxy, and Ethereum Sepolia as the blockchain network.

**Estimated setup time:** 60–90 minutes (first time)

---

## Architecture Overview

```
Internet
   │
   ▼
[Nginx] ── TLS termination ── port 443/80
   │
   ▼
[Docker: lexichain-app] ── port 3000 (internal)
   │
   ├──► [PostgreSQL] ── port 5432 (internal or external)
   ├──► [Clerk API]   ── external HTTPS
   ├──► [UploadThing] ── external HTTPS
   ├──► [Gemini API]  ── external HTTPS
   ├──► [Mistral API] ── external HTTPS
   └──► [Ethereum Sepolia RPC] ── external HTTPS
```

---

## Prerequisites

Before starting, ensure you have:

- [ ] OpenStack VPS with **Ubuntu 22.04 LTS** (minimum: 2 vCPU, 4 GB RAM, 40 GB disk)
- [ ] SSH access to the VPS
- [ ] A **PostgreSQL** database (on the same VPS or a managed DB service)
- [ ] All external service accounts set up (Clerk, UploadThing, Gemini, Mistral)
- [ ] A Sepolia ETH wallet with some test ETH (see [Smart Contract docs](./05-smart-contract.md))
- [ ] The `DocumentRegistry` contract deployed to Sepolia (see [Smart Contract docs](./05-smart-contract.md))

---

## Part 1: VPS Initial Setup

### 1.1 Connect and update the VPS

```bash
ssh ubuntu@<VPS_IP>

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget git unzip ufw
```

### 1.2 Configure the firewall (UFW)

```bash
# Allow SSH (important: do this FIRST to avoid locking yourself out)
sudo ufw allow OpenSSH

# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable firewall
sudo ufw enable

# Verify
sudo ufw status
```

> If you are using OpenStack Security Groups, also open ports 80, 443, and 22 in the Security Group rules on the OpenStack dashboard.

---

## Part 2: Install Docker

```bash
# Install Docker
curl -fsSL https://get.docker.com | sudo sh

# Add your user to the docker group (avoids needing sudo for docker commands)
sudo usermod -aG docker $USER

# Log out and back in for the group change to take effect
exit
# ... reconnect via SSH ...

# Verify
docker --version
docker compose version
```

---

## Part 3: Set Up PostgreSQL

**Option A: PostgreSQL on the same VPS (recommended for simplicity)**

```bash
sudo apt install -y postgresql postgresql-contrib

# Start and enable PostgreSQL
sudo systemctl start postgresql
sudo systemctl enable postgresql

# Create database and user
sudo -u postgres psql <<EOF
CREATE USER lexichain WITH PASSWORD 'your_strong_password_here';
CREATE DATABASE lexichain OWNER lexichain;
GRANT ALL PRIVILEGES ON DATABASE lexichain TO lexichain;
EOF
```

Test the connection:
```bash
psql -U lexichain -d lexichain -h localhost -c "SELECT version();"
```

Your `DATABASE_URL` will be:
```
postgresql://lexichain:your_strong_password_here@localhost:5432/lexichain
```

**Option B: Use a managed PostgreSQL service**  
If your organization provides a managed DB, use the connection string provided by that service.

---

## Part 4: Build the Docker Image

### 4.1 Clone the repository on the VPS

```bash
git clone https://github.com/your-org/lexichain.git /opt/lexichain
cd /opt/lexichain
```

Or transfer the code via `scp` / `rsync`:
```bash
# From your local machine:
rsync -avz --exclude='.git' --exclude='node_modules' --exclude='.next' \
  ./ ubuntu@<VPS_IP>:/opt/lexichain/
```

### 4.2 Build the Docker image

```bash
cd /opt/lexichain
docker build -t lexichain:latest .
```

This will:
1. Install npm dependencies
2. Generate Prisma client
3. Build the Next.js production bundle
4. Create a minimal runner image with a non-root user

> The build takes **3–8 minutes** on first run. Subsequent builds use Docker layer cache and are faster.

---

## Part 5: Configure Environment Variables

Create the production environment file:

```bash
sudo mkdir -p /etc/lexichain
sudo nano /etc/lexichain/production.env
```

Fill in all values:

```bash
# ── Application ──────────────────────────────────────
NODE_ENV=production
APP_URL=https://yourdomain.com
# If you don't have a domain yet, use http://<VPS_IP>
# APP_URL=http://<VPS_IP>

# ── Clerk Authentication ──────────────────────────────
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...
CLERK_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

# ── Database ──────────────────────────────────────────
DATABASE_URL=postgresql://lexichain:your_strong_password_here@localhost:5432/lexichain

# ── File Storage (UploadThing) ────────────────────────
UPLOADTHING_TOKEN=your_uploadthing_token
UPLOADTHING_APP_ID=your_uploadthing_app_id

# ── AI — Gemini (Primary) ─────────────────────────────
AI_API_KEY1=AIza...
AI_API_KEY2=AIza...   # optional second key for rotation
AI_MODEL_PRIMARY=gemini-2.5-flash-preview-04-17
AI_EMBEDDING_MODEL=text-embedding-004

# ── AI — Mistral (Fallback) ───────────────────────────
MISTRAL_API_KEY=your_mistral_api_key
AI_MODEL_FALLBACK=mistral-large-latest
AI_MODEL_MISTRAL_VISION=pixtral-large-latest
AI_MODEL_MISTRAL_OCR=mistral-ocr-latest

# ── Blockchain (Sepolia Testnet) ──────────────────────
BLOCKCHAIN_NETWORK=sepolia
BLOCKCHAIN_RPC_URL=https://rpc.sepolia.org
BLOCKCHAIN_PRIVATE_KEY=0x...
BLOCKCHAIN_CONTRACT_ADDRESS=0x...

# ── Email (SMTP) ──────────────────────────────────────
EMAIL_HOST=smtp-relay.brevo.com
EMAIL_PORT=587
EMAIL_SECURE=false
EMAIL_USER=your_brevo_smtp_user
EMAIL_PASS=your_brevo_smtp_password
MAIL_FROM=LexiChain <no-reply@yourdomain.com>
```

Secure the file:
```bash
sudo chmod 600 /etc/lexichain/production.env
```

---

## Part 6: Run Database Migrations

Before starting the app, run Prisma migrations:

```bash
docker run --rm \
  --env-file /etc/lexichain/production.env \
  --network host \
  lexichain:latest \
  npx prisma migrate deploy
```

> `--network host` allows the container to reach PostgreSQL on `localhost`.  
> This command is safe to run on every deployment — it only applies pending migrations.

---

## Part 7: Start the Application

```bash
docker run -d \
  --name lexichain \
  --restart unless-stopped \
  --env-file /etc/lexichain/production.env \
  --network host \
  -p 3000:3000 \
  lexichain:latest
```

Verify the container is running:
```bash
docker ps
docker logs lexichain --tail 50
```

Test that the app responds:
```bash
curl http://localhost:3000/api/health
# Expected: {"status":"ok"}
```

---

## Part 8: Set Up Nginx Reverse Proxy

### 8.1 Install Nginx

```bash
sudo apt install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
```

### 8.2 Configure Nginx

```bash
sudo nano /etc/nginx/sites-available/lexichain
```

**Without TLS (temporary — use if you don't have a domain yet):**

```nginx
server {
    listen 80;
    server_name _;   # Matches any hostname or IP

    # Increase upload limits for contract PDFs
    client_max_body_size 35M;

    location / {
        proxy_pass http://localhost:3000;
        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;
    }
}
```

**With TLS (recommended — requires a domain):**

```nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    # Redirect all HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Increase upload limits for contract PDFs
    client_max_body_size 35M;

    location / {
        proxy_pass http://localhost:3000;
        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 https;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }
}
```

Enable the site:
```bash
sudo ln -s /etc/nginx/sites-available/lexichain /etc/nginx/sites-enabled/
sudo nginx -t  # Test configuration
sudo systemctl reload nginx
```

### 8.3 Get TLS Certificate (if you have a domain)

```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Follow the prompts

# Auto-renew (runs twice daily)
sudo systemctl enable certbot.timer
```

---

## Part 9: Configure Clerk Webhooks

For user sync to work in production, configure your Clerk webhook:

1. Go to [Clerk Dashboard](https://dashboard.clerk.com) → your app → **Webhooks**
2. Add a new endpoint:
   - **URL**: `https://yourdomain.com/api/webhooks/clerk`
   - **Events**: `user.created`, `user.updated`, `user.deleted`
3. Copy the **Signing Secret** → set as `CLERK_WEBHOOK_SECRET` in your env file
4. Restart the container after updating the env:
   ```bash
   docker stop lexichain && docker rm lexichain
   # Re-run the docker run command from Part 7
   ```

---

## Part 10: Verify the Deployment

Run through this checklist:

```bash
# 1. App responds
curl https://yourdomain.com/api/health

# 2. View app logs
docker logs lexichain -f

# 3. Check Nginx logs
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
```

**Manual UI verification:**
- [ ] Sign-up / sign-in flow works
- [ ] Upload a PDF contract → analysis runs
- [ ] Blockchain proof registered (check `/blockchain` page)
- [ ] Email notification received after analysis
- [ ] Dashboard stats populated

---

## Part 11: Updates & Rollbacks

### Deploy a new version

```bash
cd /opt/lexichain

# Pull latest code
git pull origin main

# Build new image
docker build -t lexichain:latest .

# Run migrations (safe — only applies new ones)
docker run --rm \
  --env-file /etc/lexichain/production.env \
  --network host \
  lexichain:latest \
  npx prisma migrate deploy

# Replace running container
docker stop lexichain && docker rm lexichain

docker run -d \
  --name lexichain \
  --restart unless-stopped \
  --env-file /etc/lexichain/production.env \
  --network host \
  -p 3000:3000 \
  lexichain:latest
```

### Rollback to a previous version

Tag your images before deploying to enable rollback:

```bash
# Before deploying new version, tag current as backup
docker tag lexichain:latest lexichain:backup

# If the new deployment fails, rollback:
docker stop lexichain && docker rm lexichain
docker run -d --name lexichain --restart unless-stopped \
  --env-file /etc/lexichain/production.env \
  --network host -p 3000:3000 \
  lexichain:backup
```

---

## Part 12: Monitoring & Maintenance

### View application logs

```bash
# Live logs
docker logs lexichain -f

# Last 100 lines
docker logs lexichain --tail 100

# Filter for errors
docker logs lexichain 2>&1 | grep -i error
```

### Monitor container health

```bash
docker inspect --format='{{json .State.Health}}' lexichain | python3 -m json.tool
```

### Database maintenance

```bash
# Connect to PostgreSQL
sudo -u postgres psql -d lexichain

# Check database size
\l+

# Vacuum analyze (optimize queries)
VACUUM ANALYZE;
```

### Disk space management

```bash
# Remove unused Docker images
docker image prune -f

# Check disk usage
df -h
du -sh /var/lib/docker/
```

---

## Part 13: Environment Variable Reference

Complete reference for all variables used in production:

| Variable | Required | Description |
|----------|----------|-------------|
| `NODE_ENV` | ✅ | Must be `production` |
| `APP_URL` | ✅ | Full public URL of the app |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | ✅ | Clerk publishable key |
| `CLERK_SECRET_KEY` | ✅ | Clerk secret key |
| `CLERK_WEBHOOK_SECRET` | ✅ | Clerk webhook signing secret |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | ✅ | `/sign-in` |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | ✅ | `/sign-up` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | ✅ | `/dashboard` |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | ✅ | `/dashboard` |
| `DATABASE_URL` | ✅ | PostgreSQL connection string |
| `UPLOADTHING_TOKEN` | ✅ | UploadThing auth token |
| `UPLOADTHING_APP_ID` | ✅ | UploadThing app ID |
| `AI_API_KEY1` | ✅ | Gemini API key (primary) |
| `AI_API_KEY2` | ❌ | Gemini API key (rotation) |
| `AI_API_KEY3` | ❌ | Gemini API key (rotation) |
| `AI_MODEL_PRIMARY` | ✅ | Gemini model name |
| `AI_EMBEDDING_MODEL` | ✅ | `text-embedding-004` |
| `MISTRAL_API_KEY` | ❌ | Mistral key (activates fallback) |
| `AI_MODEL_FALLBACK` | ❌ | `mistral-large-latest` |
| `AI_MODEL_MISTRAL_VISION` | ❌ | `pixtral-large-latest` |
| `AI_MODEL_MISTRAL_OCR` | ❌ | `mistral-ocr-latest` |
| `BLOCKCHAIN_NETWORK` | ✅ | `sepolia` |
| `BLOCKCHAIN_RPC_URL` | ✅ | Sepolia RPC endpoint |
| `BLOCKCHAIN_PRIVATE_KEY` | ✅ | Platform wallet private key |
| `BLOCKCHAIN_CONTRACT_ADDRESS` | ✅ | DocumentRegistry address |
| `EMAIL_HOST` | ❌ | SMTP server hostname |
| `EMAIL_PORT` | ❌ | SMTP port (587 or 465) |
| `EMAIL_SECURE` | ❌ | `false` for STARTTLS |
| `EMAIL_USER` | ❌ | SMTP username |
| `EMAIL_PASS` | ❌ | SMTP password |
| `MAIL_FROM` | ❌ | From address for emails |

---

## Troubleshooting

### Container won't start

```bash
docker logs lexichain
# Look for: DATABASE_URL connection refused, missing env vars, etc.
```

Common causes:
- `DATABASE_URL` is wrong or PostgreSQL is not running
- A required env var is missing
- Port 3000 is already in use: `sudo lsof -i :3000`

### 502 Bad Gateway from Nginx

The app is not running or not listening:
```bash
docker ps           # Is the container running?
curl localhost:3000  # Does it respond locally?
docker logs lexichain --tail 20
```

### Blockchain not working

```bash
# Test RPC connectivity
curl -X POST https://rpc.sepolia.org \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'

# Verify your wallet has Sepolia ETH
# Check: https://sepolia.etherscan.io/address/<YOUR_WALLET_ADDRESS>
```

### AI analysis failing

- Verify `AI_API_KEY1` is valid at https://aistudio.google.com
- Check if Gemini model name is correct (model names change — check Google AI docs)
- If all Gemini keys are exhausted, verify `MISTRAL_API_KEY` is set for fallback

### Email not sending

- Without `EMAIL_HOST`, emails are silently dropped in production
- Test SMTP credentials with: `npx nodemailer-test` or Brevo's SMTP tester
- Check Brevo sending quota

---

## Security Hardening Checklist

- [ ] Production env file has `chmod 600`
- [ ] PostgreSQL password is strong (20+ random characters)
- [ ] Blockchain wallet is dedicated (not holding real ETH)
- [ ] Clerk webhooks endpoint is HTTPS only
- [ ] UFW firewall enabled with only required ports
- [ ] TLS certificate installed and auto-renewing
- [ ] `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` starts with `pk_live_` (not `pk_test_`)
- [ ] `NODE_ENV=production` set
- [ ] No `.env` file committed to git
