- 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.
219 lines
5.1 KiB
Markdown
219 lines
5.1 KiB
Markdown
# 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
|
|
```
|