464 lines
12 KiB
Markdown
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) | |