# DevOps — Taskfile + Fly.io + Supabase + Docker + K8s > Canonical DevOps patterns for André Bassi's projects. Taskfile-first (scripts are the audit trail), tests gate deploys, chainguard images, credentials only via pass. Point an LLM here and it automates/deploys exactly in this pattern. Dedicated deep-dive specs: IaC -> llms-terraform.txt | provisioning -> llms-ansible.txt | task automation -> llms-taskfile.txt. ## Hard Rules 1. Taskfile-first: NEVER run ad-hoc shell commands. Create `scripts/.sh`, wrap in a `Taskfile.yaml` task, run `task `. 2. Every command output logged: `2>&1 | tee /tmp/.txt`. 3. Every command gets `timeout 30s` (exception: docker build/push). 4. Tests gate deploy: `build` and `deploy` tasks declare `deps: [test]`. Failing test = no deploy. 5. Credentials ONLY via `pass show //` exported to env vars. Never echo, never plaintext. `unset` after use. 6. Final container image: `chainguard/static:latest` (0 CVEs) for Go binaries. 7. kubectl: verify context BEFORE any command. KUBECONFIG explicit. Never `-w` watch flag. ## Taskfile Vocabulary (task names are the project's public API) dev # full stack (backend + frontend + worker) dev:api dev:worker web:dev test # all tests — dependency of build & deploy test:backend # deps: [supabase:start]; GOWORK=off go test ./... -cover -race test:frontend # pnpm vitest run test:coverage # HTML coverage reports lint # golangci-lint + eslint build # CGO_ENABLED=0 static binary; deps: [test] supabase:start supabase:reset migrate # push migrations to remote migrate:local # apply locally deploy deploy:backend deploy:frontend deploy:worker logs:fly status:fly health Taskfile snippet pattern: version: '3' tasks: test:backend: deps: [supabase:start] dir: backend cmds: - GOWORK=off go test ./... -cover -race 2>&1 | tee /tmp/test-backend.log build: deps: [test] cmds: - ./scripts/build.sh 2>&1 | tee /tmp/build.log deploy:backend: deps: [test] cmds: - ./scripts/fly-deploy.sh backend 2>&1 | tee /tmp/deploy-backend.log ## scripts/ Folder Pattern scripts/ ├── version.sh # semver from commit count ├── fly-setup.sh # create apps + stage secrets + cert (idempotent) ├── fly-deploy.sh [backend|frontend|all] ├── fly-dns.sh # Cloudflare CNAME via API ├── fly-access.sh # Cloudflare Zero Trust Access app (idempotent) └── migrate-*.sh # data migration utilities - Scripts are reference, reusable, auditable. They document what was done. - All scripts `#!/bin/bash` + `set -euo pipefail`. ## Docker (multi-stage, distroless) FROM golang:1.26-alpine AS builder RUN apk add --no-cache git WORKDIR /app COPY . . RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${VERSION}" -o server ./cmd/server FROM chainguard/static:latest COPY --from=builder /app/server /server EXPOSE 3000 ENTRYPOINT ["/server"] - Backend Dockerfile: use `ENTRYPOINT []` + `CMD ["/app/api"]` when Fly `release_command` must override. - Multi-arch to registry: `docker buildx build --platform linux/amd64,linux/arm64 --push -t registry/image:revN .` - ALWAYS increment revN and update deploy manifests. - Ephemeral/dev registry: `ttl.sh` (24h). Production: semver tags (v1.2.3). ## Fly.io - Region: `iad` (co-located Supabase us-east-1) or `gru` (Brazil-facing). - Backend: `strategy = "immediate"`, `min/max_machines_running = 1` (no multi-instance when SHARD_ID=0). - Frontend: `force_https = true`, `auto_stop = off`. `[build.args]` carry `NEXT_PUBLIC_*`. - Temporal worker: `strategy = "immediate"` (rolling with max_unavailable=0 panics; immediate tolerates ~6s downtime). - Temporal server: `auto_stop = false`, `min_machines_running = 1` (cloudflared .internal doesn't trigger auto_start). - Migrations run in `release_command` (cmd/migrate). - Postgres on Fly: `fly proxy 15432:5432 -a &` then psql via localhost:15432. NEVER `fly postgres connect`. - Deploy + debug loop: `fly deploy` → `fly logs` → curl endpoint → fix → redeploy. ## Supabase / PostgreSQL - PostgreSQL 15+; backend connects as `postgres`/service_role (bypasses RLS). - RLS active on tenant-scoped tables (tenants, devices, ...). - Migrations: `supabase/migrations/_.sql` — sequential, idempotent. Push: `supabase db push`. - DATABASE_URL: direct 5432, NOT pooler 6543 (pooler `postgres.` format fails on new projects). - Managed PG always `sslmode=require`. - pg_dump version mismatch fix: run pg_dump inside the supabase db container (`docker exec supabase_db_ pg_dump ...`). ## Cloudflare - DNS proxied (orange cloud) when using Cloudflare Access — Access only works behind proxy. - SSL mode Full (not Flexible) when origin has valid cert (Fly LE cert). - Zero Trust Access: OTP by email for demo/internal apps; app + policy created via API script (idempotent). - Workers/Containers: `npx wrangler deploy --containers-rollout=immediate && npx wrangler tail --format=pretty`. ## Kubernetes (infra-heavy projects) - Verify before ANY kubectl: `kubectl config current-context`. - KUBECONFIG explicit: `/Users/andrebassi/.kube/config`. - Never `-w` watch. Max sleep 15s; prefer status checks (`kubectl describe`, conditions). - GitOps: ArgoCD + GitLab CI. IaC: Terraform/Terragrunt, Helm, Ansible. - Stack: EKS, GKE, Talos, MicroK8s; KubeBlocks for DBs; Istio mesh; KEDA autoscaling. - Delete with finalizers: `timeout 30s kubectl delete ...` then patch `{"metadata":{"finalizers":[]}}`. - External Secrets + GitLab: check project ID, token scopes (read_api), force reconcile via annotate. ## Credentials (pass) pass ls // export AWS_ACCESS_KEY_ID=$(pass show //aws-access-key) export AWS_SECRET_ACCESS_KEY=$(pass show //aws-secret-key) # SSH keys: pass show //ssh-key > /tmp/key.pem && chmod 600 /tmp/key.pem ssh -i /tmp/key.pem user@host "cmd" && rm -f /tmp/key.pem - Structure: `//`. - Cleanup after use: `unset AWS_SECRET_ACCESS_KEY`, remove temp key files. ## Observability - Go: Sentry SDK + slog JSON middleware (`internal/observability`). - Health endpoints on every service; `task health` checks them. - Post-incident: postmortem doc (summary, timeline, root cause, resolution, impact, prevention, lessons). ## CI/CD - GitLab CI for pipelines; ArgoCD for K8s GitOps; `fly deploy` for Fly apps. - Pipeline order: lint → test (coverage gates) → build → deploy. - Git pushes to GitLab with token: `git push "https://oauth2:${TOKEN}@gitlab.com/.git" `.