# 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

```bash
# 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:**

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

## 3. Deploy the Application

### Clone the Repository

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

### Create Environment File

```bash
cp .env.example .env
nano .env
```

Fill in all values:

```env
# 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:

```bash
# 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:

```bash
# 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:

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

### Set CORS (for browser uploads)

```json
[
  {
    "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](https://dashboard.clerk.com)
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

### Option A: Let's Encrypt with Certbot (Recommended)

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

```bash
docker compose up -d
```

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

#### Step 2: Get the SSL certificate

```bash
# 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](https://github.com/acmesh-official/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

```bash
# 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:

```bash
# 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

```bash
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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
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

```bash
# 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

```bash
# 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

```bash
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:

```bash
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:
   ```bash
   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

```bash
# 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

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