8.5 KiB
DeepDrftHome — Production Installation Checklist
Fresh-box checklist for deploying the DeepDrftHome solution (DeepDrftPublic, DeepDrftManager, DeepDrftAPI) to a new production host. Every package, directory, and service is treated as absent. Gated steps — those requiring a decision, a secret, or a network action from the operator — are marked [GATE].
This document is a reference you run from. It can drift from the actual
deploy/scripts (bootstrap.sh,install.sh,setup-step10-creds.sh, the systemd units, and the nginx templates) — when those change, update this checklist to match.
Phase 0 — Prerequisites (build/admin machine)
- Confirm DNS A/AAAA records for
deepdrft.comandapp.deepdrft.compoint at the new host's IP — certbot's HTTP-01 challenge fails if DNS hasn't propagated. [GATE] - Generate a CI deploy ed25519 key on your local machine (not the host):
ssh-keygen -t ed25519 -C "gitea-ci-deepdrft-prod" -f ~/.ssh/gitea_deepdrft_prod. Public key → installer prompt; private key → Gitea secretDEEPDRFT_PROD_SSH_DEPLOY. [GATE] - Download the latest
deepdrft-install.tar.gzrelease asset (built bypackage-install.ymlon adeploy/push tomaster). If none exists, push a no-op change todeploy/onmasterand wait for the artifact. [GATE] scp deepdrft-install.tar.gz root@<host>:/tmp/
Phase 1 — Bootstrap / installer (run as root)
- Run:
INSTALL_PKG_PATH=/tmp/deepdrft-install.tar.gz bash bootstrap.sh(installs OS prereqs, hands off toinstall.sh). - The installer is interactive — have ready: app user (
deepdrft//deepdrft), PG role (deepdrft), DB names (deepdrft-meta,deepdrft-auth), public domain, app subdomain (defaultapp.<public>), ports (5000/5001/5002), certbot email, PG password (twice), CI deploy public key. [GATE]
Automated Steps 0–10: apt preflight (postgresql, nginx, rsync, openssl, jq, wget) → create user → enable-linger → directory layout → deploy scripts to /opt/deepdrft/bin/ → systemd units enabled (not started) → credentials (Step 6, prompts) → PostgreSQL role + DBs → authorized_keys forced-command → nginx vhosts → summary.
Phase 2 — Credentials (Step 6, interactive) [GATE]
Writes 6 files to /deepdrft/.config/credentials/ (mode 600): filedatabase.json (no prompt; vault path hardcoded), apikey.json (auto-generate or paste), connections.json (PG password), authblocks.json (JWT secret, issuer/audience, SMTP host+token+From, admin user/email/password, support email), api-public.json + api-manager.json (auto-built). JWT issuer/audience must match appsettings.json AuthBlocks:Jwt.
Verify: sudo -u deepdrft ls -la /deepdrft/.config/credentials/ → 6 × mode 600, owned deepdrft:deepdrft.
Phase 3 — CorsSettings [GATE]
DeepDrftAPI/appsettings.json CorsSettings.AllowedOrigins must include the Manager origin https://app.deepdrft.com. The API throws on startup if origins are empty; a missing Manager origin causes silent 401s on CMS auth. (Confirm this is present before building.)
Phase 4 — Gitea secrets [GATE]
Add DEEPDRFT_PROD_SSH_DEPLOY = full private key contents. (The dev/beta host uses DEEPDRFT_DCH7_SSH_DEPLOY.)
Phase 5 — TLS (after DNS propagates) [GATE]
host deepdrft.com
host app.deepdrft.com
snap install --classic certbot
ln -sf /snap/bin/certbot /usr/bin/certbot
certbot --nginx --email <email> --agree-tos --no-eff-email -d deepdrft.com -d app.deepdrft.com
nginx -t && systemctl reload nginx
certbot renew --dry-run
The installer's vhosts are HTTP-only (listen 80); certbot rewrites them in place to add the 443 blocks.
Phase 6 — Verify SSH forced-command chain (before first deploy) [GATE]
- Layer 1:
ssh -i key deepdrft@host deploy-public→ prints the[deploy-public]prefix then a missing-archive error (non-zero exit expected). - Layer 2:
ssh -i key deepdrft@host id→ssh-wrapper: unknown command: id(no shell). - Layer 3: rsync a smoke file → lands in
/deepdrft/staging/. - Layer 4:
deploy-public,deploy-manager,deploy-apieach print their prefix.
Do not proceed until all four pass.
Phase 7 — First deploy (push master)
Three workflows trigger by path filter:
deploy-api.yml:DeepDrftAPI/,DeepDrftData/,DeepDrftContent/,DeepDrftModels/deploy-public.yml:DeepDrftPublic/,DeepDrftPublic.Client/,DeepDrftShared.Client/,DeepDrftModels/deploy-manager.yml:DeepDrftManager/,DeepDrftShared.Client/,DeepDrftModels/
Root-file-only changes trigger none. deploy-api builds + publishes self-contained linux-x64, runs ef migrations bundle, rsyncs, then deploy-api.sh applies the EF bundle to deepdrft-meta, swaps bin/, restarts the unit. deploy-public also installs the wasm-tools workload. Watch the three parallel Gitea jobs. [GATE]
Phase 8 — EF migration verification [GATE]
The EF bundle runs before the binary swap (metadata DB). The AuthBlocks deepdrft-auth schema self-migrates on first boot and seeds the admin. Verify \dt on both DBs plus __EFMigrationsHistory. On failure: journalctl --user -u deepdrftapi.
Phase 9 — Service health
Check deepdrftapi / deepdrftpublic / deepdrftmanager via systemctl --user status and journalctl. Common failures: missing/wrong credential key; unreadable creds (must be 600 deepdrft:deepdrft); PostgreSQL not on peer auth; wrong vault path; OOM on droplets < 2 GB (add swap).
Phase 10 — Smoke-test per host
API (port 5002, internal):
curl localhost:5002/api/track/page→ empty list on fresh install.curl localhost:5002/api/stats/home→ zeros.POST /api/auth/loginwith admin creds → a JWT.
Public site:
curl https://deepdrft.com/renders.curl https://deepdrft.com/robots.txt→Allow: /(NOTDisallow: /— that means the env isn't Production).curl https://deepdrft.com/sitemap.xml→ XML (static roots present even with 0 releases).- Confirm the unit carries
Environment=ASPNETCORE_ENVIRONMENT=Production.
Manager (CMS):
curl https://app.deepdrft.com/renders.curl https://app.deepdrft.com/robots.txt→Disallow: /(always uncrawlable).curl -o /dev/null -w "%{http_code}" https://app.deepdrft.com/→ 200.
Blazor WebSocket:
curl -H "Upgrade: websocket" -H "Connection: Upgrade" https://deepdrft.com/_blazor→ 101 (not 502/504 from nginx).
Phase 11 — Hardening
- Change the admin password immediately — the seed credentials are stored plaintext on disk. [GATE]
- Firewall (UFW): allow 22/80/443, deny the rest. Port 5002 (API) is internal-only.
apt-get install -y unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades- Review
pg_hba.conf— nohost all all 0.0.0.0/0 md5line; thedeepdrftrole connects via Unix socket (peer auth). - Back up
~/api/deepdrft/vaults— it is not in any deploy artifact or EF bundle; a server wipe loses all audio permanently. [GATE]
Phase 12 — Iteration notes
- EF migrations auto-apply before the binary swap on every deploy — no manual
dotnet ef database update. - AuthBlocks self-migrates on each API start.
- The FileDatabase vault is never touched by deploys; new vault types are created by the app on first access.
- Credential rotation: rerun
setup-step10-creds.sh --forceon the host asdeepdrft, then restart the affected services. - Each deploy script moves the current
bin/tobin.prev/— manual rollback:mv ~/public/bin.prev ~/public/bin && systemctl --user restart deepdrftpublic.service(substitutemanager/api/deepdrft). - Two distinct staging dirs:
~/staging/(CI rsync jail) vs~/api/deepdrft/vaults/staging/(large-audio upload staging). Do not conflate them.
Host path reference
| Path | What |
|---|---|
/deepdrft/.config/credentials/ |
6 × JSON credential files (600) |
/deepdrft/.config/systemd/user/ |
3 × .service unit files |
/deepdrft/public/bin/ |
DeepDrftPublic publish output |
/deepdrft/manager/bin/ |
DeepDrftManager publish output |
/deepdrft/api/deepdrft/bin/ |
DeepDrftAPI publish output |
/deepdrft/api/deepdrft/vaults/ |
FileDatabase vault — never delete, never in deploy |
/deepdrft/staging/ |
rsync jail root (CI artifact drop zone) |
/opt/deepdrft/bin/ssh-wrapper |
Forced-command dispatcher |
/opt/deepdrft/bin/deploy-*.sh |
Per-service deploy scripts |
/etc/nginx/sites-available/deepdrft.com.conf |
Public nginx vhost |
/etc/nginx/sites-available/app.deepdrft.com.conf |
Manager nginx vhost |