# honeyDue — K3s Production Deployment Production Kubernetes deployment for honeyDue on Hetzner Cloud using K3s. **Architecture**: 3-node HA K3s cluster (CX33), Neon Postgres, Redis (in-cluster), Backblaze B2 (uploads), Cloudflare CDN/TLS. **Domains**: `api.myhoneydue.com`, `admin.myhoneydue.com` --- ## Quick Start ```bash cd honeyDueAPI-go/deploy-k3s # 1. Fill in the single config file cp config.yaml.example config.yaml # Edit config.yaml — fill in ALL empty values # 2. Create secret files # See secrets/README.md for the full list echo "your-neon-password" > secrets/postgres_password.txt openssl rand -base64 48 > secrets/secret_key.txt echo "your-smtp-password" > secrets/email_host_password.txt echo "your-fcm-key" > secrets/fcm_server_key.txt cp /path/to/AuthKey.p8 secrets/apns_auth_key.p8 cp /path/to/origin.pem secrets/cloudflare-origin.crt cp /path/to/origin-key.pem secrets/cloudflare-origin.key # 3. Provision → Secrets → Deploy ./scripts/01-provision-cluster.sh ./scripts/02-setup-secrets.sh ./scripts/03-deploy.sh # 4. Set up Hetzner LB + Cloudflare DNS (see sections below) # 5. Verify ./scripts/04-verify.sh curl https://api.myhoneydue.com/api/health/ ``` That's it. Everything reads from `config.yaml` + `secrets/`. --- ## Table of Contents 1. [Prerequisites](#1-prerequisites) 2. [Configuration](#2-configuration) 3. [Provision Cluster](#3-provision-cluster) 4. [Create Secrets](#4-create-secrets) 5. [Deploy](#5-deploy) 6. [Configure Load Balancer & DNS](#6-configure-load-balancer--dns) 7. [Verify](#7-verify) 8. [Monitoring & Logs](#8-monitoring--logs) 9. [Scaling](#9-scaling) 10. [Rollback](#10-rollback) 11. [Backup & DR](#11-backup--dr) 12. [Security Checklist](#12-security-checklist) 13. [Troubleshooting](#13-troubleshooting) --- ## 1. Prerequisites | Tool | Install | Purpose | |------|---------|---------| | `hetzner-k3s` | `gem install hetzner-k3s` | Cluster provisioning | | `kubectl` | https://kubernetes.io/docs/tasks/tools/ | Cluster management | | `helm` | https://helm.sh/docs/intro/install/ | Optional: Prometheus/Grafana | | `stern` | `brew install stern` | Multi-pod log tailing | | `docker` | https://docs.docker.com/get-docker/ | Image building | | `python3` | Pre-installed on macOS | Config parsing | | `htpasswd` | `brew install httpd` or `apt install apache2-utils` | Admin basic auth secret | Verify: ```bash hetzner-k3s version && kubectl version --client && docker version && python3 --version ``` ## 2. Configuration There are two things to fill in: ### config.yaml — all string configuration ```bash cp config.yaml.example config.yaml ``` Open `config.yaml` and fill in every empty `""` value: | Section | What to fill in | |---------|----------------| | `cluster.hcloud_token` | Hetzner API token (Read/Write) — generate at console.hetzner.cloud | | `registry.*` | GHCR credentials (same as Docker Swarm setup) | | `database.host`, `database.user` | Neon PostgreSQL connection info | | `email.user` | Fastmail email address | | `push.apns_key_id`, `push.apns_team_id` | Apple Push Notification identifiers | | `storage.b2_*` | Backblaze B2 bucket and credentials | | `redis.password` | Strong password for Redis authentication (required for production) | | `admin.basic_auth_user` | HTTP basic auth username for admin panel | | `admin.basic_auth_password` | HTTP basic auth password for admin panel | Everything else has sensible defaults. `config.yaml` is gitignored. ### secrets/ — file-based secrets These are binary or multi-line files that can't go in YAML: | File | Source | |------|--------| | `secrets/postgres_password.txt` | Your Neon database password | | `secrets/secret_key.txt` | `openssl rand -base64 48` (min 32 chars) | | `secrets/email_host_password.txt` | Fastmail app password | | `secrets/fcm_server_key.txt` | Firebase console → Project Settings → Cloud Messaging | | `secrets/apns_auth_key.p8` | Apple Developer → Keys → APNs key | | `secrets/cloudflare-origin.crt` | Cloudflare → SSL/TLS → Origin Server → Create Certificate | | `secrets/cloudflare-origin.key` | (saved with the certificate above) | ## 3. Provision Cluster ```bash export KUBECONFIG=$(pwd)/kubeconfig ./scripts/01-provision-cluster.sh ``` This script: 1. Reads cluster config from `config.yaml` 2. Generates `cluster-config.yaml` for hetzner-k3s 3. Provisions 3x CX33 nodes with HA etcd (5-10 minutes) 4. Writes node IPs back into `config.yaml` 5. Labels the Redis node After provisioning: ```bash kubectl get nodes ``` ## 4. Create Secrets ```bash ./scripts/02-setup-secrets.sh ``` This reads `config.yaml` for registry credentials and creates all Kubernetes Secrets from the `secrets/` files: - `honeydue-secrets` — DB password, app secret, email password, FCM key, Redis password (if configured) - `honeydue-apns-key` — APNS .p8 key (mounted as volume in pods) - `ghcr-credentials` — GHCR image pull credentials - `cloudflare-origin-cert` — TLS certificate for Ingress - `admin-basic-auth` — htpasswd secret for admin panel basic auth (if configured) ## 5. Deploy **Full deploy** (build + push + apply): ```bash ./scripts/03-deploy.sh ``` **Deploy pre-built images** (skip build): ```bash ./scripts/03-deploy.sh --skip-build --tag abc1234 ``` The script: 1. Reads registry config from `config.yaml` 2. Builds and pushes 3 Docker images to GHCR 3. Generates a Kubernetes ConfigMap from `config.yaml` (converts to flat env vars) 4. Applies all manifests with image tag substitution 5. Waits for all rollouts to complete ## 6. Configure Load Balancer & DNS ### Hetzner Load Balancer 1. [Hetzner Console](https://console.hetzner.cloud/) → **Load Balancers → Create** 2. Location: **fsn1**, add all 3 nodes as targets 3. Service: TCP 443 → 443, health check on TCP 443 4. Note the LB IP and update `load_balancer_ip` in `config.yaml` ### Cloudflare DNS 1. [Cloudflare Dashboard](https://dash.cloudflare.com/) → `myhoneydue.com` → **DNS** | Type | Name | Content | Proxy | |------|------|---------|-------| | A | `api` | `` | Proxied (orange cloud) | | A | `admin` | `` | Proxied (orange cloud) | 2. **SSL/TLS → Overview** → Set mode to **Full (Strict)** 3. If you haven't generated the origin cert yet: **SSL/TLS → Origin Server → Create Certificate** - Hostnames: `*.myhoneydue.com`, `myhoneydue.com` - Validity: 15 years - Save to `secrets/cloudflare-origin.crt` and `secrets/cloudflare-origin.key` - Re-run `./scripts/02-setup-secrets.sh` ## 7. Verify ```bash # Automated cluster health check ./scripts/04-verify.sh # External health check (after DNS propagation) curl -v https://api.myhoneydue.com/api/health/ ``` Expected: `{"status": "ok"}` with HTTP 200. ## 8. Monitoring & Logs ### Logs with stern ```bash stern -n honeydue api # All API pod logs stern -n honeydue worker # All worker logs stern -n honeydue . # Everything stern -n honeydue api | grep ERROR # Filter ``` ### kubectl logs ```bash kubectl logs -n honeydue deployment/api -f kubectl logs -n honeydue --previous # Crashed container ``` ### Resource usage ```bash kubectl top pods -n honeydue kubectl top nodes ``` ### Optional: Prometheus + Grafana ```bash helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm install monitoring prometheus-community/kube-prometheus-stack \ --namespace monitoring --create-namespace \ --set grafana.adminPassword=your-password # Access Grafana kubectl port-forward -n monitoring svc/monitoring-grafana 3001:80 # Open http://localhost:3001 ``` ## 9. Scaling ### Manual ```bash kubectl scale deployment/api -n honeydue --replicas=5 kubectl scale deployment/worker -n honeydue --replicas=3 ``` ### HPA (auto-scaling) API auto-scales 3→6 replicas on CPU > 70% or memory > 80%: ```bash kubectl get hpa -n honeydue kubectl describe hpa api -n honeydue ``` ### Adding nodes Edit `config.yaml` to add nodes, then re-run provisioning: ```bash ./scripts/01-provision-cluster.sh ``` ## 10. Rollback ```bash ./scripts/rollback.sh ``` Shows rollout history, asks for confirmation, rolls back all deployments to previous revision. Single deployment rollback: ```bash kubectl rollout undo deployment/api -n honeydue ``` ## 11. Backup & DR | Component | Strategy | Action Required | |-----------|----------|-----------------| | PostgreSQL | Neon PITR (automatic) | None | | Redis | Reconstructible cache + Asynq queue | None | | etcd | K3s auto-snapshots (12h, keeps 5) | None | | B2 Storage | B2 versioning + lifecycle rules | Enable in B2 settings | | Secrets | Local `secrets/` + `config.yaml` | Keep secure offline backup | **Disaster recovery**: Re-provision → re-create secrets → re-deploy. Database recovers via Neon PITR. ## 12. Security See **[SECURITY.md](SECURITY.md)** for the comprehensive hardening guide, incident response playbooks, and full compliance checklist. ### Summary of deployed security controls | Control | Status | Manifests | |---------|--------|-----------| | Pod security contexts (non-root, read-only FS, no caps) | Applied | All `deployment.yaml` | | Network policies (default-deny + explicit allows) | Applied | `manifests/network-policies.yaml` | | RBAC (dedicated SAs, no K8s API access) | Applied | `manifests/rbac.yaml` | | Pod disruption budgets | Applied | `manifests/pod-disruption-budgets.yaml` | | Redis authentication | Applied (if `redis.password` set) | `redis/deployment.yaml` | | Cloudflare-only origin lockdown | Applied | `ingress/ingress.yaml` | | Admin basic auth | Applied (if `admin.*` set) | `ingress/middleware.yaml` | | Security headers (HSTS, CSP, Permissions-Policy) | Applied | `ingress/middleware.yaml` | | Secret encryption at rest | K3s config | `--secrets-encryption` | ### Quick checklist - [ ] Hetzner Firewall: allow only 22, 443, 6443 from your IP - [ ] SSH: key-only auth (`PasswordAuthentication no`) - [ ] `redis.password` set in `config.yaml` - [ ] `admin.basic_auth_user` and `admin.basic_auth_password` set in `config.yaml` - [ ] `kubeconfig`: `chmod 600 kubeconfig`, never commit - [ ] `config.yaml`: contains tokens — never commit, keep secure backup - [ ] Image scanning: `trivy image` or `docker scout cves` before deploy - [ ] Run `./scripts/04-verify.sh` — includes automated security checks ## 13. Troubleshooting ### ImagePullBackOff ```bash kubectl describe pod -n honeydue # Check: image name, GHCR credentials, image exists ``` Fix: verify `registry.*` in config.yaml, re-run `02-setup-secrets.sh`. ### CrashLoopBackOff ```bash kubectl logs -n honeydue --previous # Common: missing env vars, DB connection failure, invalid APNS key ``` ### Redis connection refused / NOAUTH ```bash kubectl get pods -n honeydue -l app.kubernetes.io/name=redis # If redis.password is set, you must authenticate: kubectl exec -it deploy/redis -n honeydue -- redis-cli -a "$REDIS_PASSWORD" ping # Without -a: (error) NOAUTH Authentication required. ``` ### Health check failures ```bash kubectl exec -it deploy/api -n honeydue -- curl -v http://localhost:8000/api/health/ kubectl exec -it deploy/api -n honeydue -- env | sort ``` ### Pods stuck in Pending ```bash kubectl describe pod -n honeydue # For Redis: ensure a node has label honeydue/redis=true kubectl get nodes --show-labels | grep redis ``` ### DNS not resolving ```bash dig api.myhoneydue.com +short # Verify LB IP matches what's in config.yaml ``` ### Certificate / TLS errors ```bash kubectl get secret cloudflare-origin-cert -n honeydue kubectl describe ingress honeydue -n honeydue curl -vk --resolve api.myhoneydue.com:443: https://api.myhoneydue.com/api/health/ ```