spomeni/deploy.md
2026-06-20 18:17:30 +02:00

12 KiB

Deployment Guide — testbed.mk on VPS

Architecture Overview

Internet
  │
  ├── *.testbed.mk ──► Nginx (:80/:443)
  │                     ├── Extracts subdomain → sets X-Subdomain header
  │                     └── Proxies to app:3000
  │
  └── testbed.mk ──► Nginx ──► Next.js app
                               ├── Clerk (auth)
                               ├── PostgreSQL (db:5432)
                               └── Contabo S3 (images)

Stack: Docker Compose with 3 containers — app (Next.js), db (PostgreSQL 16), nginx (Nginx)

1. VPS Preparation

System Requirements

  • Ubuntu 22.04+ or similar Linux
  • Minimum 2GB RAM, 1 vCPU
  • Open ports: 22 (SSH), 80 (HTTP), 443 (HTTPS)

Install Docker

# Update packages
sudo apt update && sudo apt upgrade -y

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

# Install Docker Compose (if not included)
sudo apt install -y docker-compose-plugin

# Add your user to docker group (optional, avoids sudo)
sudo usermod -aG docker $USER
newgrp docker

# Verify
docker --version
docker compose version

2. DNS Configuration

In your DNS provider, create these records for testbed.mk:

Type Host Value TTL
A @ YOUR_VPS_IP 300
A * YOUR_VPS_IP 300

This means:

  • testbed.mk → your VPS
  • anything.testbed.mk → your VPS (wildcard)

Verify DNS propagation:

dig testbed.mk +short
dig random.testbed.mk +short
# Both should return your VPS IP

3. Deploy the Application

Clone the Repository

git clone <your-repo-url> /opt/spomeniQR
cd /opt/spomeniQR

Create Environment File

cp .env.example .env
nano .env

Fill in all values:

# Clerk (PRODUCTION keys from https://dashboard.clerk.com)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_SECRET_KEY=sk_live_...

# Database (strong password!)
DATABASE_URL=postgresql://postgres:STRONG_PASSWORD_HERE@db:5432/monuments
POSTGRES_PASSWORD=STRONG_PASSWORD_HERE

# Contabo S3
S3_ENDPOINT=https://eu2.contabostorage.com
S3_REGION=eu-2
S3_ACCESS_KEY_ID=your-contabo-access-key
S3_SECRET_ACCESS_KEY=your-contabo-secret-key
S3_BUCKET_NAME=monuments-images

# App
NEXT_PUBLIC_APP_URL=https://testbed.mk
NEXT_PUBLIC_APP_DOMAIN=testbed.mk

Important: Use a strong, unique password for POSTGRES_PASSWORD.

Create the Prisma Migration

Before the first deployment, create the initial migration locally or on the server:

# Start only the database first
docker compose up db -d

# Wait for it to be ready (~5 seconds)
sleep 5

# Run the migration
docker compose exec db psql -U postgres -c "CREATE DATABASE monuments;" 2>/dev/null || true

# Run Prisma migration using a temporary container
docker compose run --rm app npx prisma migrate deploy

Alternatively, you can generate a migration file locally first:

# On your local machine (with DATABASE_URL pointing to any postgres)
npx prisma migrate dev --name init

Then commit the generated migration files. The scripts/start.sh entrypoint will run npx prisma migrate deploy automatically on every container start.

4. Configure Contabo S3

Create the Bucket

  1. Log into Contabo Object Storage at https://contabostorage.com
  2. Create a bucket named monuments-images
  3. Choose the same region as your S3_REGION (e.g. eu-2)

Set Public Read Access

The monument images need to be publicly viewable. Set this bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": ["*"] },
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::monuments-images/*"]
    }
  ]
}

Set CORS (for browser uploads)

[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT"],
    "AllowedOrigins": ["https://testbed.mk", "https://*.testbed.mk"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3600
  }
]

Create API Keys

  1. In the Contabo Object Storage panel, create a new access key
  2. Grant it read/write permissions on the monuments-images bucket
  3. Copy the Access Key ID and Secret Access Key to your .env

5. Configure Clerk (Production)

  1. Go to Clerk Dashboard
  2. Switch to your Production instance
  3. Under Paths, set:
    • Sign-in: /sign-in
    • Sign-up: /sign-up
  4. Under Domains, add:
    • testbed.mk
    • *.testbed.mk (if supported — otherwise just the apex domain)
  5. Copy the Production publishable key and secret key to your .env (the ones starting with pk_live_ and sk_live_)

6. SSL / HTTPS Setup

The project includes two Nginx configs:

  • nginx/conf.d/default.conf — HTTP only (for initial setup / local dev)
  • nginx/conf.d/production.conf — HTTPS with Let's Encrypt

Step 1: Start with HTTP first

Make sure default.conf is active (it is by default):

docker compose up -d

Verify the app is reachable: http://testbed.mk

Step 2: Get the SSL certificate

# Install certbot on the host
sudo apt install -y certbot

# Get a wildcard certificate (requires DNS-01 challenge for *.testbed.mk)
# OR get a single-domain cert (simpler, no wildcard):

# For a single-domain cert (covers testbed.mk only, NOT subdomains):
sudo certbot certonly --webroot \
  -w /opt/spomeniQR/certbot/www \
  -d testbed.mk

# For a wildcard cert (covers testbed.mk AND *.testbed.mk):
# You MUST use DNS-01 challenge. Example with Cloudflare DNS plugin:
sudo apt install -y python3-certbot-dns-cloudflare
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d testbed.mk \
  -d '*.testbed.mk'

For the wildcard certificate, you need to use a DNS-01 challenge. This requires:

  • Your DNS provider's API credentials
  • The appropriate certbot DNS plugin

If your DNS provider doesn't have a certbot plugin, you can use acme.sh with DNS manual mode.

Alternative: Use a certificate for each subdomain on-the-fly. This requires a more complex Nginx setup (not covered here — wildcard cert is recommended for simplicity).

Step 3: Switch to HTTPS config

# Replace the HTTP config with the HTTPS config
cd /opt/spomeniQR/nginx/conf.d/
mv default.conf default.conf.bak
cp production.conf default.conf

# Restart nginx
docker compose restart nginx

Step 4: Auto-renewal

Let's Encrypt certificates expire every 90 days. Set up auto-renewal:

# Test renewal
sudo certbot renew --dry-run

# Add a cron job for auto-renewal
sudo crontab -e
# Add this line:
0 3 * * * certbot renew --quiet --deploy-hook "docker restart spomeniqr-nginx"

Option B: No SSL (Development / Staging)

If you're just testing, keep default.conf as-is. The app works over HTTP. Just make sure:

  • Clerk dashboard allows http://testbed.mk as a domain
  • NEXT_PUBLIC_APP_URL=http://testbed.mk in your .env

7. Build and Run

cd /opt/spomeniQR

# Build and start all services
docker compose up -d --build

# View logs
docker compose logs -f app

# Check status
docker compose ps

The app should now be live at https://testbed.mk (or http://testbed.mk if no SSL).

8. Verify Everything Works

Check the Services

# All containers should be "Up"
docker compose ps

# Check app logs
docker compose logs app | tail -20

# Check nginx logs
docker compose logs nginx | tail -20

Test the Endpoints

# Landing page
curl -I https://testbed.mk

# Subdomain routing (should pass X-Subdomain header)
curl -I https://eiffel-tower.testbed.mk

# API health check
curl https://testbed.mk/api/check-subdomain?slug=test

Test in Browser

  1. Visit https://testbed.mk — should show the landing page
  2. Click Sign Up — should create a Clerk account
  3. Go through the onboarding wizard
  4. After publishing, visit https://{your-subdomain}.testbed.mk
  5. Verify photos upload correctly (check Contabo S3 bucket)

9. Maintenance

View Logs

# All services
docker compose logs -f

# Specific service
docker compose logs -f app
docker compose logs -f db
docker compose logs -f nginx

Database Access

# Connect to PostgreSQL
docker compose exec db psql -U postgres -d monuments

# Or use Prisma Studio (opens a web GUI)
docker compose run --rm app npx prisma studio

Update the Application

cd /opt/spomeniQR
git pull origin main
docker compose up -d --build

The start.sh script runs npx prisma migrate deploy automatically, so any schema changes will be applied on startup.

Database Backup

# Create a backup
docker compose exec db pg_dump -U postgres monuments > backup_$(date +%Y%m%d).sql

# Restore from backup
cat backup_20240101.sql | docker compose exec -T db psql -U postgres monuments

Restart Services

# Restart everything
docker compose restart

# Restart only the app (e.g., after env change)
docker compose restart app

# Restart nginx (e.g., after config change)
docker compose restart nginx

10. Troubleshooting

App won't start

docker compose logs app

Common issues:

  • DATABASE_URL is wrong: Ensure it matches your POSTGRES_PASSWORD and uses db as hostname (not localhost) inside Docker
  • Clerk keys are wrong: Verify pk_live_ / sk_live_ keys
  • S3 credentials wrong: Check your Contabo access key

502 Bad Gateway

The app isn't running or not ready yet:

docker compose ps            # Check if app is running
docker compose logs app      # Check for startup errors
docker compose restart app   # Try restarting

Subdomain routing not working

  1. Check DNS: dig random.testbed.mk +short should return your VPS IP
  2. Check Nginx config contains the X-Subdomain header logic:
    docker compose exec nginx cat /etc/nginx/conf.d/default.conf
    
  3. Check the middleware: subdomains rely on the X-Subdomain header set by Nginx

SSL certificate errors

# Check if certificate files exist
ls -la /opt/spomeniQR/certbot/conf/live/testbed.mk/

# Renew manually
sudo certbot renew --force-renewal

S3 uploads failing

  1. Check CORS configuration includes https://testbed.mk
  2. Check bucket name matches S3_BUCKET_NAME in .env
  3. Check access key has read+write permissions
  4. Test with: curl -I https://eu2.contabostorage.com/monuments-images/

Can't connect to PostgreSQL

# Check if db is healthy
docker compose ps db

# Try connecting
docker compose exec db psql -U postgres -d monuments -c "SELECT 1;"

# Check the connection string
docker compose exec app printenv DATABASE_URL

11. Environment Variables Reference

Variable Required Description
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY Yes Clerk publishable key (pk_live_...)
CLERK_SECRET_KEY Yes Clerk secret key (sk_live_...)
DATABASE_URL Yes PostgreSQL connection string
POSTGRES_PASSWORD Yes PostgreSQL password (used by db container)
S3_ENDPOINT Yes Contabo S3 endpoint URL
S3_REGION Yes Contabo S3 region (e.g. eu-2)
S3_ACCESS_KEY_ID Yes S3 access key
S3_SECRET_ACCESS_KEY Yes S3 secret key
S3_BUCKET_NAME Yes S3 bucket name (monuments-images)
NEXT_PUBLIC_APP_URL Yes Public URL (https://testbed.mk)
NEXT_PUBLIC_APP_DOMAIN Yes Domain only (testbed.mk)