-
Fetch SSH key from Vault into a secure temp file:
export VAULT_ADDR=http://127.0.0.1:8200
export PROJECT_SLUG=<project-slug>
TMPKEY=$(mktemp)
trap "rm -f $TMPKEY" EXIT
vault kv get -field=private_key secret/${PROJECT_SLUG}/ssh > "$TMPKEY"
chmod 600 "$TMPKEY"
All subsequent ssh, rsync, and scp commands use -i "$TMPKEY". The file is wiped
automatically when the shell exits or the script completes.
-
Build backend:
cd backend && npm run build
Produces backend/dist/.
-
Build frontend:
cd frontend && npm run build
Produces frontend/dist/. If VITE_DEMO_MODE must be set, export it before building — Vite bakes
this value in at build time and it cannot be changed without a rebuild:
VITE_DEMO_MODE=true npm run build
-
Transfer backend artifacts to VM:
ssh -i "$TMPKEY" ubuntu@<host> "mkdir -p /opt/him/backend/dist"
rsync -avz --delete backend/dist/ ubuntu@<host>:/opt/him/backend/dist/
rsync -avz backend/package.json backend/package-lock.json ubuntu@<host>:/opt/him/backend/
CRITICAL: Always mkdir -p /opt/him/backend/dist before rsyncing. The trailing slash on
backend/dist/ copies the directory's contents — if the target directory does not exist, rsync
creates it one level too shallow and the container fails with
Cannot find module '/app/dist/index.js'.
-
Install node_modules on VM (production only, inside a throwaway container):
ssh -i "$TMPKEY" ubuntu@<host> \
"cd /opt/him/backend && docker run --rm -v \$(pwd):/app -w /app node:22-alpine npm install --production"
-
Transfer frontend build:
rsync -avz --delete frontend/dist/ ubuntu@<host>:/opt/him/frontend/
-
Transfer config files:
scp -i "$TMPKEY" deployment/docker-compose.yml ubuntu@<host>:/opt/him/docker-compose.yml
scp -i "$TMPKEY" deployment/nginx/him.conf ubuntu@<host>:/etc/nginx/sites-available/him
-
Write deploy stamp files (enables SHA verification in the health endpoint):
ssh -i "$TMPKEY" ubuntu@<host> \
"printf '%s' '$(git rev-parse HEAD)' > /opt/him/backend/DEPLOY_SHA && \
printf '%s' '$(date -u +%Y-%m-%dT%H:%M:%SZ)' > /opt/him/backend/DEPLOY_TIME"
Write to /opt/him/backend/, not /opt/him/ — the container mounts /opt/him/backend/ as /app;
files one level up are invisible to the container.
-
Restart containers:
ssh -i "$TMPKEY" ubuntu@<host> "cd /opt/him && docker compose up -d --force-recreate"
Use docker compose (v2, no hyphen). --force-recreate is required because the app is deployed
as files, not as a new image.
-
Reload nginx:
ssh -i "$TMPKEY" ubuntu@<host> "sudo nginx -t && sudo systemctl reload nginx"
-
Verify: Invoke smaqit.infrastructure-deploy-verify with the VM URL.