feat: add Docker support with deployment scripts and database backup functionality
This commit is contained in:
@@ -1,218 +1,165 @@
|
||||
# VPS deployment
|
||||
|
||||
This project is a Node/Next/Payload application that currently uses:
|
||||
The live VPS deployment runs this Payload/Next app with Docker Compose and Caddy.
|
||||
|
||||
- SQLite via `DATABASE_URL`
|
||||
- local filesystem media in `public/media`
|
||||
- no Docker
|
||||
Current default target:
|
||||
|
||||
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
|
||||
```text
|
||||
https://srv1.bayerischer-mittelstandspreis.de
|
||||
```
|
||||
|
||||
Recommended layout:
|
||||
The app is intentionally split into committed code and uncommitted runtime state.
|
||||
|
||||
## Server Layout
|
||||
|
||||
```text
|
||||
/var/www/bmp-website/
|
||||
current/ # synced application code
|
||||
shared/
|
||||
.env # production secrets
|
||||
content.db # SQLite database
|
||||
media/ # Payload uploads
|
||||
backups/
|
||||
.env # production secrets, never committed
|
||||
content.db # live SQLite database, never committed
|
||||
media/ # live Payload uploads, never committed
|
||||
backups/ # server-side DB backups
|
||||
```
|
||||
|
||||
Create it on the VPS:
|
||||
The Docker Compose file lives in the repo at:
|
||||
|
||||
```text
|
||||
deploy/vps/docker-compose.yml
|
||||
```
|
||||
|
||||
It starts:
|
||||
|
||||
- `bmp_website`: the Next/Payload app
|
||||
- `bmp_caddy`: Caddy reverse proxy on ports `80` and `443`
|
||||
|
||||
Caddy reuses the certificate volumes from the former n8n deployment:
|
||||
|
||||
```text
|
||||
n8n-docker_caddy_data
|
||||
n8n-docker_caddy_config
|
||||
```
|
||||
|
||||
The old n8n containers and volumes may still exist on the server, but are
|
||||
stopped after the BMP deployment takes over public traffic.
|
||||
|
||||
## What Belongs In Git
|
||||
|
||||
Commit:
|
||||
|
||||
- application code
|
||||
- Payload schema/config changes
|
||||
- `Dockerfile`
|
||||
- `.dockerignore`
|
||||
- `deploy/vps/docker-compose.yml`
|
||||
- deployment scripts
|
||||
|
||||
Do not commit:
|
||||
|
||||
- `.env`
|
||||
- `content.db`
|
||||
- `public/media/`
|
||||
- production backups
|
||||
- any secrets
|
||||
|
||||
## Deploying Code
|
||||
|
||||
Normal deploys should be code-only. The live production database and uploads on
|
||||
the VPS are authoritative and must not be overwritten by routine deploys.
|
||||
|
||||
From a clean, committed worktree:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p "$APP_ROOT"/{current,shared/media,backups}
|
||||
sudo chown -R "$APP_USER":"$APP_USER" "$APP_ROOT"
|
||||
pnpm deploy:vps
|
||||
```
|
||||
|
||||
## Production environment
|
||||
The deploy script:
|
||||
|
||||
Create `/var/www/bmp-website/shared/.env` on the VPS:
|
||||
1. refuses to deploy a dirty worktree by default
|
||||
2. rsyncs code to `/var/www/bmp-website/current`
|
||||
3. excludes `.env`, `content.db`, `public/media`, `.next`, and `node_modules`
|
||||
4. writes the deployed Git revision to `.deploy-revision`
|
||||
5. builds the Docker image on the VPS
|
||||
6. recreates only the `bmp_website` app container
|
||||
7. verifies `/` and `/admin`
|
||||
|
||||
```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:
|
||||
If you intentionally need an ad-hoc deploy before committing:
|
||||
|
||||
```bash
|
||||
scp .env "$SSH_TARGET:$APP_ROOT/shared/.env"
|
||||
ssh "$SSH_TARGET" "chmod 600 $APP_ROOT/shared/.env"
|
||||
ALLOW_DIRTY=1 pnpm deploy:vps
|
||||
```
|
||||
|
||||
If you need to package secrets in an archive, make that archive secret-only and
|
||||
encrypt it before transfer:
|
||||
Use that sparingly. The preferred workflow is:
|
||||
|
||||
```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/"
|
||||
```text
|
||||
commit locally -> deploy that commit -> verify production
|
||||
```
|
||||
|
||||
Then decrypt it on the VPS, move `.env` into place, and remove the temporary
|
||||
archive files.
|
||||
## Configuration
|
||||
|
||||
## Initial transfer
|
||||
|
||||
Run these locally from the repository root. Replace the SSH target and domain
|
||||
values first.
|
||||
The scripts default to the current VPS:
|
||||
|
||||
```bash
|
||||
SSH_TARGET=deploy@example.com
|
||||
SSH_USER=syntaxbullet
|
||||
VPS_HOST=srv1.bayerischer-mittelstandspreis.de
|
||||
VPS_PORT=22
|
||||
SSH_KEY=~/.ssh/bmp-vps-deploy
|
||||
APP_ROOT=/var/www/bmp-website
|
||||
APP_DOMAIN=srv1.bayerischer-mittelstandspreis.de
|
||||
```
|
||||
|
||||
Stop any local dev server before copying `content.db`.
|
||||
|
||||
Copy persistent state once:
|
||||
Override any value inline when needed:
|
||||
|
||||
```bash
|
||||
rsync -az content.db "$SSH_TARGET:$APP_ROOT/shared/content.db"
|
||||
rsync -az public/media/ "$SSH_TARGET:$APP_ROOT/shared/media/"
|
||||
APP_DOMAIN=www.example.com pnpm deploy:vps
|
||||
```
|
||||
|
||||
Copy application code without local state or build output:
|
||||
If the Caddy container also needs to be recreated:
|
||||
|
||||
```bash
|
||||
rsync -az --delete \
|
||||
--exclude '.env' \
|
||||
--exclude '.next/' \
|
||||
--exclude 'node_modules/' \
|
||||
--exclude 'content.db' \
|
||||
--exclude 'public/media/' \
|
||||
./ "$SSH_TARGET:$APP_ROOT/current/"
|
||||
DEPLOY_CADDY=1 pnpm deploy:vps
|
||||
```
|
||||
|
||||
Link the shared media directory into the app:
|
||||
## Status And Logs
|
||||
|
||||
```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"
|
||||
pnpm vps:status
|
||||
```
|
||||
|
||||
Do not use `--delete` when syncing media unless the source is intentionally the
|
||||
source of truth and you have a current backup.
|
||||
This prints:
|
||||
|
||||
## Build on the VPS
|
||||
- deployed revision
|
||||
- running containers
|
||||
- old n8n container status
|
||||
- shared DB/media sizes
|
||||
- disk usage
|
||||
- recent app and Caddy logs
|
||||
|
||||
Install Node.js 22 or another version accepted by `package.json`, then run:
|
||||
## Backing Up SQLite
|
||||
|
||||
Create a server-side SQLite backup before schema-sensitive deploys:
|
||||
|
||||
```bash
|
||||
cd /var/www/bmp-website/current
|
||||
npm ci
|
||||
npm run build
|
||||
pnpm vps:backup-db
|
||||
```
|
||||
|
||||
## systemd service
|
||||
This briefly stops the app container, copies
|
||||
`/var/www/bmp-website/shared/content.db` into `/var/www/bmp-website/backups/`,
|
||||
then starts the app container again. Caddy remains running.
|
||||
|
||||
Create `/etc/systemd/system/bmp-website.service`:
|
||||
## Rollback To Old n8n
|
||||
|
||||
```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:
|
||||
The old n8n compose project was intentionally not deleted. If you need to replace
|
||||
the BMP site with the previous n8n service:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now bmp-website
|
||||
sudo systemctl status bmp-website --no-pager
|
||||
CONFIRM=replace-bmp-with-n8n pnpm vps:rollback:n8n
|
||||
```
|
||||
|
||||
## nginx reverse proxy
|
||||
This stops the BMP Caddy/app containers and starts `/root/n8n-docker`.
|
||||
|
||||
Example server block:
|
||||
## Production State Caution
|
||||
|
||||
```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
|
||||
```
|
||||
After launch, CMS edits and uploads happen on the VPS. Do not sync local
|
||||
`content.db` or `public/media/` over production unless you are intentionally
|
||||
restoring or replacing production content and have a current backup.
|
||||
|
||||
Reference in New Issue
Block a user