- Created migration index file to manage database migrations. - Added script to preload participation awards and related blog posts into the CMS. - Introduced new types and default data for application phases. - Added award articles data structure for the upcoming awards.
5.1 KiB
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:
APP_ROOT=/var/www/bmp-website
APP_USER=deploy
Recommended layout:
/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:
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:
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:
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:
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.
SSH_TARGET=deploy@example.com
APP_ROOT=/var/www/bmp-website
Stop any local dev server before copying content.db.
Copy persistent state once:
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:
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:
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:
cd /var/www/bmp-website/current
npm ci
npm run build
systemd service
Create /etc/systemd/system/bmp-website.service:
[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:
sudo systemctl daemon-reload
sudo systemctl enable --now bmp-website
sudo systemctl status bmp-website --no-pager
nginx reverse proxy
Example server block:
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:
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:
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:
ssh "$SSH_TARGET" "systemctl status bmp-website --no-pager"
curl -I https://example.com
curl -I https://example.com/admin