# VPS deployment This project is a Node/Next/Payload application that currently uses: - SQLite via `DATABASE_URL` - local filesystem media in `public/media` - no Docker Treat code and persistent state separately. Git or rsync can move the code, but the database and media directory must be copied and backed up explicitly. ## Persistent paths Use one stable directory on the VPS: ```bash APP_ROOT=/var/www/bmp-website APP_USER=deploy ``` Recommended layout: ```text /var/www/bmp-website/ current/ # synced application code shared/ .env # production secrets content.db # SQLite database media/ # Payload uploads backups/ ``` Create it on the VPS: ```bash sudo mkdir -p "$APP_ROOT"/{current,shared/media,backups} sudo chown -R "$APP_USER":"$APP_USER" "$APP_ROOT" ``` ## Production environment Create `/var/www/bmp-website/shared/.env` on the VPS: ```dotenv DATABASE_URL=file:/var/www/bmp-website/shared/content.db PAYLOAD_SECRET=replace-with-existing-secret NEXT_PUBLIC_SERVER_URL=https://example.com CRON_SECRET=replace-with-existing-secret PREVIEW_SECRET=replace-with-existing-secret PORT=3000 NODE_ENV=production ``` Keep `PAYLOAD_SECRET`, `CRON_SECRET`, and `PREVIEW_SECRET` stable once the site is live. Do not commit the production `.env`. `NEXT_PUBLIC_SERVER_URL` is read during the Next build, so set it correctly before running `npm run build`. If you already have the correct local `.env`, transfer it separately from the application code and lock down its permissions: ```bash scp .env "$SSH_TARGET:$APP_ROOT/shared/.env" ssh "$SSH_TARGET" "chmod 600 $APP_ROOT/shared/.env" ``` If you need to package secrets in an archive, make that archive secret-only and encrypt it before transfer: ```bash tar -czf bmp-website-secrets.tar.gz .env gpg -c bmp-website-secrets.tar.gz scp bmp-website-secrets.tar.gz.gpg "$SSH_TARGET:$APP_ROOT/shared/" ``` Then decrypt it on the VPS, move `.env` into place, and remove the temporary archive files. ## Initial transfer Run these locally from the repository root. Replace the SSH target and domain values first. ```bash SSH_TARGET=deploy@example.com APP_ROOT=/var/www/bmp-website ``` Stop any local dev server before copying `content.db`. Copy persistent state once: ```bash rsync -az content.db "$SSH_TARGET:$APP_ROOT/shared/content.db" rsync -az public/media/ "$SSH_TARGET:$APP_ROOT/shared/media/" ``` Copy application code without local state or build output: ```bash rsync -az --delete \ --exclude '.env' \ --exclude '.next/' \ --exclude 'node_modules/' \ --exclude 'content.db' \ --exclude 'public/media/' \ ./ "$SSH_TARGET:$APP_ROOT/current/" ``` Link the shared media directory into the app: ```bash ssh "$SSH_TARGET" "mkdir -p $APP_ROOT/current/public && rm -rf $APP_ROOT/current/public/media && ln -s $APP_ROOT/shared/media $APP_ROOT/current/public/media" ssh "$SSH_TARGET" "ln -sfn $APP_ROOT/shared/.env $APP_ROOT/current/.env" ``` Do not use `--delete` when syncing media unless the source is intentionally the source of truth and you have a current backup. ## Build on the VPS Install Node.js 22 or another version accepted by `package.json`, then run: ```bash cd /var/www/bmp-website/current npm ci npm run build ``` ## systemd service Create `/etc/systemd/system/bmp-website.service`: ```ini [Unit] Description=BMP website After=network.target [Service] Type=simple User=deploy Group=deploy WorkingDirectory=/var/www/bmp-website/current EnvironmentFile=/var/www/bmp-website/shared/.env ExecStart=/usr/bin/env npm run start Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` Enable and start it: ```bash sudo systemctl daemon-reload sudo systemctl enable --now bmp-website sudo systemctl status bmp-website --no-pager ``` ## nginx reverse proxy Example server block: ```nginx server { listen 80; server_name example.com www.example.com; client_max_body_size 25m; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } ``` After enabling the site, issue TLS certificates with your normal ACME client such as Certbot. ## Repeat deploys Before every deploy, back up the live SQLite database on the VPS: ```bash APP_ROOT=/var/www/bmp-website sqlite3 "$APP_ROOT/shared/content.db" ".backup '$APP_ROOT/backups/content-$(date +%Y%m%d-%H%M%S).db'" ``` Then sync code, rebuild, and restart: ```bash rsync -az --delete \ --exclude '.env' \ --exclude '.next/' \ --exclude 'node_modules/' \ --exclude 'content.db' \ --exclude 'public/media/' \ ./ "$SSH_TARGET:$APP_ROOT/current/" ssh "$SSH_TARGET" "cd $APP_ROOT/current && npm ci && npm run build && sudo systemctl restart bmp-website" ``` Verify: ```bash ssh "$SSH_TARGET" "systemctl status bmp-website --no-pager" curl -I https://example.com curl -I https://example.com/admin ```