sanook-cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/CHANGELOG.md +144 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +394 -51
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +2 -2
- package/dist/providers/keys.js +3 -2
- package/dist/providers/registry.js +133 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +218 -27
- package/dist/ui/banner.js +4 -9
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/setup.js +6 -5
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- package/skills/visual-regression-testing/SKILL.md +120 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compose-local-dev-stack
|
|
3
|
+
description: Wires a local multi-service development stack with Docker Compose — app plus backing datastores (Postgres/Redis/Kafka), dependency-ordered healthchecks (depends_on condition: service_healthy), pinned images and named volumes, seed/init scripts, hot-reload bind mounts, profiles, and one-command up/down/reset via a Makefile.
|
|
4
|
+
when_to_use: An app needs real local backing services (db, cache, queue) and "start everything" is fragile, slow, or undocumented. Not the dev container the editor runs in (setup-devcontainer-env), not the shippable app image (dockerfile-optimize), not cluster deployment (k8s-manifest-review).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this when the request is about **standing up the app's runtime dependencies on a laptop**, reproducibly, with one command:
|
|
10
|
+
|
|
11
|
+
- "Get Postgres + Redis + the API running locally so I can develop"
|
|
12
|
+
- "The onboarding doc says `docker compose up` but it races / half the services aren't ready"
|
|
13
|
+
- "Add Kafka (or a queue, or a second DB) to the local stack"
|
|
14
|
+
- "I want hot reload — edit code, see it without rebuilding the image"
|
|
15
|
+
- "Seed the dev database automatically" / "give me a clean-slate reset command"
|
|
16
|
+
|
|
17
|
+
NOT this skill:
|
|
18
|
+
- The container the editor/agent itself runs inside (devcontainer.json, features, VS Code attach) → setup-devcontainer-env
|
|
19
|
+
- Shrinking/hardening the **production** image (multi-stage, distroless, non-root) → dockerfile-optimize
|
|
20
|
+
- Deploying these services to a cluster (Deployments, probes, resource limits, Helm) → k8s-manifest-review
|
|
21
|
+
- Pinning the *host* language/tool versions (node/python/go via asdf/mise/`.tool-versions`) → pin-toolchain-versions
|
|
22
|
+
- A schema change's lock/data-loss safety → db-migration-safety (this skill only *runs* migrations on start)
|
|
23
|
+
|
|
24
|
+
## Steps
|
|
25
|
+
|
|
26
|
+
1. **One file, services as the unit. Pin every tag, name every volume.** Floating `:latest` makes the stack non-reproducible and breaks silently on pull; bare anonymous volumes orphan and lose data on `down`. Use `compose.yaml` (the modern name — drop the `version:` key, it's obsolete):
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
name: myapp
|
|
30
|
+
services:
|
|
31
|
+
db:
|
|
32
|
+
image: postgres:16.4-alpine # pin minor; never :latest
|
|
33
|
+
environment:
|
|
34
|
+
POSTGRES_USER: app
|
|
35
|
+
POSTGRES_PASSWORD: app
|
|
36
|
+
POSTGRES_DB: app
|
|
37
|
+
volumes:
|
|
38
|
+
- pgdata:/var/lib/postgresql/data # named -> survives `down`
|
|
39
|
+
- ./db/init:/docker-entrypoint-initdb.d:ro # runs ONCE on empty volume
|
|
40
|
+
healthcheck:
|
|
41
|
+
test: ["CMD-SHELL", "pg_isready -U app -d app"]
|
|
42
|
+
interval: 3s
|
|
43
|
+
timeout: 3s
|
|
44
|
+
retries: 20
|
|
45
|
+
start_period: 5s
|
|
46
|
+
ports: ["5432:5432"]
|
|
47
|
+
|
|
48
|
+
redis:
|
|
49
|
+
image: redis:7.4-alpine
|
|
50
|
+
command: ["redis-server", "--save", "", "--appendonly", "no"] # ephemeral cache
|
|
51
|
+
healthcheck:
|
|
52
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
53
|
+
interval: 3s
|
|
54
|
+
timeout: 2s
|
|
55
|
+
retries: 20
|
|
56
|
+
|
|
57
|
+
app:
|
|
58
|
+
build: { context: ., target: dev } # dev stage, not prod
|
|
59
|
+
command: ["npm", "run", "dev"] # hot-reload command, overrides Dockerfile CMD
|
|
60
|
+
depends_on:
|
|
61
|
+
db: { condition: service_healthy } # waits for healthcheck, not just "started"
|
|
62
|
+
redis: { condition: service_healthy }
|
|
63
|
+
environment:
|
|
64
|
+
DATABASE_URL: postgres://app:app@db:5432/app # use service name, not localhost
|
|
65
|
+
REDIS_URL: redis://redis:6379
|
|
66
|
+
volumes:
|
|
67
|
+
- ./src:/app/src # bind mount -> edits reflect live
|
|
68
|
+
- /app/node_modules # anon vol masks host node_modules
|
|
69
|
+
ports: ["3000:3000"]
|
|
70
|
+
|
|
71
|
+
volumes:
|
|
72
|
+
pgdata:
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. **Order startup with `depends_on: condition: service_healthy` — never bare `depends_on`.** Bare `depends_on` only waits for the container to *start*, not to be *ready*; the app then connects to a Postgres still replaying WAL and crash-loops. The gate is the **healthcheck on each backing service**. Pick the right probe per service:
|
|
76
|
+
|
|
77
|
+
| Service | Healthcheck test | Why not just TCP |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Postgres | `pg_isready -U $USER -d $DB` | port opens before it accepts queries |
|
|
80
|
+
| MySQL | `mysqladmin ping -h localhost` | same early-port problem |
|
|
81
|
+
| Redis | `redis-cli ping` → `PONG` | trivial, do it |
|
|
82
|
+
| Kafka (KRaft) | `kafka-broker-api-versions --bootstrap-server localhost:9092` | broker advertises before it serves metadata |
|
|
83
|
+
| RabbitMQ | `rabbitmq-diagnostics -q ping` | mgmt port lies about readiness |
|
|
84
|
+
| Elasticsearch | `curl -fsS localhost:9200/_cluster/health?wait_for_status=yellow` | green never comes single-node |
|
|
85
|
+
| App migrations | a one-shot `migrate` service the app `depends_on` (condition: `service_completed_successfully`) | keeps schema setup off the app's hot path |
|
|
86
|
+
|
|
87
|
+
Tune `retries × interval ≥ real cold-start time` (Kafka/ES need `start_period: 20s`+) or healthy never arrives and the dependents abort.
|
|
88
|
+
|
|
89
|
+
3. **Seed once via `docker-entrypoint-initdb.d`; run migrations every start via a one-shot service.** The init dir (`*.sql`/`*.sh`, alphabetical) runs **only when the data volume is empty** — perfect for extensions, roles, and static seed (`01-schema.sql`, `02-seed.sql`). It does **not** re-run after the volume exists, so never put evolving migrations there. Migrations belong in a dedicated short-lived service the app waits on:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
migrate:
|
|
93
|
+
build: { context: ., target: dev }
|
|
94
|
+
command: ["npm", "run", "migrate:deploy"] # or: alembic upgrade head / flyway migrate
|
|
95
|
+
depends_on: { db: { condition: service_healthy } }
|
|
96
|
+
restart: "no"
|
|
97
|
+
```
|
|
98
|
+
Then set `app.depends_on.migrate.condition: service_completed_successfully`. Idempotent migration tools make this safe to run on every `up`.
|
|
99
|
+
|
|
100
|
+
4. **Hot reload = bind mount source + a dev `command` + a watcher, not a rebuild.** Bind `./src:/app/src` and run the dev server (`npm run dev`/`uvicorn --reload`/`air`/`nodemon`). Mask installed deps with an **anonymous volume** (`- /app/node_modules`) so the host's empty/mismatched dir doesn't shadow the image's. Build the image from a **`dev` stage** (`target: dev`) that includes dev deps and the watcher — keep the lean prod stage for shipping (that's dockerfile-optimize's job). Changing `package.json`/`requirements.txt` still needs a rebuild; code does not.
|
|
101
|
+
|
|
102
|
+
5. **Split config: committed `compose.yaml` + `.env` + an uncommitted `compose.override.yml`.** Compose **auto-merges** `compose.override.yml` on top of `compose.yaml` with no `-f` flag — put local-only tweaks there (extra port bindings, mounted debug tools, `DEBUG=1`) and gitignore it so teammates' hacks don't collide. Variables interpolate from `.env` (committed `.env.example`, real `.env` gitignored). Never hardcode host-specific ports or paths in the base file.
|
|
103
|
+
|
|
104
|
+
6. **Gate optional services behind `profiles`.** Tag heavy/rarely-needed services (Kafka, a second DB, mailhog, a metrics stack) with `profiles: ["kafka"]` so a plain `docker compose up` starts only the core stack. Opt in with `docker compose --profile kafka up`. Keeps the default path fast; a service with no `profiles` always runs.
|
|
105
|
+
|
|
106
|
+
7. **Use the default network and talk service-to-service by name; publish only the host ports you need.** Compose gives you a default bridge network where services resolve each other by **service name** (`db`, `redis`) — the app must use `db:5432`, never `localhost:5432` (localhost inside the app container is the app). Publish stable host ports (`5432:5432`) only for tools you run on the host (psql, a GUI). Collisions with a host Postgres → remap the **host** side (`5433:5432`), never the container side.
|
|
107
|
+
|
|
108
|
+
8. **Make one-command verbs in a `Makefile` (or `Taskfile.yml`) so nobody memorizes flags.** `up` must block until healthy; `reset` must wipe volumes:
|
|
109
|
+
|
|
110
|
+
```makefile
|
|
111
|
+
up: ## start core stack, wait until healthy
|
|
112
|
+
docker compose up -d --wait
|
|
113
|
+
down: ## stop, keep data
|
|
114
|
+
docker compose down
|
|
115
|
+
reset: ## stop AND wipe volumes -> clean slate
|
|
116
|
+
docker compose down -v --remove-orphans
|
|
117
|
+
docker compose up -d --wait
|
|
118
|
+
logs: ## tail everything
|
|
119
|
+
docker compose logs -f --tail=100
|
|
120
|
+
ps:
|
|
121
|
+
docker compose ps
|
|
122
|
+
```
|
|
123
|
+
`--wait` makes `up` exit non-zero if any service never goes healthy — that's your machine-checkable gate. `down -v` is the *only* thing that deletes data; keep it on `reset` alone so `down` is always safe.
|
|
124
|
+
|
|
125
|
+
## Common Errors
|
|
126
|
+
|
|
127
|
+
- **Bare `depends_on:` (list form).** Waits for container *start*, not readiness; the app races the DB and crash-loops on cold boot. Use the map form with `condition: service_healthy`.
|
|
128
|
+
- **No `healthcheck` on a backing service.** Then `service_healthy` has nothing to gate on and Compose errors or treats it as instantly up. Every service you depend-on needs a real probe (table in step 2).
|
|
129
|
+
- **App connects to `localhost` instead of the service name.** `localhost` inside the app container is the app itself — connection refused. Use `db`/`redis`/`kafka` (the service names) in `DATABASE_URL`/`REDIS_URL`.
|
|
130
|
+
- **Anonymous/missing volume on a datastore.** `docker compose down` orphans the anonymous volume and the next `up` starts empty; data "randomly" vanishes. Always name datastore volumes and declare them under `volumes:`.
|
|
131
|
+
- **Expecting `docker-entrypoint-initdb.d` to re-run.** It runs **only on an empty data volume**. Edited a seed file and "nothing happened"? The volume already exists — `docker compose down -v` (or `make reset`) to re-init. Don't put live migrations there.
|
|
132
|
+
- **`start_period` too short for Kafka/Elasticsearch.** They take 20–60s to be ready; with the default `start_period: 0s` and few retries, healthy never arrives and dependents abort. Set `start_period: 30s` and enough `retries`.
|
|
133
|
+
- **`:latest` / unpinned tags.** A teammate pulls a newer Postgres major, the data dir format changes, the volume won't mount. Pin to a minor tag (`postgres:16.4-alpine`).
|
|
134
|
+
- **Host port already in use (`bind: address already in use`).** A host Postgres or a previous stack holds 5432. Remap the host side only (`5433:5432`); changing the container side breaks intra-network DNS.
|
|
135
|
+
- **Host `node_modules`/`venv` shadowing the image's via the source bind mount.** App can't find deps or loads wrong-arch binaries. Add the anonymous-volume mask (`- /app/node_modules`) *after* the source bind.
|
|
136
|
+
- **Secrets committed in `compose.yaml`.** Real credentials in the base file leak to git. Keep them in the gitignored `.env`; commit only `.env.example` with placeholders.
|
|
137
|
+
|
|
138
|
+
## Verify
|
|
139
|
+
|
|
140
|
+
1. **Cold up from nothing:** `make reset` (wipes), then `make up`. The command must **exit 0** — `--wait` fails the command if any service is unhealthy. `docker compose ps` shows every core service `running (healthy)`.
|
|
141
|
+
2. **Ordering held:** check `docker compose logs migrate` / app — the app started its first DB query *after* `db` was healthy and migrations completed, with **no** connection-refused retries in the log.
|
|
142
|
+
3. **Seeded:** `docker compose exec db psql -U app -d app -c "select count(*) from <seeded_table>;"` returns the expected non-zero count without any manual step.
|
|
143
|
+
4. **Hot reload:** with the stack up, edit a source file under the bind mount → the app reloads and serves the change **without** `docker compose build` or restart.
|
|
144
|
+
5. **Reachability:** a host tool hits the published port (`psql -h localhost -p 5432 -U app`), and the app reaches the DB **by service name** (no `localhost` in its config).
|
|
145
|
+
6. **Reset is clean:** `make reset` recreates the stack and the seeded count from step 3 matches again (volume truly wiped and re-init'd, not stale).
|
|
146
|
+
7. **Profiles:** plain `docker compose up -d --wait` starts only core services; `--profile kafka up` additionally starts the gated ones; `docker compose ps` confirms each case.
|
|
147
|
+
8. **`down` is safe:** `make down` then `make up` preserves data (row count unchanged); only `make reset` resets it.
|
|
148
|
+
|
|
149
|
+
Done = `make reset && make up` exits 0 with every service `healthy`, the DB is auto-seeded, a source edit hot-reloads without a rebuild, the app talks to backing services by name, and `make reset` reproducibly returns the stack to a clean seeded state.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configure-bundler-build
|
|
3
|
+
description: Configures and optimizes the JS/TS build toolchain — tsconfig plus a bundler (Vite/esbuild/Rollup/tsup/webpack) — for correct module output (ESM/CJS/dual + types), code splitting, tree-shaking, sourcemaps, env injection, and fast incremental builds.
|
|
4
|
+
when_to_use: Setting up or fixing how an app or library compiles and bundles — wrong module format, broken tree-shaking, missing/incorrect types, slow builds, tsconfig errors. Distinct from dockerfile-optimize (container images) and optimize-core-web-vitals (browser runtime metrics).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when the problem is **how source compiles and emits**, not how it runs in a browser or container:
|
|
10
|
+
|
|
11
|
+
- "Set up the build for this app/library" (pick bundler, tsconfig, output format)
|
|
12
|
+
- "My library ships ESM but breaks in a CJS `require()`" (or vice versa) — dual-package output
|
|
13
|
+
- "Consumers get `Could not find a declaration file`" — missing/mislocated `.d.ts`
|
|
14
|
+
- "Tree-shaking isn't dropping unused exports" — dead code in the bundle
|
|
15
|
+
- "`tsc`/`vite build` is slow" — switch transform to esbuild/swc, add a persistent cache
|
|
16
|
+
- "`define`/`import.meta.env` isn't replacing my env var" or a secret leaked into the client bundle
|
|
17
|
+
- tsconfig errors: `module`/`moduleResolution` mismatch, `"x.js" has no exported member`, paths not resolving
|
|
18
|
+
|
|
19
|
+
NOT this skill:
|
|
20
|
+
- Shrinking the runtime container image, multi-stage Docker layers → dockerfile-optimize
|
|
21
|
+
- LCP/INP/CLS, lazy-loading images, render-blocking JS in the browser → optimize-core-web-vitals
|
|
22
|
+
- Cross-package build orchestration, workspace topo build order, Turbo/Nx pipelines → setup-monorepo-tooling
|
|
23
|
+
- `npm publish`, `files`/`publishConfig`, provenance, version bump → publish-package-registry
|
|
24
|
+
- ESLint/Prettier/pre-commit wiring → setup-lint-format-precommit
|
|
25
|
+
- Pinning the Node/pnpm/tsc *versions* themselves (engines, `.nvmrc`, Volta) → pin-toolchain-versions
|
|
26
|
+
|
|
27
|
+
## Steps
|
|
28
|
+
|
|
29
|
+
1. **Pick the bundler by build target — do not default to webpack.**
|
|
30
|
+
|
|
31
|
+
| Target | Use | Why |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| **App** (SPA/SSR, has an entry HTML or framework) | **Vite** | Rollup-based prod build, esbuild dev, HMR, code-splitting out of the box |
|
|
34
|
+
| **Library** (published to npm, consumers bundle it) | **tsup** (esbuild) or **Rollup** | dual ESM+CJS + `.d.ts` in one config; Rollup when you need fine-grained chunking |
|
|
35
|
+
| **Node tool / CLI / serverless fn** (single self-run entry) | **esbuild** | fastest, bundle deps in, `--platform=node`, no chunk graph needed |
|
|
36
|
+
| Legacy app needing module federation / exotic loaders | webpack | only when a Vite/Rollup plugin doesn't exist |
|
|
37
|
+
|
|
38
|
+
Default: **app → Vite, library → tsup, node-tool → esbuild.** One tool emits JS; **`tsc` emits types** (or `tsup --dts` / `vite-plugin-dts` wraps it). Never run `tsc` as the bundler for shipping code — it doesn't bundle, tree-shake, or split.
|
|
39
|
+
|
|
40
|
+
2. **Set the tsconfig essentials — `moduleResolution` is the #1 footgun.** Pick the resolution mode by who resolves modules:
|
|
41
|
+
|
|
42
|
+
| Scenario | `module` | `moduleResolution` |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| Bundler handles resolution (Vite/tsup/esbuild) | `ESNext` (or `Preserve`) | `bundler` |
|
|
45
|
+
| Node runs the output directly (Node ESM/CJS) | `NodeNext` | `nodenext` |
|
|
46
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
// tsconfig.json — app/library baseline
|
|
49
|
+
{
|
|
50
|
+
"compilerOptions": {
|
|
51
|
+
"target": "ES2022", // match your lowest runtime; don't ship ES5 needlessly
|
|
52
|
+
"lib": ["ES2022", "DOM"], // drop "DOM" for node-only code
|
|
53
|
+
"module": "ESNext",
|
|
54
|
+
"moduleResolution": "bundler",
|
|
55
|
+
"strict": true,
|
|
56
|
+
"skipLibCheck": true,
|
|
57
|
+
"esModuleInterop": true,
|
|
58
|
+
"isolatedModules": true, // required: esbuild/swc compile file-by-file
|
|
59
|
+
"verbatimModuleSyntax": true,// makes `import type` explicit — kills accidental value imports
|
|
60
|
+
"declaration": true, // emit .d.ts (libraries)
|
|
61
|
+
"declarationMap": true, // go-to-definition into your source
|
|
62
|
+
"sourceMap": true,
|
|
63
|
+
"outDir": "dist",
|
|
64
|
+
"paths": { "@/*": ["./src/*"] }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
`paths` are a **type-level alias only** — the bundler must be told too (Vite `resolve.alias`, tsup/esbuild via `vite-tsconfig-paths`/`esbuild` alias, or `tsconfig-paths`). tsc does not rewrite them in emitted JS.
|
|
69
|
+
|
|
70
|
+
3. **For a library, emit dual ESM+CJS with a correct `exports` map — the `exports` map is the contract, file extensions are the proof.** tsup config:
|
|
71
|
+
```ts
|
|
72
|
+
// tsup.config.ts
|
|
73
|
+
import { defineConfig } from "tsup";
|
|
74
|
+
export default defineConfig({
|
|
75
|
+
entry: ["src/index.ts"],
|
|
76
|
+
format: ["esm", "cjs"], // → index.js (esm) + index.cjs
|
|
77
|
+
dts: true, // → index.d.ts (+ .d.cts for cjs types)
|
|
78
|
+
sourcemap: true,
|
|
79
|
+
treeshake: true,
|
|
80
|
+
clean: true,
|
|
81
|
+
target: "node18",
|
|
82
|
+
external: [/^node:/], // never bundle node builtins
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
```jsonc
|
|
86
|
+
// package.json — types condition MUST come first in each block
|
|
87
|
+
{
|
|
88
|
+
"type": "module",
|
|
89
|
+
"exports": {
|
|
90
|
+
".": {
|
|
91
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
92
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
93
|
+
},
|
|
94
|
+
"./package.json": "./package.json"
|
|
95
|
+
},
|
|
96
|
+
"main": "./dist/index.cjs", // legacy fallback for old resolvers
|
|
97
|
+
"module": "./dist/index.js",
|
|
98
|
+
"types": "./dist/index.d.ts",
|
|
99
|
+
"sideEffects": false,
|
|
100
|
+
"files": ["dist"]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
Keep `peerDependencies` (react, etc.) in `external` so you don't bundle two copies into the consumer.
|
|
104
|
+
|
|
105
|
+
4. **App: split code with dynamic `import()`, then control chunks deliberately.** Route-level `const Page = lazy(() => import('./Page'))` and `import('heavy-lib')` create async chunks automatically. Pull stable vendor deps into their own long-cached chunk:
|
|
106
|
+
```ts
|
|
107
|
+
// vite.config.ts
|
|
108
|
+
build: {
|
|
109
|
+
sourcemap: true,
|
|
110
|
+
rollupOptions: {
|
|
111
|
+
output: {
|
|
112
|
+
manualChunks: { vendor: ["react", "react-dom"] }, // or a function for finer control
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
chunkSizeWarningLimit: 500,
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
Don't over-split (HTTP/2 helps, but hundreds of tiny chunks add request + parse overhead). Split on real route/feature boundaries, not per-file.
|
|
119
|
+
|
|
120
|
+
5. **Enable tree-shaking — it only works on static ESM.** Author with `import`/`export` (no `require`, no `module.exports`); CJS interop defeats it. Mark the package `"sideEffects": false` (or list the few files with real side effects, e.g. `["**/*.css", "./src/polyfill.ts"]`) so the bundler may drop unused modules. Annotate top-level calls that look impure but aren't with `/*#__PURE__*/`:
|
|
121
|
+
```ts
|
|
122
|
+
export const icon = /*#__PURE__*/ createIcon(path); // droppable if `icon` is unused
|
|
123
|
+
```
|
|
124
|
+
A `"sideEffects": false` lie (a module that *does* mutate global state on import) causes silently-missing behavior — list those files.
|
|
125
|
+
|
|
126
|
+
6. **Inject env at build time via `define`/`import.meta.env` — never bake a secret.** Static replacement only:
|
|
127
|
+
```ts
|
|
128
|
+
// vite: only VITE_* are exposed to client; access via import.meta.env.VITE_API_URL
|
|
129
|
+
// esbuild/tsup: define: { "process.env.NODE_ENV": JSON.stringify("production") }
|
|
130
|
+
```
|
|
131
|
+
**Anything bundled for the browser is public.** Put API keys/DB URLs behind a server route or read them at runtime on the server (`process.env`) — a `define`'d secret is grep-able in `dist/`. Gate dev-only code behind `if (import.meta.env.DEV)` / `process.env.NODE_ENV !== "production"` so it tree-shakes out of prod.
|
|
132
|
+
|
|
133
|
+
7. **Always emit sourcemaps; choose by environment.** `sourcemap: true` (full, external `.map`) for libraries and CI artifacts. For a public web app, ship `hidden` sourcemaps (uploaded to your error tracker, not referenced in the bundle) so stack traces de-minify without exposing source to every visitor. Never `eval`/inline sourcemaps in production.
|
|
134
|
+
|
|
135
|
+
8. **Make builds fast and incremental.** Use an esbuild/swc transform (Vite and tsup already do) instead of `ts-loader`/`babel` for the JS transform — 10–100× faster. Keep type-checking **out of the bundle path**: run `tsc --noEmit` (or `vite build` + a parallel `tsc -b --watch`) so a type error doesn't block fast iteration but still gates CI. Turn on the persistent cache (Vite caches in `node_modules/.vite`; for `tsc -b` use `incremental: true` + `tsBuildInfoFile`). Add `--metafile` (esbuild) / `rollup-plugin-visualizer` to find what's bloating the bundle.
|
|
136
|
+
|
|
137
|
+
9. **Verify the output shape before declaring done** (see Verify) — `publint` + `@arethetypeswrong/cli` catch the dual-package and types-resolution bugs that don't surface until a consumer installs you.
|
|
138
|
+
|
|
139
|
+
## Common Errors
|
|
140
|
+
|
|
141
|
+
- **`moduleResolution: node` (classic) with modern packages.** Fails to resolve `exports`-map-only packages. Use `bundler` (bundler resolves) or `nodenext` (Node resolves) — never the legacy `node`/`node10`.
|
|
142
|
+
- **`types` condition placed last in the `exports` map.** TS reads conditions top-down and takes the first match; if `import`/`require` come before `types`, the consumer gets "no declaration file." `types` must be the **first** key in each condition block.
|
|
143
|
+
- **`.cjs` file emitting `export {}` (or `.mjs` with `require`).** The `exports` map points at the wrong file per condition, or `"type": "module"` mismatches the extension. ESM → `.js`/`.mjs`, CJS → `.cjs`. Verify with `node -e "require('your-pkg')"` and a separate `import`.
|
|
144
|
+
- **`"sideEffects": false` on a package that has side effects.** Tree-shaking drops a polyfill/CSS/registration import → feature silently missing in prod only. List the real side-effect files instead of a blanket `false`.
|
|
145
|
+
- **Secret in a `VITE_`/`define`d var.** It's inlined into client JS and shipped to every browser. Only public values get `VITE_`/`NEXT_PUBLIC_`; secrets stay server-side at runtime.
|
|
146
|
+
- **`paths` alias resolves in the editor but `Cannot find module '@/x'` at build.** tsc/`paths` is type-only; the bundler needs its own alias (`vite-tsconfig-paths`, `resolve.alias`, or `tsconfig-paths`). Configure both.
|
|
147
|
+
- **`isolatedModules` errors on `export { Foo }` where `Foo` is a type.** esbuild/swc compile each file alone and can't tell types from values. Use `export type { Foo }` / `import type` (enforced by `verbatimModuleSyntax`).
|
|
148
|
+
- **Bundling `peerDependencies` (react, etc.) into a library.** Consumer gets two React copies → "invalid hook call." Mark peers `external`.
|
|
149
|
+
- **Running `tsc` as the production bundler.** It transpiles per-file but doesn't bundle, tree-shake, split, or rewrite `paths` — output has unresolved aliases and no chunking. Use a real bundler for JS; `tsc` only for `.d.ts`.
|
|
150
|
+
- **No sourcemaps in prod (or inline/eval maps).** Minified stack traces are useless; inline maps bloat the bundle and leak source. Emit external (`hidden` for public web), upload to the error tracker.
|
|
151
|
+
- **Targeting `ES5`/old `lib` by reflex.** Forces heavy down-leveling and polyfills for runtimes that support modern JS. Set `target`/`lib` to your *actual* lowest runtime.
|
|
152
|
+
|
|
153
|
+
## Verify
|
|
154
|
+
|
|
155
|
+
1. **Clean build succeeds:** `rm -rf dist && <build>` exits 0 and `dist/` contains the expected entry files (`.js`, `.cjs`, `.d.ts`, `.map`).
|
|
156
|
+
2. **Types resolve both ways (library):** `npx @arethetypeswrong/cli --pack` reports no ❌ — no "masquerading as CJS/ESM", no missing types per condition.
|
|
157
|
+
3. **Package shape is publishable:** `npx publint` is clean — `exports`, `main`/`module`/`types`, and file extensions all consistent.
|
|
158
|
+
4. **Dual import actually loads:** in a scratch dir, `node -e "import('your-pkg').then(m=>console.log(Object.keys(m)))"` **and** `node -e "console.log(Object.keys(require('your-pkg')))"` both print the API — no `ERR_REQUIRE_ESM` / `ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
159
|
+
5. **Type-check passes independently:** `tsc --noEmit` exits 0 (proves the build path didn't skip a type error).
|
|
160
|
+
6. **Tree-shaking works:** bundle a fixture importing one named export; the visualizer/`--metafile` shows unused siblings absent from output. Bundle size drops when an unused heavy import is removed.
|
|
161
|
+
7. **Code-splitting present (app):** prod build emits ≥1 async chunk per lazy route, and the vendor chunk is separate from app code (check `dist/assets/`).
|
|
162
|
+
8. **No secret in the bundle:** `grep -r "<a known secret substring>" dist/` returns nothing; only intended public `VITE_*`/`NEXT_PUBLIC_*` values appear.
|
|
163
|
+
9. **Sourcemaps map back:** open a built file's `.map` or trigger an error — stack trace points to original `src/` lines, not minified columns.
|
|
164
|
+
10. **Incremental rebuild is fast:** a one-line edit triggers a sub-second rebuild (warm cache), not a full cold compile.
|
|
165
|
+
|
|
166
|
+
Done = clean build emits the correct module formats + types, `attw` and `publint` are clean, both `import()` and `require()` load the API, `tsc --noEmit` passes, tree-shaking and code-splitting are confirmed in the output, no secret leaked into `dist/`, and warm rebuilds are fast.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configure-dns-tls
|
|
3
|
+
description: Configures DNS records and TLS for a service — A/AAAA/CNAME/ALIAS/MX/TXT/CAA, zero-downtime cutovers via pre-lowered TTL, automated ACME/Let's Encrypt/cert-manager issuance and auto-renewal, and TLS 1.2+/1.3-only settings with HSTS, OCSP stapling, and 80→443 redirect — eliminating expired-cert and bad-cutover outages.
|
|
4
|
+
when_to_use: Pointing a domain at a service, enabling HTTPS, automating/rotating certificates (ACME/cert-manager), or migrating DNS. Distinct from configure-reverse-proxy-lb (the proxy/LB that terminates the TLS this issues) and setup-cdn-edge-waf (the CDN/WAF edge in front).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when the task is **names and certificates** — getting a domain to resolve to your service and serving valid HTTPS that renews itself:
|
|
10
|
+
|
|
11
|
+
- "Point `app.example.com` at this load balancer / IP without downtime"
|
|
12
|
+
- "Enable HTTPS / fix the expired-cert outage / stop the cert from ever expiring again"
|
|
13
|
+
- "Automate certs with Let's Encrypt / cert-manager; issue a wildcard"
|
|
14
|
+
- "Migrate DNS to a new provider / cut over to a new origin"
|
|
15
|
+
- "Lock down SPF/DKIM/DMARC, or CAA so only my CA can issue"
|
|
16
|
+
- "Why does SSL Labs give us a B? Harden the TLS config"
|
|
17
|
+
|
|
18
|
+
NOT this skill:
|
|
19
|
+
- Configuring the proxy/LB/Ingress that **terminates** TLS, virtual hosts, upstream pools, timeouts → configure-reverse-proxy-lb
|
|
20
|
+
- The CDN/edge, WAF rules, edge caching, or DDoS layer in front of origin → setup-cdn-edge-waf
|
|
21
|
+
- Application-layer auth/authz, token scopes, RBAC → design-authorization-model
|
|
22
|
+
- Tamper-evident security event logs (incl. cert-rotation events) → build-audit-logging
|
|
23
|
+
|
|
24
|
+
This skill owns the **record values, the cutover choreography, certificate lifecycle, and the TLS handshake policy**. It hands the terminated connection to the proxy.
|
|
25
|
+
|
|
26
|
+
## Steps
|
|
27
|
+
|
|
28
|
+
1. **Pick the record type by what you're pointing at — do not CNAME the apex.**
|
|
29
|
+
|
|
30
|
+
| Need | Record | Notes |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Name → IPv4 | `A` | Bare IP only |
|
|
33
|
+
| Name → IPv6 | `AAAA` | Add alongside A; serve dual-stack |
|
|
34
|
+
| Subdomain → another hostname | `CNAME` | e.g. `www → app.example.com`; cannot coexist with other records on that name |
|
|
35
|
+
| **Apex** (`example.com`) → hostname | `ALIAS`/`ANAME`/flattened-CNAME | Apex can't be a real CNAME (breaks SOA/NS/MX). Use the provider's ALIAS (Route 53 alias, Cloudflare CNAME-flattening, etc.) |
|
|
36
|
+
| Mail | `MX` | Priority + target; target must be an A/AAAA, never a CNAME |
|
|
37
|
+
| SPF/DKIM/DMARC/verification | `TXT` | One SPF per domain; DMARC at `_dmarc`; DKIM at `<sel>._domainkey` |
|
|
38
|
+
| Who may issue certs | `CAA` | `0 issue "letsencrypt.org"` + `0 issuewild "letsencrypt.org"` |
|
|
39
|
+
|
|
40
|
+
Set CAA **before** first ACME issuance, or issuance fails with `CAA record prevents issuance`. Example:
|
|
41
|
+
```
|
|
42
|
+
example.com. CAA 0 issue "letsencrypt.org"
|
|
43
|
+
example.com. CAA 0 issuewild "letsencrypt.org"
|
|
44
|
+
example.com. CAA 0 iodef "mailto:security@example.com"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. **Zero-downtime cutover: lower the TTL BEFORE the change — this is the whole trick.** Resolvers cache the old answer for up to its TTL; if you cut over while TTL is 3600, clients hit the dead origin for an hour.
|
|
48
|
+
1. Drop the record's TTL to `60` (or `30`). **Wait out the *old* TTL** (e.g. wait the full prior 3600s) so every cache holds the short TTL.
|
|
49
|
+
2. Run both origins in parallel (old + new healthy) during the switch — never tear down old first.
|
|
50
|
+
3. Change the record value to the new target.
|
|
51
|
+
4. Verify the new answer is served (step in Verify) and the new origin takes real traffic.
|
|
52
|
+
5. Only after traffic has fully drained from the old origin (watch its access logs go quiet for > one TTL), decommission it and **raise TTL back** to 3600+ to cut query volume/cost.
|
|
53
|
+
|
|
54
|
+
3. **Automate certificates — manual renewal is a guaranteed future outage.** Use ACME (Let's Encrypt / ZeroSSL). Never click-issue a 1-year cert you have to remember to renew; LE is 90-day by design to *force* automation.
|
|
55
|
+
- **VM / bare proxy:** `certbot` with a renewal timer, or the proxy's built-in ACME (Caddy auto-HTTPS, Traefik resolver, nginx + `acme.sh`).
|
|
56
|
+
- **Kubernetes:** **cert-manager** — a `ClusterIssuer` + `Certificate` (or Ingress annotation) reconciles renewal automatically; renews at ~⅔ of lifetime.
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
# cert-manager: DNS-01 wildcard via Cloudflare
|
|
60
|
+
apiVersion: cert-manager.io/v1
|
|
61
|
+
kind: ClusterIssuer
|
|
62
|
+
metadata: { name: letsencrypt-prod }
|
|
63
|
+
spec:
|
|
64
|
+
acme:
|
|
65
|
+
server: https://acme-v02.api.letsencrypt.org/directory
|
|
66
|
+
email: ops@example.com
|
|
67
|
+
privateKeySecretRef: { name: letsencrypt-prod-key }
|
|
68
|
+
solvers:
|
|
69
|
+
- dns01:
|
|
70
|
+
cloudflare:
|
|
71
|
+
apiTokenSecretRef: { name: cloudflare-token, key: api-token }
|
|
72
|
+
---
|
|
73
|
+
apiVersion: cert-manager.io/v1
|
|
74
|
+
kind: Certificate
|
|
75
|
+
metadata: { name: example-tls, namespace: web }
|
|
76
|
+
spec:
|
|
77
|
+
secretName: example-tls # Ingress references this
|
|
78
|
+
issuerRef: { name: letsencrypt-prod, kind: ClusterIssuer }
|
|
79
|
+
dnsNames: ["example.com", "*.example.com"]
|
|
80
|
+
```
|
|
81
|
+
Iterate against the **staging** ACME server first — set the `ClusterIssuer` `spec.acme.server` to `https://acme-staging-v02.api.letsencrypt.org/directory` (or `certbot --test-cert`) to dodge LE prod rate limits (50 certs / registered-domain / week) while debugging, then flip the server back to prod and re-issue.
|
|
82
|
+
|
|
83
|
+
4. **Choose the ACME challenge and cert shape deliberately.**
|
|
84
|
+
|
|
85
|
+
| Axis | Pick | Why |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| **HTTP-01** | single host, port 80 reachable from internet | simplest; needs `/.well-known/acme-challenge/` served; **cannot** do wildcards |
|
|
88
|
+
| **DNS-01** | wildcards, internal hosts, no inbound 80, or many SANs | proves control via a `_acme-challenge` TXT; needs DNS-provider API creds; works behind a firewall |
|
|
89
|
+
| **Wildcard** `*.example.com` | many dynamic subdomains | DNS-01 only; one cert, but a single shared private key (bigger blast radius) |
|
|
90
|
+
| **SAN / multi-domain** | a known fixed set of names | explicit per-name; rotate one without touching others; preferred when the list is stable |
|
|
91
|
+
|
|
92
|
+
Default: **SAN cert via DNS-01** for anything non-trivial; wildcard only when subdomains are unbounded/dynamic.
|
|
93
|
+
|
|
94
|
+
5. **Set a modern TLS policy at the terminator — TLS 1.2+ only, redirect, HSTS, stapling.** Configure on whatever terminates (see configure-reverse-proxy-lb), but the *policy* is owned here:
|
|
95
|
+
- Protocols: **TLS 1.3 + TLS 1.2 only**. Disable TLS 1.0/1.1 and SSLv3 entirely.
|
|
96
|
+
- Ciphers: TLS 1.3 defaults; for 1.2 use forward-secret AEAD suites (ECDHE + AES-GCM/CHACHA20), no CBC/RC4/3DES.
|
|
97
|
+
- **Redirect 80→443** with `301`, then serve everything over HTTPS.
|
|
98
|
+
- **HSTS** on HTTPS responses: `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` — but only add `preload`/`includeSubDomains` once *every* subdomain is HTTPS (it's hard to undo). Roll out short → long → preload.
|
|
99
|
+
- **OCSP stapling** on (`ssl_stapling on;` in nginx) so clients don't round-trip the CA.
|
|
100
|
+
- Serve the **full chain** (leaf + intermediates), not just the leaf — the #2 cause of "works in my browser, fails in `curl`/old Android".
|
|
101
|
+
|
|
102
|
+
```nginx
|
|
103
|
+
server {
|
|
104
|
+
listen 443 ssl http2;
|
|
105
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
106
|
+
ssl_prefer_server_ciphers off;
|
|
107
|
+
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # full chain
|
|
108
|
+
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
|
|
109
|
+
ssl_stapling on; ssl_stapling_verify on;
|
|
110
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
111
|
+
}
|
|
112
|
+
server { listen 80; server_name example.com; return 301 https://$host$request_uri; }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
6. **Prove auto-renew works before you trust it.** A cert that issues fine but never renews is a 90-day time bomb. Force a dry-run/staging renewal now (step in Verify) so you discover broken DNS creds or a missing port today, not at 2am on day 89.
|
|
116
|
+
|
|
117
|
+
## Common Errors
|
|
118
|
+
|
|
119
|
+
- **CNAME on the apex.** Breaks NS/SOA/MX co-existence; many resolvers reject it. Use ALIAS/ANAME/CNAME-flattening for `example.com`.
|
|
120
|
+
- **Cutover without pre-lowering TTL.** You switch the record but caches serve the dead origin for the full old TTL (often an hour). Lower TTL and wait out the *old* TTL first.
|
|
121
|
+
- **Raising TTL or killing the old origin too early.** Do it only after old-origin logs go quiet for > one TTL; otherwise stragglers 502.
|
|
122
|
+
- **Missing/forbidding CAA.** No CAA = any CA may issue (security gap); a CAA that omits your CA = ACME fails with `CAA record prevents issuance`. Add the issuing CA explicitly, including `issuewild` for wildcards.
|
|
123
|
+
- **HTTP-01 for a wildcard.** Impossible — wildcards require DNS-01. Switch the solver.
|
|
124
|
+
- **Manual cert renewal "we'll remember."** You won't. The outage is scheduled for expiry day. Automate or it will lapse.
|
|
125
|
+
- **Serving only the leaf cert.** Browsers cache intermediates and "work"; `curl`, Java, old Android, and API clients fail chain validation. Always deploy `fullchain.pem`.
|
|
126
|
+
- **Burning LE rate limits while debugging.** Iterate against `acme-staging-v02` (or `certbot --test-cert`); only hit prod once issuance succeeds in staging.
|
|
127
|
+
- **`includeSubDomains`/`preload` HSTS before all subdomains are HTTPS.** Any plain-HTTP subdomain becomes unreachable, and `preload` is baked into browsers for months. Roll HSTS out short → long → preload.
|
|
128
|
+
- **DNS-01 with under-scoped API creds.** The token can't write `_acme-challenge` TXT, so renewal silently fails. Scope the token to DNS-edit on that zone and test it.
|
|
129
|
+
- **Mixed content after enabling HTTPS.** Page loads over HTTPS but pulls `http://` assets → browser blocks them. Rewrite asset URLs to `https://` or protocol-relative; verify console is clean.
|
|
130
|
+
- **Clock skew on the TLS host.** A wrong system clock makes a valid cert read as not-yet-valid/expired. Run NTP.
|
|
131
|
+
|
|
132
|
+
## Verify
|
|
133
|
+
|
|
134
|
+
1. **Records resolve correctly:** `dig +short A app.example.com` (and `AAAA`) returns the new target; `dig CAA example.com` shows your CA; `dig TXT _dmarc.example.com` shows the DMARC policy. Query an external resolver (`dig @1.1.1.1 …`) too, not just the local cache.
|
|
135
|
+
2. **TTL was actually lowered before cutover:** `dig app.example.com | grep -E '^app'` shows the short TTL *before* you change the value; confirm the answer flips after, and that it propagated (`dig @8.8.8.8` and `@1.1.1.1` agree).
|
|
136
|
+
3. **Full chain + protocol scan:** `echo | openssl s_client -connect example.com:443 -servername example.com -showcerts` shows leaf **and** intermediate(s), `Verify return code: 0 (ok)`. `testssl.sh example.com` (or SSL Labs) reports TLS 1.2/1.3 only, no TLS 1.0/1.1, HSTS present, OCSP stapled — target grade **A/A+**.
|
|
137
|
+
4. **Redirect + HSTS:** `curl -sI http://example.com` → `301` to `https://`; `curl -sI https://example.com | grep -i strict-transport` shows the HSTS header.
|
|
138
|
+
5. **No mixed content:** load the page, browser console shows zero "Mixed Content" / blocked-asset warnings; all subresources are `https://`.
|
|
139
|
+
6. **Expiry & auto-renew proven:** `echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate` shows a future date; then force a **staging** renewal — `certbot renew --dry-run` (VM) or, for k8s, point the issuer at `acme-staging-v02`, run `cmctl renew example-tls`, and watch `cmctl status certificate example-tls` go Ready — and confirm a fresh cert issues without manual steps.
|
|
140
|
+
7. **Mail auth (if MX set):** SPF/DKIM/DMARC TXT records validate (e.g. an external mail-tester) — no `softfail`/missing-DKIM.
|
|
141
|
+
|
|
142
|
+
Done = every name resolves to the new target on external resolvers, HTTPS serves the **full chain** over **TLS 1.2/1.3 only** with HSTS + stapling + 80→443 redirect and no mixed content (SSL Labs/testssl ≥ A), CAA locks issuance to your CA, and a staging force-renew has **proven** auto-renewal works before any cert nears expiry.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: configure-reverse-proxy-lb
|
|
3
|
+
description: Configures a reverse proxy / load balancer (nginx, Envoy, Caddy, HAProxy) in front of services — upstream pools, active/passive health checks, per-hop connect/read/send timeouts, TLS termination vs passthrough, idempotent-only retries with circuit breaking, sticky sessions, and zero-drop graceful reloads.
|
|
4
|
+
when_to_use: Putting a proxy/LB in front of services, fixing 502/504s, balancing across instances, or routing by host/path. Distinct from configure-dns-tls (DNS records + cert issuance), setup-cdn-edge-waf (the CDN/WAF edge), rate-limiting (app-level request caps), and k8s-manifest-review (in-cluster Service/Ingress objects).
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Reach for this skill when the request is about **the proxy/LB layer between clients and your services**:
|
|
10
|
+
|
|
11
|
+
- "Put nginx/Envoy/Caddy/HAProxy in front of these app instances"
|
|
12
|
+
- "We're getting random 502/504s — fix the timeouts"
|
|
13
|
+
- "Balance traffic across N backends and drop a dead one automatically"
|
|
14
|
+
- "Route by `Host:` / path prefix to different upstreams"
|
|
15
|
+
- "Terminate TLS at the proxy" / "pass TLS straight through to the backend"
|
|
16
|
+
- "Config reload kills in-flight requests — make it zero-drop"
|
|
17
|
+
|
|
18
|
+
NOT this skill:
|
|
19
|
+
- Creating DNS records or issuing/renewing the cert itself → configure-dns-tls
|
|
20
|
+
- The CDN/edge tier, bot rules, or WAF rulesets → setup-cdn-edge-waf
|
|
21
|
+
- Per-user/per-key request caps and 429s at the app → rate-limiting
|
|
22
|
+
- Kubernetes `Service`/`Ingress`/`Gateway` objects in-cluster → k8s-manifest-review
|
|
23
|
+
|
|
24
|
+
## Steps
|
|
25
|
+
|
|
26
|
+
1. **Pick the proxy by requirement — default to nginx.**
|
|
27
|
+
|
|
28
|
+
| Proxy | Pick when | Watch out |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| **nginx** | General L7 in front of HTTP/HTTPS apps — the default | Active health checks need nginx **Plus**; OSS only does passive `max_fails` |
|
|
31
|
+
| **Envoy** | Dynamic config via xDS, gRPC/HTTP2, fine-grained circuit breaking, outlier detection | Steep config; run with a control plane (Istio/Contour/Gloo) for anything large |
|
|
32
|
+
| **Caddy** | You want automatic TLS (ACME) with near-zero config | Less knob-level control over upstreams/retries |
|
|
33
|
+
| **HAProxy** | Heavy L4 (TCP) LB, max throughput, advanced balancing/observability | L7 ergonomics weaker than nginx for content routing |
|
|
34
|
+
|
|
35
|
+
For a typical web service: **nginx terminating TLS, round-robin or least-conn upstream, passive health checks**. Reach for Envoy only when you genuinely need dynamic upstreams or per-endpoint outlier ejection.
|
|
36
|
+
|
|
37
|
+
2. **Define the upstream pool + algorithm — least-conn is the safer default for mixed latency.**
|
|
38
|
+
|
|
39
|
+
```nginx
|
|
40
|
+
upstream app {
|
|
41
|
+
least_conn; # round-robin is fine for uniform requests; least_conn for variable latency
|
|
42
|
+
server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
|
|
43
|
+
server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
|
|
44
|
+
server 10.0.1.13:8080 max_fails=3 fail_timeout=10s backup; # only when primaries are down
|
|
45
|
+
keepalive 64; # REUSE upstream conns — without this every request does a fresh TCP+TLS handshake
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **round-robin** (default): uniform, cheap requests.
|
|
50
|
+
- **least-conn**: requests with variable duration — avoids piling onto a slow node.
|
|
51
|
+
- **consistent-hash** (`hash $arg_key consistent;` / Envoy ring-hash): only when a key must stick to a backend (cache affinity, sharding). Plain `ip_hash` rebalances badly when a node leaves; use `consistent` so a single ejection doesn't reshuffle every key.
|
|
52
|
+
|
|
53
|
+
3. **Set timeouts at EVERY hop — a proxy timeout shorter than the app is the #1 cause of 502/504.** A 502 = backend refused/reset the connection; a 504 = backend accepted but didn't answer before `proxy_read_timeout`. The proxy's read timeout must be **longer** than the slowest legitimate backend response, and the backend's own keepalive must be **longer** than the proxy's so the proxy never reuses a socket the backend just closed (classic race → sporadic 502).
|
|
54
|
+
|
|
55
|
+
```nginx
|
|
56
|
+
location / {
|
|
57
|
+
proxy_pass http://app;
|
|
58
|
+
proxy_http_version 1.1;
|
|
59
|
+
proxy_set_header Connection ""; # required so keepalive to upstream actually works
|
|
60
|
+
|
|
61
|
+
proxy_connect_timeout 2s; # TCP connect to backend — short; a backend that won't accept is dead
|
|
62
|
+
proxy_send_timeout 30s; # writing the request body to backend
|
|
63
|
+
proxy_read_timeout 60s; # waiting for the backend's response — MUST exceed slowest real response
|
|
64
|
+
}
|
|
65
|
+
# And: backend keepalive_timeout (e.g. 75s) > nginx upstream idle reuse window, to avoid the reuse-after-close 502.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Envoy: set `connect_timeout` on the cluster and `route.timeout` per route; default route timeout is 15s and silently truncates long requests — set it deliberately.
|
|
69
|
+
|
|
70
|
+
4. **Add health checks — passive at minimum, active if your proxy supports it.** Passive ejection (`max_fails`/`fail_timeout`, Envoy outlier detection) reacts only to *real* request failures, so a freshly-booted-but-broken node still gets traffic until it fails N live requests. Active checks (nginx Plus `health_check`, HAProxy `option httpchk`, Envoy `health_checks`) probe a `/healthz` endpoint and eject before user traffic hits it.
|
|
71
|
+
|
|
72
|
+
- Health endpoint must check **dependencies** (DB, cache reachable), not just "process is up" — otherwise you keep a node that 500s on every real request.
|
|
73
|
+
- Set an explicit `unhealthy`→`healthy` hysteresis (e.g. eject after 3 fails, re-add after 2 passes) so a flapping node doesn't oscillate in and out of rotation.
|
|
74
|
+
|
|
75
|
+
5. **TLS: terminate at the proxy unless the backend legally must see the cert.** Terminate (decrypt at proxy, plaintext or re-encrypt to backend) for HTTP routing, header inspection, and central cert management — the common case. **Passthrough** (L4 `stream`/SNI routing, proxy never decrypts) only for end-to-end encryption mandates or non-HTTP TLS. When terminating, forward the original scheme/IP so the app builds correct URLs and logs the real client:
|
|
76
|
+
|
|
77
|
+
```nginx
|
|
78
|
+
proxy_set_header Host $host;
|
|
79
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
80
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
81
|
+
proxy_set_header X-Forwarded-Proto $scheme; # app uses this to know the request was HTTPS
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Pin `ssl_protocols TLSv1.2 TLSv1.3;` and a modern cipher suite; redirect `:80` → `:443`.
|
|
85
|
+
|
|
86
|
+
6. **Retry idempotent requests ONLY, with circuit breaking.** Auto-retrying a `POST`/`PATCH` that timed out can double-charge a card or double-write. Restrict retries to safe methods + connect/early failures, cap attempts, and stop retrying once the backend is clearly down.
|
|
87
|
+
|
|
88
|
+
```nginx
|
|
89
|
+
proxy_next_upstream error timeout http_502 http_503; # NOT non_idempotent — never blindly retry POST
|
|
90
|
+
proxy_next_upstream_tries 2;
|
|
91
|
+
proxy_next_upstream_timeout 10s;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Envoy: `retry_policy` with `retry_on: connect-failure,refused-stream,unavailable`, `num_retries: 2`, plus `retry_back_off`. Add **circuit breaking** (Envoy `circuit_breakers` max connections/pending/retries, or outlier detection ejecting a 5xx-storming host) so retries don't amplify load against a struggling backend into a full meltdown.
|
|
95
|
+
|
|
96
|
+
7. **Sticky sessions only when state truly demands it.** Cookie/affinity routing (`sticky cookie`, Envoy hash policy) pins a client to one backend — necessary for in-memory session state, fatal for even load balancing and graceful drain (a drained node's clients all break). **First fix the state**: move sessions to Redis/JWT so any backend serves any user, then drop stickiness. Only keep it for unavoidable backend-local state, and pair it with consistent hashing so losing one node reshuffles minimally.
|
|
97
|
+
|
|
98
|
+
8. **Make reloads zero-drop (graceful drain).** A naive restart cuts in-flight connections → user-visible 5xx during every deploy.
|
|
99
|
+
- **nginx:** `nginx -t && nginx -s reload` — the master spins up new workers on the new config and lets old workers finish in-flight requests before exiting. Never `kill -9` / hard restart for a config change.
|
|
100
|
+
- **HAProxy:** run with `-sf $(cat pid)` (seamless finish) or the master-worker socket reload.
|
|
101
|
+
- **Envoy:** hot restart / xDS push drains the old listener.
|
|
102
|
+
- For removing a **backend**: first mark it `down`/drain in the pool and reload so the proxy stops sending *new* requests, wait for in-flight to finish, then stop the backend. Tie the backend's shutdown to its readiness probe (fail `/healthz` → proxy ejects → then SIGTERM) so the LB drains it before it dies.
|
|
103
|
+
|
|
104
|
+
## Common Errors
|
|
105
|
+
|
|
106
|
+
- **`proxy_read_timeout` shorter than the slowest real response.** Long uploads/reports hit a **504** even though the backend is healthy. Set the read timeout above the legitimate p99, and only then chase a slow endpoint separately.
|
|
107
|
+
- **Backend keepalive shorter than the proxy's upstream idle window.** Backend closes an idle socket the proxy then reuses → sporadic **502** under no real load. Make backend `keepalive_timeout` longer than the proxy's, and set `proxy_http_version 1.1` + `Connection ""`.
|
|
108
|
+
- **No `keepalive` in the upstream block.** Every request does a fresh TCP (and TLS) handshake to the backend — latency and CPU explode under load. Add `keepalive N` and clear the `Connection` header.
|
|
109
|
+
- **Retrying non-idempotent requests.** `proxy_next_upstream` including `non_idempotent` (or an Envoy `retry_on` that catches POSTs) silently double-executes writes on a timeout → duplicate charges/orders. Retry safe methods + connect failures only.
|
|
110
|
+
- **Health check that only pings the port / returns 200 unconditionally.** A node with a dead DB stays in rotation and 500s every request. Probe real dependencies in `/healthz`.
|
|
111
|
+
- **`ip_hash` / non-consistent hashing for affinity.** Removing or adding one node reshuffles *every* client to a new backend, blowing caches and sessions. Use `consistent` hashing.
|
|
112
|
+
- **Trusting client-supplied `X-Forwarded-For`/`X-Forwarded-Proto`.** The app sees spoofed client IPs or thinks plaintext is HTTPS. Reset these headers at the trust boundary (`proxy_set_header ... $remote_addr`/`$scheme`); never pass the raw inbound value through.
|
|
113
|
+
- **Hard restart on config change.** `systemctl restart nginx` / `kill -9` drops in-flight connections every deploy. Use `reload` / `-sf` graceful paths.
|
|
114
|
+
- **Stopping a backend before draining it.** Killing an instance while the LB still routes to it = a burst of 5xx for its in-flight requests. Drain (fail readiness → eject) first, then SIGTERM.
|
|
115
|
+
- **Default Envoy 15s route timeout left implicit.** Long-running requests get cut at 15s with no obvious cause. Set `route.timeout` explicitly per route.
|
|
116
|
+
- **Single proxy = single point of failure.** One LB box and the whole service is down when it dies or reloads badly. Run ≥2 behind a VIP/anycast/keepalived or a managed LB.
|
|
117
|
+
|
|
118
|
+
## Verify
|
|
119
|
+
|
|
120
|
+
1. **Config is valid before reload:** `nginx -t` (or `haproxy -c -f`, `envoy --mode validate`, `caddy validate`) returns OK. Never reload an unvalidated config.
|
|
121
|
+
2. **Balancing works:** fire `N` requests (`hey`, `vegeta`, `for i in $(seq 100); do curl -s .../whoami; done`) and confirm responses spread across all backends per the chosen algorithm (e.g. roughly even for round-robin).
|
|
122
|
+
3. **Dead-backend reroute, zero 5xx:** kill one backend mid-load. Traffic must reroute to healthy nodes and the client must see **no 5xx** (passive: a brief blip until `max_fails`; active: none). The killed node returns to rotation after it's healthy again.
|
|
123
|
+
4. **Timeouts behave:** point at a backend that sleeps longer than `proxy_read_timeout` → you get **504** at the configured time, not earlier/later. A backend refusing connections → **502** (not a retry storm).
|
|
124
|
+
5. **Retries are idempotent-only:** a timed-out `GET` retries to a second backend (one served response); a timed-out `POST` does **not** double-execute (assert the write happened exactly once at the backend).
|
|
125
|
+
6. **Zero-drop reload:** run sustained load (`vegeta attack -rate=200 -duration=60s`), trigger a config `reload` mid-run, and confirm **0 connection errors / 0 non-2xx** attributable to the reload in the report.
|
|
126
|
+
7. **TLS + forwarded headers:** `curl -v https://host` negotiates TLS1.2/1.3; the backend logs the real client IP (`X-Real-IP`) and sees `X-Forwarded-Proto: https`; `:80` 301-redirects to `:443`.
|
|
127
|
+
8. **Drain before stop:** mark a backend down, confirm new requests stop hitting it while in-flight ones complete, *then* stop it — no 5xx in the transition.
|
|
128
|
+
|
|
129
|
+
Done = killing a backend reroutes with **zero 5xx**, timeouts produce the right code at the right time, idempotent-only retries never double-write, and a config reload under sustained load drops **zero** in-flight connections — all with a validated config and ≥2 proxies (no single point of failure).
|