# 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@ 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 <:/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:// # APP_URL=http:// # ── 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 ``` 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/ ``` ### 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