deploy: add Docker and Caddy production stack
This commit is contained in:
109
docs/DEPLOYMENT-PLAN.md
Normal file
109
docs/DEPLOYMENT-PLAN.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# 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](PRODUCTION.md) 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:
|
||||
|
||||
```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
|
||||
|
||||
- [Docker Ubuntu installation](https://docs.docker.com/engine/install/ubuntu/)
|
||||
- [Docker firewall behavior](https://docs.docker.com/engine/network/packet-filtering-firewalls/)
|
||||
- [Bun Docker guide](https://bun.sh/guides/ecosystem/docker)
|
||||
- [Caddy automatic HTTPS](https://caddyserver.com/docs/automatic-https)
|
||||
- [Existing backup and recovery workflow](OPERATIONS.md)
|
||||
Reference in New Issue
Block a user