Files
LexiChain/docs/06-deployment-openstack.md
2026-05-13 21:08:27 +01:00

16 KiB
Raw Blame History

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: 6090 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)
  • The DocumentRegistry contract deployed to Sepolia (see Smart Contract docs)

Part 1: VPS Initial Setup

1.1 Connect and update the VPS

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)

# 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

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

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:

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

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

Or transfer the code via scp / rsync:

# From your local machine:
rsync -avz --exclude='.git' --exclude='node_modules' --exclude='.next' \
  ./ ubuntu@<VPS_IP>:/opt/lexichain/

4.2 Build the Docker image

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 38 minutes on first run. Subsequent builds use Docker layer cache and are faster.


Part 5: Configure Environment Variables

Create the production environment file:

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

Fill in all values:

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

sudo chmod 600 /etc/lexichain/production.env

Part 6: Run Database Migrations

Before starting the app, run Prisma migrations:

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

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:

docker ps
docker logs lexichain --tail 50

Test that the app responds:

curl http://localhost:3000/api/health
# Expected: {"status":"ok"}

Part 8: Set Up Nginx Reverse Proxy

8.1 Install Nginx

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

8.2 Configure Nginx

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

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

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

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:

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)

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 → 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:
    docker stop lexichain && docker rm lexichain
    # Re-run the docker run command from Part 7
    

Part 10: Verify the Deployment

Run through this checklist:

# 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

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:

# 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

# 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

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

Database maintenance

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

# Check database size
\l+

# Vacuum analyze (optimize queries)
VACUUM ANALYZE;

Disk space management

# 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

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:

docker ps           # Is the container running?
curl localhost:3000  # Does it respond locally?
docker logs lexichain --tail 20

Blockchain not working

# 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