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

464 lines
12 KiB
Markdown

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