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