Files
minabot/docs/DEPLOYMENT-PLAN.md
2026-09-07 11:50:22 +02:00

6.3 KiB

Minabot deployment plan

Target: https://mina.teppelinlabs.com on syntaxbullet@46.62.156.93. Prepared September 7, 2026. Executed the same day; see production operations for the deployed configuration and results.

Verified baseline

  • Ubuntu 26.04.1, x86_64, 3.7 GiB RAM, approximately 34 GiB free disk.
  • DNS A resolves to this server; no AAAA answer was returned.
  • Docker and Caddy are not installed. Only SSH is publicly listening.
  • SSH keys work, password authentication is disabled, UFW and Fail2ban are active.
  • Passwordless sudo is installed in /etc/sudoers.d/90-syntaxbullet and verified with sudo -k followed by sudo -n id in a fresh SSH connection.
  • The checkout has uncommitted live Discord image changes and migrations 0009/0010. Include and validate the intended complete release before tagging it.

Architecture

Internet → Caddy container on TCP 80/443 → app container on port 3000 → SQLite.

Use Docker Compose with one app process and one Caddy container on a shared bridge network. The app needs outbound access to Discord. Publish only Caddy's ports; do not publish app port 3000 or mount the Docker socket into either container. Keep reminders, backups, and live Discord updates in the existing app process. Multiple app replicas are unsuitable for the current live-image worker.

Use /opt/minabot for deployment configuration and tagged releases, /srv/minabot/data mounted as /data for SQLite and snapshots, and persistent Docker volumes for Caddy /data and /config. Match data ownership to the app's non-root container UID. Use restart: unless-stopped, bounded container logs, a health check, and a sufficient startup grace period for migrations/backups. An unhealthy health check alone does not restart a container; monitor it separately.

Implementation sequence

  1. Add HOST configuration to src/index.ts, retaining 127.0.0.1 locally and using HOST=0.0.0.0 inside Docker. The current hard-coded loopback binding prevents Caddy from reaching the app across containers.
  2. Add a multi-stage Dockerfile, .dockerignore, Compose file, Caddyfile, and deployment environment example. Pin tested Bun/Caddy versions and image digests. Build Linux amd64 dependencies in the image using the frozen lockfile; never copy macOS node_modules. Exclude real dotenv files, databases, backups, and Git.
  3. Preserve the existing startup contract: migrations must succeed before scripts/start.ts loads dist/index.js. Include migrations and required script imports, production dependencies, built assets, and the bundled Instrument Serif font. Use a glibc-based Bun runtime with a sans-serif font installed, and verify the native @napi-rs/canvas renderer inside that exact image.
  4. Install Docker Engine and Compose from Docker's official Ubuntu repository. Ubuntu 26.04 is supported. Keep Docker's firewall management enabled; published ports can bypass UFW. Publish only 80/443 and verify exposure externally. Align the existing Fail2ban nftables action with the chosen Docker firewall backend (prefer an iptables-compatible SSH action for the standard Docker backend), then recheck SSH protection. Open TCP 80/443 in UFW and any Hetzner firewall.
  5. Put runtime secrets in /opt/minabot/.env.production with mode 0600 and inject through Compose env_file. Set NODE_ENV=production, HOST=0.0.0.0, PORT=3000, APP_ORIGIN=https://mina.teppelinlabs.com, and DATABASE_PATH=/data/minabot.sqlite. Supply DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, a stable random AUTH_COOKIE_SECRET, and the bot/channel settings. Do not bake secrets into images or build arguments. Register this Discord OAuth redirect exactly: https://mina.teppelinlabs.com/api/auth/discord/callback.
  6. Default to a fresh production database. If existing local data should be moved, use a verified SQLite snapshot, not a live database-file copy. Avoid running two workers against copied live Discord message state. Resolve the data choice before first production startup.
  7. Build and tag the release, start it on the private Docker network, verify its health, then start Caddy. Caddy obtains and renews TLS certificates and redirects HTTP to HTTPS. Persist its certificate storage across container replacement.

Proposed Caddyfile:

mina.teppelinlabs.com {
    encode zstd gzip
    reverse_proxy app:3000
}

Verification and operations

  • Before release: typecheck, tests, build, and disposable production API smoke test. Validate Compose and Caddy configuration; run migrations, health, static assets, native PNG rendering, and a restart/persistence test inside the Linux image.
  • After launch: verify public HTTPS, HTTP redirect, /api/health, Discord sign-in, secure cookies, habit persistence, and port 3000 being unreachable externally. Exercise bot workers with stubbed transports; a live Discord post or DM needs an explicitly selected test destination and authorization to send it.
  • Keep daily verified snapshots and seven retained copies using the existing app backup system. Configure an off-server destination separately; none is provisioned yet. Test restore to a new database with the matching image before relying on it.
  • Configure an independent HTTPS monitor and alert destination. Existing monitor scripts provide checks but do not provision an external monitoring service.
  • Updates: build/test the next immutable image, snapshot the database, stop the old app, and start the new release. Accept brief downtime; do not overlap workers. Keep the previous image. If schema compatibility prevents image-only rollback, stop the app and restore its matching pre-migration snapshot to a new file. The restore tool revokes sessions and disables reminders; audit restored live Discord state before starting workers. Never use docker compose down -v.

Remaining launch inputs: fresh database versus migration, Discord redirect setup and production credentials, off-server backup destination, and monitoring alerts.

References