Busflow Docs

Internal documentation portal

Skip to content

Pre-First-Client Gate β€” Must Complete Before Onboarding ​

Created: 2026-04-20 Goal: Close all infrastructure, security, and operational gaps that block a production-ready SaaS deployment.


1. Replace MinIO with Hetzner Object Storage (File Storage) ​

Current state: MinIO runs as an in-cluster container on the Hetzner Volume, providing S3-compatible file storage for Nhost Storage. This works but does not scale β€” the volume is block storage with manual resize, and MinIO adds operational overhead (container updates, health monitoring, backup).

Target: Point Nhost Storage directly at Hetzner Object Storage (fsn1.your-objectstorage.com) and remove the MinIO service from docker-compose.production.yml. MinIO stays in preview/dev environments only.

Migration:

  1. Create a Hetzner Object Storage bucket (e.g., busflow-files, region fsn1)
  2. Update docker-compose.production.yml:
    diff
      storage:
    -   S3_ENDPOINT: http://minio:9000
    -   S3_ACCESS_KEY: ${MINIO_ROOT_USER}
    -   S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
    +   S3_ENDPOINT: https://fsn1.your-objectstorage.com
    +   S3_ACCESS_KEY: ${S3_ACCESS_KEY}
    +   S3_SECRET_KEY: ${S3_SECRET_KEY}
        S3_BUCKET: ${S3_BUCKET:-busflow-files}
    -   S3_REGION: us-east-1
    +   S3_REGION: us-east-1  # Hetzner ignores this; SDK accepts it
  3. Delete minio and minio-init services from production compose
  4. Migrate existing MinIO files to the bucket (use mc mirror)
  5. Add S3_ACCESS_KEY / S3_SECRET_KEY to GitHub production environment secrets

Risks to verify:

  • Path-style addressing: Nhost Storage (MinIO Go SDK) auto-detects path-style for non-AWS endpoints. If uploads fail with DNS errors, add S3_FORCE_PATH_STYLE=true.
  • Network egress: The storage container must reach the public Hetzner endpoint over HTTPS. Docker overlay networks allow outbound by default; no firewall changes needed.

Decision: MinIO was kept for initial deployment because it was already configured, requires no external credentials, and avoids the €6/month Hetzner Object Storage base fee during pre-revenue. There is no architectural reason against Hetzner Object Storage β€” it is the correct production choice.


2. Docker Swarm Secrets (Runtime Secret Protection) ​

Current state: All secrets are passed as plaintext environment variables (visible via docker inspect).

Target: Migrate to Docker Swarm Secrets (/run/secrets/) so secrets are encrypted at rest in the Raft log and mounted as tmpfs files.

β†’ Full spec: ADR-029, secrets-rotation-runbook.md, TODOS.md Β§2B.


3. Off-Site Postgres Backups (Cloudflare R2) ​

Current state: postgres-backup service is defined in the compose file but the OFFSITE_S3_* secrets are not set β€” no off-site backups are running.

Target: Create a Cloudflare R2 bucket (busflow-backups, 10 GB free tier), set the secrets, and verify the nightly backup cron.

β†’ Full spec: backup-verify-runbook.md.


4. Volume Monitoring & Auto-Resize ​

Current state: Postgres data on the Hetzner Volume has no disk space monitoring.

Target: Deploy node_exporter + Prometheus alert rules (warning at 80%, critical at 90%) with auto-resize via GitHub Actions workflow.

β†’ Full spec: observability.md Β§Volume Monitoring.


5. Observability Stack Deployment ​

Current state: LGTM stack (Loki, Grafana, Tempo, Mimir) is specced in docker-compose.observability.yml but not deployed.

Target: Deploy the observability stack, enable Postgres exporter (after secrets-sync.yml lands the DSN), and configure Grafana dashboards.

β†’ Full spec: observability.md.

Internal documentation β€” Busflow