Update and generalize the deploy workflow
This commit is contained in:
parent
17f53946f1
commit
39d8d920b6
4 changed files with 59 additions and 18 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
.local
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
|
|
@ -18,6 +19,10 @@ Thumbs.db
|
||||||
!.env.example
|
!.env.example
|
||||||
!.env.test
|
!.env.test
|
||||||
|
|
||||||
|
# Deploy: only the example unit is tracked
|
||||||
|
/deploy/*
|
||||||
|
!/deploy/production-example.service
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,10 @@ Preview a build locally with `npm run preview` (static) or `node --env-file=.env
|
||||||
|
|
||||||
`just deploy <host>` rsyncs the static bundle to `~/squalk/` on the host and purges the Cloudflare cache.
|
`just deploy <host>` rsyncs the static bundle to `~/squalk/` on the host and purges the Cloudflare cache.
|
||||||
|
|
||||||
`just deploy-ssr <host>` ships the Node build, `package.json`/`package-lock.json` and `.env.production` (as `~/squalk/.env`, since the server reads the `PUBLIC_*` values at runtime), runs `npm ci --omit=dev` and restarts the `squalk` systemd unit. On the host you need:
|
`just deploy-ssr <mode>` builds with `--mode <mode>` (so vite bakes `.env.<mode>` in), ships the Node build, `package.json`/`package-lock.json` and `.env.<mode>` (as `.env` in the app dir, since the server reads the `PUBLIC_*` values at runtime), runs `npm ci --omit=dev` and restarts the instance's systemd unit. Everything instance-specific lives in `.env.<mode>.local` (gitignored, never shipped): `DEPLOY_HOST`, `DEPLOY_DIR` and `DEPLOY_SERVICE` (all required), plus the Cloudflare credentials (`CF_ZONE_ID`/`CF_API_TOKEN`) for the cache purge. Multiple instances coexist by giving each its own mode, directory, unit and port. On the host you need:
|
||||||
|
|
||||||
- Node 22 or newer (the relay client uses the built-in `WebSocket`).
|
- Node 22 or newer (the relay client uses the built-in `WebSocket`).
|
||||||
- The unit from [`deploy/squalk.service`](deploy/squalk.service), with `ORIGIN` set to the public URL — it feeds canonical links, `robots.txt` and the sitemap.
|
- The unit from [`deploy/production-example.service`](deploy/production-example.service), with `ORIGIN` set to the public URL — it feeds canonical links, `robots.txt` and the sitemap.
|
||||||
- A reverse proxy in front of the port in `PORT`, replacing whatever served the static files before. With Caddy:
|
- A reverse proxy in front of the port in `PORT`, replacing whatever served the static files before. With Caddy:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
25
deploy/production-example.service
Normal file
25
deploy/production-example.service
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Example systemd unit for the server-rendered build (PUBLIC_SSR=yes).
|
||||||
|
# Copy to /etc/systemd/system/<instance>.service, adjust the paths, the port
|
||||||
|
# and ORIGIN, then: systemctl daemon-reload && systemctl enable --now <instance>
|
||||||
|
[Unit]
|
||||||
|
Description=Squalk forum
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
# Ephemeral unprivileged user, created by systemd for the service's lifetime
|
||||||
|
# (implies ProtectSystem=strict, ProtectHome=read-only, PrivateTmp,
|
||||||
|
# NoNewPrivileges). The app dir stays root-owned; the service only reads it.
|
||||||
|
DynamicUser=yes
|
||||||
|
WorkingDirectory=/srv/squalk
|
||||||
|
# PUBLIC_* values read at runtime by the server (deploy-ssr copies .env.<mode> here)
|
||||||
|
EnvironmentFile=/srv/squalk/.env
|
||||||
|
# ORIGIN is the public URL: it drives canonical links, robots.txt and the sitemap
|
||||||
|
Environment=NODE_ENV=production PORT=3000 ORIGIN=https://forum.example.com
|
||||||
|
ExecStart=/usr/bin/node build
|
||||||
|
Restart=on-failure
|
||||||
|
TimeoutStopSec=10
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
43
justfile
43
justfile
|
|
@ -1,9 +1,7 @@
|
||||||
set dotenv-load
|
set dotenv-load
|
||||||
|
|
||||||
# Cloudflare credentials (set these as environment variables)
|
# Cloudflare credentials: per deploy target in .env.<mode>.local (gitignored,
|
||||||
CF_ZONE_ID := env_var_or_default("CF_ZONE_ID", "")
|
# never shipped to the server), falling back to the environment / .env
|
||||||
CF_API_TOKEN := env_var_or_default("CF_API_TOKEN", "")
|
|
||||||
CF_HOST := env_var_or_default("CF_HOST", "")
|
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
@ -12,7 +10,7 @@ dev:
|
||||||
build:
|
build:
|
||||||
PUBLIC_SSR=no npm run build
|
PUBLIC_SSR=no npm run build
|
||||||
|
|
||||||
# Server-rendered bundle (needs Node 22+ on the host, see deploy/squalk.service)
|
# Server-rendered bundle (needs Node 22+ on the host, see deploy/production-example.service)
|
||||||
build-ssr:
|
build-ssr:
|
||||||
PUBLIC_SSR=yes npm run build
|
PUBLIC_SSR=yes npm run build
|
||||||
|
|
||||||
|
|
@ -21,18 +19,31 @@ deploy target: build
|
||||||
@just purge-web-cache
|
@just purge-web-cache
|
||||||
|
|
||||||
# Ships the Node build plus its runtime deps and env, then restarts the unit.
|
# Ships the Node build plus its runtime deps and env, then restarts the unit.
|
||||||
# The remote step runs in a login shell so the user's PATH (npm, nvm…) applies.
|
# `mode` picks the instance: vite bakes .env.<mode> into the build, and
|
||||||
deploy-ssr target: build-ssr
|
# .env.<mode>.local provides the deployment details (DEPLOY_HOST, DEPLOY_DIR,
|
||||||
rsync -av --delete --progress --exclude node_modules build/ {{target}}:~/squalk/build/
|
# DEPLOY_SERVICE) plus the Cloudflare credentials. The remote step runs in a
|
||||||
rsync -av package.json package-lock.json {{target}}:~/squalk/
|
# login shell so the user's PATH (npm, nvm…) applies.
|
||||||
rsync -av .env.production {{target}}:~/squalk/.env
|
deploy-ssr mode:
|
||||||
ssh {{target}} '$SHELL -l -c "cd ~/squalk && npm ci --omit=dev && sudo systemctl restart squalk"'
|
#!/usr/bin/env bash
|
||||||
@just purge-web-cache
|
set -euo pipefail
|
||||||
|
[ -f .env.{{mode}}.local ] || { echo "Missing .env.{{mode}}.local"; exit 1; }
|
||||||
|
set -a; source .env.{{mode}}.local; set +a
|
||||||
|
: "${DEPLOY_HOST:?DEPLOY_HOST missing in .env.{{mode}}.local}"
|
||||||
|
: "${DEPLOY_DIR:?DEPLOY_DIR missing in .env.{{mode}}.local}"
|
||||||
|
: "${DEPLOY_SERVICE:?DEPLOY_SERVICE missing in .env.{{mode}}.local}"
|
||||||
|
PUBLIC_SSR=yes npm run build -- --mode {{mode}}
|
||||||
|
rsync -av --delete --progress --exclude node_modules build/ "$DEPLOY_HOST:$DEPLOY_DIR/build/"
|
||||||
|
rsync -av package.json package-lock.json "$DEPLOY_HOST:$DEPLOY_DIR/"
|
||||||
|
rsync -av .env.{{mode}} "$DEPLOY_HOST:$DEPLOY_DIR/.env"
|
||||||
|
ssh "$DEPLOY_HOST" "\$SHELL -l -c 'cd $DEPLOY_DIR && npm ci --omit=dev && sudo systemctl restart $DEPLOY_SERVICE'"
|
||||||
|
just purge-web-cache {{mode}}
|
||||||
|
|
||||||
purge-web-cache:
|
purge-web-cache mode="production":
|
||||||
@echo "\nPurging Cloudflare cache... for zone {{CF_ZONE_ID}}"
|
#!/usr/bin/env bash
|
||||||
@curl -s -X POST "https://api.cloudflare.com/client/v4/zones/{{CF_ZONE_ID}}/purge_cache" \
|
if [ -f .env.{{mode}}.local ]; then set -a; source .env.{{mode}}.local; set +a; fi
|
||||||
-H "Authorization: Bearer {{CF_API_TOKEN}}" \
|
echo -e "\nPurging Cloudflare cache... for zone ${CF_ZONE_ID:-<unset>}"
|
||||||
|
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID:-}/purge_cache" \
|
||||||
|
-H "Authorization: Bearer ${CF_API_TOKEN:-}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
--data '{"purge_everything": true}' \
|
--data '{"purge_everything": true}' \
|
||||||
| jq -r 'if .success then "✅ Cache purged successfully" else "‼️ Error: " + (.errors[0].message // "Unknown error") end'
|
| jq -r 'if .success then "✅ Cache purged successfully" else "‼️ Error: " + (.errors[0].message // "Unknown error") end'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue