@ai-support-agent/cli 0.1.32-beta.1 → 0.1.33-beta.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.
Files changed (62) hide show
  1. package/dist/commands/claude-code-args.d.ts +1 -0
  2. package/dist/commands/claude-code-args.d.ts.map +1 -1
  3. package/dist/commands/claude-code-args.js +3 -0
  4. package/dist/commands/claude-code-args.js.map +1 -1
  5. package/dist/commands/claude-code-runner.d.ts.map +1 -1
  6. package/dist/commands/claude-code-runner.js +2 -1
  7. package/dist/commands/claude-code-runner.js.map +1 -1
  8. package/dist/commands/plugin-dir.d.ts +20 -0
  9. package/dist/commands/plugin-dir.d.ts.map +1 -0
  10. package/dist/commands/plugin-dir.js +71 -0
  11. package/dist/commands/plugin-dir.js.map +1 -0
  12. package/dist/plugin/.claude-plugin/plugin.json +9 -0
  13. package/dist/plugin/LICENSE +26 -0
  14. package/dist/plugin/README.md +86 -0
  15. package/dist/plugin/SYNC.md +113 -0
  16. package/dist/plugin/agents/build-error-resolver.md +159 -0
  17. package/dist/plugin/agents/code-reviewer.md +277 -0
  18. package/dist/plugin/agents/django-reviewer.md +159 -0
  19. package/dist/plugin/agents/infra-reviewer.md +172 -0
  20. package/dist/plugin/agents/investigator.md +75 -0
  21. package/dist/plugin/agents/nextjs-reviewer.md +191 -0
  22. package/dist/plugin/agents/php-reviewer.md +167 -0
  23. package/dist/plugin/agents/planner.md +184 -0
  24. package/dist/plugin/agents/python-reviewer.md +161 -0
  25. package/dist/plugin/agents/react-reviewer.md +150 -0
  26. package/dist/plugin/agents/silent-failure-hunter.md +158 -0
  27. package/dist/plugin/agents/typescript-reviewer.md +179 -0
  28. package/dist/plugin/agents/ui-reviewer.md +203 -0
  29. package/dist/plugin/commands/add-feature.md +301 -0
  30. package/dist/plugin/commands/build-fix.md +47 -0
  31. package/dist/plugin/commands/code-review.md +228 -0
  32. package/dist/plugin/commands/fix-defect.md +393 -0
  33. package/dist/plugin/commands/learn-eval.md +94 -0
  34. package/dist/plugin/commands/learn.md +84 -0
  35. package/dist/plugin/commands/plan.md +211 -0
  36. package/dist/plugin/commands/test-coverage.md +64 -0
  37. package/dist/plugin/commands/update-docs.md +98 -0
  38. package/dist/plugin/hooks/hooks.json +59 -0
  39. package/dist/plugin/hooks/scripts/auto-format.sh +63 -0
  40. package/dist/plugin/hooks/scripts/check-secrets-before-commit.sh +50 -0
  41. package/dist/plugin/hooks/scripts/guard-dangerous-commands.sh +55 -0
  42. package/dist/plugin/hooks/scripts/on-command-resume.sh +112 -0
  43. package/dist/plugin/hooks/scripts/on-command-stop.sh +200 -0
  44. package/dist/plugin/hooks/scripts/protect-sensitive-files.sh +58 -0
  45. package/dist/plugin/rules/common/coding-guidelines.md +73 -0
  46. package/dist/plugin/rules/documentation/api-docs.md +46 -0
  47. package/dist/plugin/rules/documentation/docs-site.md +60 -0
  48. package/dist/plugin/rules/documentation/source-docs.md +89 -0
  49. package/dist/plugin/rules/documentation/test-docs.md +39 -0
  50. package/dist/plugin/rules/logging/logging-rules.md +83 -0
  51. package/dist/plugin/rules/php/coding-rules.md +40 -0
  52. package/dist/plugin/rules/python/coding-rules.md +40 -0
  53. package/dist/plugin/rules/typescript/coding-rules.md +45 -0
  54. package/dist/plugin/skills/api-design/SKILL.md +269 -0
  55. package/dist/plugin/skills/backend-patterns/SKILL.md +312 -0
  56. package/dist/plugin/skills/database-migrations/SKILL.md +323 -0
  57. package/dist/plugin/skills/docker-patterns/SKILL.md +308 -0
  58. package/dist/plugin/skills/docs-site/SKILL.md +341 -0
  59. package/dist/plugin/skills/e2e-testing/SKILL.md +334 -0
  60. package/dist/plugin/skills/frontend-patterns/SKILL.md +318 -0
  61. package/dist/plugin/skills/integration-testing/SKILL.md +273 -0
  62. package/package.json +2 -2
@@ -0,0 +1,323 @@
1
+ ---
2
+ name: database-migrations
3
+ description: A pattern library for running database migrations safely. Covers universal principles such as backward compatibility, the expand/contract pattern, and rollback design; dangerous operations to avoid in PostgreSQL/MySQL; tool-specific notes for Django, Laravel, TypeORM, and Prisma; and lazy migration for DynamoDB. Use this when planning or reviewing schema changes, data migrations, or migration files.
4
+ ---
5
+
6
+ # Database Migrations — Patterns for Safe DB Migrations
7
+
8
+ Migrations against a production database tend to be operations you can't undo once they fail.
9
+ This skill collects the principles, dangerous patterns, and tool-specific pointers needed to
10
+ carry out schema changes and data migrations with zero downtime and zero data loss.
11
+
12
+ ## 1. Universal Principles
13
+
14
+ ### 1.1 Backward compatibility: don't break the old code while it's still running
15
+
16
+ During a deploy there is always a moment where "old code + new schema" or "new code + old schema"
17
+ coexist. With rolling deploys this window can last minutes; even with blue/green deploys there's
18
+ a brief moment of overlap at cutover. A migration must therefore always satisfy both of these:
19
+
20
+ - The new schema must not break the old code (don't drop or rename columns first)
21
+ - The new code must be able to start up against the old schema (don't assume a new column is
22
+ already required)
23
+
24
+ ### 1.2 Expand / Contract (the two-phase deploy)
25
+
26
+ Never make a breaking change in one shot. Always break it down into "add → migrate → remove."
27
+
28
+ ```text
29
+ Phase 1 (Expand) : Add new columns/tables. Old code simply ignores them, so nothing breaks
30
+ Phase 2 (Migrate) : Deploy new code that writes to both, or treats the new side as authoritative.
31
+ Backfill the data
32
+ Phase 3 (Contract) : Once you've confirmed all code reads only from the new side, drop the old
33
+ columns/tables
34
+ ```
35
+
36
+ Leave at least one full deploy cycle between Phase 1 and Phase 3.
37
+ There's no need to rush Phase 3 — you can always delete later, but deleted data doesn't come back.
38
+
39
+ ### 1.3 Rollback-ability
40
+
41
+ - For every migration, ask: "if the app is rolled back to the previous version right after this
42
+ migration is applied, does it still work?"
43
+ - A rollback mechanism doesn't have to be a "down" migration. If you follow expand/contract,
44
+ rolling back just the application *is* the rollback (the schema can safely stay in its
45
+ forward-compatible state)
46
+ - Destructive operations (DROP, TRUNCATE, narrowing a column's type) can't be undone by a down
47
+ migration. Take a backup or archive the data to a separate table before running them
48
+
49
+ ### 1.4 Keep schema changes and data migrations separate
50
+
51
+ Don't mix DDL (schema changes) and DML (data migrations) in the same migration.
52
+
53
+ - DDL finishes quickly and its lock impact is easy to estimate. DML scales with row count and can
54
+ run long
55
+ - Mixing them means that if something fails partway through, you're left in the worst possible
56
+ state: the schema changed but the data is half-migrated
57
+ - Put data migrations in their own migration, or better, a separate batch job or management
58
+ command
59
+
60
+ ### 1.5 One migration, one purpose
61
+
62
+ Each file should contain exactly one logical change.
63
+ Cramming "add a column to `users` + add an index to `orders` + drop an unused table" into one file
64
+ makes it hard to reason about state after a partial failure and hard to retry just the failed
65
+ piece. Keeping migrations small makes it easy to pinpoint what failed, retry it, and review it.
66
+
67
+ ## 2. Dangerous Patterns in PostgreSQL / MySQL
68
+
69
+ ### 2.1 Adding a NOT NULL column to a large table
70
+
71
+ ```sql
72
+ -- Dangerous: on PostgreSQL 10 and earlier, or some MySQL configurations, this rewrites
73
+ -- every row and holds a long-lived lock
74
+ ALTER TABLE orders ADD COLUMN status varchar(20) NOT NULL DEFAULT 'pending';
75
+
76
+ -- Safe, staged migration (PostgreSQL)
77
+ ALTER TABLE orders ADD COLUMN status varchar(20); -- 1. Add nullable (instant)
78
+ -- 2. Deploy the app so it writes a value on new rows
79
+ -- 3. Backfill existing rows in batches (see 2.4)
80
+ ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'pending'; -- 4. Set the default
81
+ ALTER TABLE orders ALTER COLUMN status SET NOT NULL; -- 5. Enforce NOT NULL last
82
+ ```
83
+
84
+ On PostgreSQL 11+, `ADD COLUMN` with a constant default is a metadata-only change and is fast,
85
+ but `SET NOT NULL` still requires a full table scan. On PostgreSQL 12+, it's safer to first add
86
+ `CHECK (status IS NOT NULL) NOT VALID` and then run `VALIDATE CONSTRAINT`.
87
+
88
+ ### 2.2 Locking caused by index creation
89
+
90
+ ```sql
91
+ -- Dangerous: a plain CREATE INDEX blocks writes to the table
92
+ CREATE INDEX idx_orders_user_id ON orders (user_id);
93
+
94
+ -- Safe: use CONCURRENTLY on PostgreSQL (must run outside a transaction)
95
+ CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
96
+ ```
97
+
98
+ - `CONCURRENTLY` cannot run inside a transaction. In Django this means `atomic = False`; other
99
+ tools have their own way to disable the per-migration transaction
100
+ - If it fails, an INVALID index is left behind. Drop it with `DROP INDEX CONCURRENTLY` and retry
101
+ - MySQL (InnoDB) usually supports online DDL, but be explicit with
102
+ `ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE` so you get an error (rather than a silent lock)
103
+ if it's not possible
104
+ - For very large tables on MySQL, consider gh-ost or pt-online-schema-change
105
+
106
+ ### 2.3 Renaming a column or changing its type: do it as a staged migration
107
+
108
+ `RENAME COLUMN` itself is fast, but as soon as it runs, any old code still referencing the old
109
+ name breaks immediately. Treat a rename as "add a column under a new name."
110
+
111
+ ```text
112
+ 1. Add the new-named column (nullable)
113
+ 2. Deploy the app so it writes to both columns, but still reads from the old one
114
+ 3. Backfill the data by copying old -> new
115
+ 4. Deploy the app so it reads from the new column (still writing to both)
116
+ 5. Deploy the app so it writes only to the new column
117
+ 6. Drop the old column (Contract)
118
+ ```
119
+
120
+ Type changes (e.g. int -> bigint) follow the same recipe by going through a new column of the
121
+ new type. On PostgreSQL, `ALTER TYPE` often rewrites every row under an ACCESS EXCLUSIVE lock.
122
+ The exception is compatible changes, like widening a varchar, which are metadata-only.
123
+
124
+ ### 2.4 Batching backfills
125
+
126
+ A single bulk UPDATE against existing rows causes long-running transactions, replication lag,
127
+ lock contention, and (on PostgreSQL) VACUUM pressure. Always split it up.
128
+
129
+ ```sql
130
+ -- Dangerous: a single bulk update (tens of millions of rows will blow up locks and WAL/binlog)
131
+ UPDATE orders SET status = 'pending' WHERE status IS NULL;
132
+
133
+ -- Safe: batch by primary key range
134
+ UPDATE orders SET status = 'pending'
135
+ WHERE id IN (
136
+ SELECT id FROM orders
137
+ WHERE status IS NULL
138
+ ORDER BY id
139
+ LIMIT 5000 -- tune the batch size based on observed load
140
+ );
141
+ -- Loop this from the application until zero rows match
142
+ ```
143
+
144
+ - Commit each batch in its own transaction, and add a short pause between batches
145
+ - Write it idempotently (the WHERE clause should make it safe to re-run after a partial failure)
146
+ - Check a replica-lag metric inside the loop and pause if it exceeds a threshold
147
+
148
+ ## 3. Tool-Specific Notes
149
+
150
+ ### 3.1 Django migrations
151
+
152
+ ```bash
153
+ # Always add this to CI: it catches model/migration drift (a missing makemigrations)
154
+ python manage.py makemigrations --check --dry-run
155
+
156
+ # Get in the habit of reviewing the SQL that will actually run before applying it
157
+ python manage.py sqlmigrate shop 0042
158
+ ```
159
+
160
+ ```python
161
+ from django.db import migrations
162
+
163
+ def forwards(apps, schema_editor):
164
+ # Always use apps.get_model. Importing the model directly will break this
165
+ # migration when the model changes in the future
166
+ Order = apps.get_model("shop", "Order")
167
+ while True:
168
+ ids = list(
169
+ Order.objects.filter(status__isnull=True).values_list("id", flat=True)[:5000]
170
+ )
171
+ if not ids:
172
+ break
173
+ Order.objects.filter(id__in=ids).update(status="pending")
174
+
175
+ class Migration(migrations.Migration):
176
+ atomic = False # Disable the per-migration transaction for bulk data work or
177
+ # CREATE INDEX CONCURRENTLY
178
+ dependencies = [("shop", "0041_add_status")]
179
+ operations = [
180
+ # Always specify reverse. Even if no reverse action is needed, make the noop
181
+ # explicit rather than leaving the migration irreversible
182
+ migrations.RunPython(forwards, migrations.RunPython.noop),
183
+ ]
184
+ ```
185
+
186
+ - Leaving `reverse` unspecified on `RunPython` makes that migration irreversible
187
+ - For adding indexes on PostgreSQL, use `AddIndexConcurrently`
188
+ (`django.contrib.postgres.operations`) together with `atomic = False`
189
+ - Squash migrations periodically to keep fresh-environment setup time and the dependency graph
190
+ manageable
191
+
192
+ ### 3.2 Laravel migrations
193
+
194
+ ```php
195
+ public function up(): void
196
+ {
197
+ Schema::table('orders', function (Blueprint $table) {
198
+ // Add as nullable; enforce NOT NULL in a separate migration
199
+ $table->string('status', 20)->nullable();
200
+ });
201
+ }
202
+
203
+ public function down(): void
204
+ {
205
+ Schema::table('orders', function (Blueprint $table) {
206
+ $table->dropColumn('status');
207
+ });
208
+ }
209
+ ```
210
+
211
+ - Always write `down()` for local-dev convenience, but don't rely on it as your production
212
+ rollback mechanism. In production, handle rollback via expand/contract plus rolling back the
213
+ application, not `migrate:rollback`
214
+ - MySQL implicitly commits DDL statements, so packing multiple DDL statements into one file can
215
+ leave things half-applied after a mid-migration failure. Stick to one migration, one purpose
216
+ - `php artisan migrate --pretend` lets you preview the SQL before it runs
217
+
218
+ ### 3.3 TypeORM / Prisma
219
+
220
+ - TypeORM: never use `synchronize: true` in production (it rewrites the schema implicitly).
221
+ Always eyeball the SQL that `migration:generate` produces to make sure it doesn't contain an
222
+ unintended DROP. Running migrations explicitly from the deploy pipeline via `migration:run` is
223
+ easier to control than auto-running them at app startup (`migrationsRun`)
224
+ - Prisma: use only `prisma migrate deploy` for production. `migrate dev` is for local development
225
+ only and can create/reset a shadow database. Don't use `prisma db push` in production either,
226
+ since it doesn't leave a migration history. The generated `migration.sql` file can be hand
227
+ edited, so make adjustments like switching `CREATE INDEX` to `CONCURRENTLY` directly in that
228
+ file
229
+ - For both tools, "auto-generated" does not mean "safe." A rename being generated as DROP + ADD,
230
+ losing data in the process, is a classic mistake with either tool
231
+
232
+ ## 4. DynamoDB
233
+
234
+ Being schemaless doesn't mean migrations go away — it just means the schema has moved into the
235
+ application code, and the application is now responsible for compatibility.
236
+
237
+ ### 4.1 Lazy migration (migrate on read)
238
+
239
+ Rewriting every item in bulk is expensive and risky, so convert items to the new shape lazily,
240
+ as they're read.
241
+
242
+ ```typescript
243
+ // Give each item a version attribute and convert it incrementally on read
244
+ function migrateItem(item: Record<string, any>): Order {
245
+ const version = item.schemaVersion ?? 1;
246
+ if (version < 2) {
247
+ // v1 -> v2: split fullName into firstName / lastName
248
+ const [firstName, ...rest] = (item.fullName ?? '').split(' ');
249
+ item.firstName = firstName;
250
+ item.lastName = rest.join(' ');
251
+ item.schemaVersion = 2;
252
+ }
253
+ return item as Order;
254
+ }
255
+ ```
256
+
257
+ - Always write items back in the latest shape, along with an updated `schemaVersion`
258
+ - Your read-path conversion function needs to keep supporting conversion from every past version.
259
+ If you want to drop support for old versions, run a one-time background scan-and-migrate job
260
+ first to bring everything current
261
+ - If you do run a bulk migration, use a segmented parallel Scan with rate limiting so you don't
262
+ eat into your write capacity
263
+
264
+ ### 4.2 Operating GSI additions
265
+
266
+ - Adding a GSI can be done online, but the backfill takes time, and queries against the index
267
+ before it finishes will return incomplete results. Wait until the index reports ACTIVE before
268
+ cutting the application over to it
269
+ - In other words, a GSI addition needs the same two-phase deploy as an RDBMS expand/contract:
270
+ add -> wait for backfill -> cut over the code
271
+ - A GSI's key attributes can't be changed after creation. To change them, add a new GSI and drop
272
+ the old one once you've cut over
273
+
274
+ ### 4.3 Key design changes are hard
275
+
276
+ - Partition keys and sort keys can't be changed once a table is created. Changing them really
277
+ means "create a new table, copy all the data, and cut over"
278
+ - In a CQRS setup, if you design things so the read model (query-side table) can be rebuilt from
279
+ the command-side events, then a key-design change on the query side reduces to a re-projection
280
+ - Where a migration truly is needed, set up dual writes via DynamoDB Streams, and cut reads over
281
+ to the new table once the copy is complete
282
+
283
+ ## 5. Pre-Execution Safety Checklist
284
+
285
+ 1. Will the previous version of the application still work after this schema change (backward
286
+ compatibility)?
287
+ 2. If this includes a breaking change (DROP / RENAME / narrowing a type), has it been broken down
288
+ into expand/contract?
289
+ 3. Have you checked the row count and size of the target table, and estimated the runtime against
290
+ a production-scale data volume?
291
+ 4. Have you identified what kind of lock will be taken, and how lock waits will affect the
292
+ application?
293
+ 5. Have you set `lock_timeout` / `statement_timeout` (or `lock_wait_timeout` on MySQL)?
294
+ 6. Is the data migration batched and idempotent, with a documented recovery procedure for a
295
+ partial failure?
296
+ 7. Have you documented the rollback procedure (one that doesn't depend on a down migration)?
297
+ 8. Have you verified that a backup / snapshot / PITR is in place right before running it?
298
+ 9. Have you rehearsed this against production-scale data in staging?
299
+ 10. Have you picked a low-traffic execution window and lined up monitoring (replica lag, locks,
300
+ error rate)?
301
+
302
+ ## 6. Anti-patterns
303
+
304
+ | Anti-pattern | What happens | Do this instead |
305
+ | --- | --- | --- |
306
+ | Releasing a column drop and the code change together | Old code crashes mid-rollout | Do the Contract step in a separate release, after the rollout is fully complete |
307
+ | Running `RENAME COLUMN` in one shot | Old code errors out immediately | Add a new column, dual-write, then cut over in stages |
308
+ | Bulk UPDATE against a huge table | Long transactions, locking, replica lag | Batch by primary-key range, with pauses |
309
+ | `CREATE INDEX CONCURRENTLY` inside a transaction | Errors out, or silently runs as a regular, locking `CREATE INDEX` | Disable the transaction and run it standalone |
310
+ | Mixing DDL and DML in one migration | A failure leaves things half-applied | Separate schema changes from data migrations |
311
+ | Treating a down migration as your production rollback plan | Data loss, or an unverified down that itself fails | Preserve forward compatibility and roll back via the application instead |
312
+ | Applying ORM-generated SQL without review | A rename becomes DROP + ADD and data is lost | Always eyeball the generated SQL |
313
+ | Using `synchronize: true` / `db push` in production | Implicit schema changes with no migration history | Change the schema only via migration files |
314
+ | Rewriting all DynamoDB items in one pass | Production impact from exhausted capacity | Use lazy migration, or a rate-limited bulk migration |
315
+ | Importing application models directly inside a migration | A future model change breaks past migrations | Use a historical model (e.g. Django's `apps.get_model`) |
316
+
317
+ ## 7. Related Workflow
318
+
319
+ - Before starting a large schema change, use `/plan` to work out the breakdown into
320
+ expand/contract steps and the release ordering
321
+ - Send changes that include migrations to `/code-review`
322
+ - For Django-specific migration safety (transaction control, `RunPython` reverse functions,
323
+ lock-holding operations), have the `django-reviewer` subagent take a close look
@@ -0,0 +1,308 @@
1
+ ---
2
+ name: docker-patterns
3
+ description: A collection of Docker / Docker Compose best practices covering multi-stage builds, layer caching, image size reduction, security, healthchecks, and Compose-based dev environment setup. Reference this when creating, modifying, or reviewing a Dockerfile or docker-compose.yml.
4
+ ---
5
+
6
+ # Docker Patterns
7
+
8
+ A skill that captures the decision criteria for writing Dockerfiles and Docker Compose files.
9
+ When starting from scratch, work through this order: base image selection → stage separation → cache optimization → security → healthchecks.
10
+
11
+ ## Multi-Stage Builds
12
+
13
+ Separate the build stage from the runtime stage so that build-only tooling (compilers, devDependencies, the Composer binary, etc.) never ends up in the runtime image.
14
+
15
+ Benefits:
16
+ - The runtime image is smaller, which speeds up distribution and deployment
17
+ - Vulnerabilities originating from build tools don't linger in the runtime environment
18
+ - Source code and intermediate build artifacts can't leak into the final image
19
+
20
+ ### Node.js example
21
+
22
+ ```dockerfile
23
+ # Good: use devDependencies in the build stage, hand only the build output to runtime
24
+ FROM node:22-slim AS build
25
+ WORKDIR /app
26
+ COPY package.json package-lock.json ./
27
+ RUN npm ci
28
+ COPY . .
29
+ RUN npm run build
30
+
31
+ FROM node:22-slim AS runtime
32
+ WORKDIR /app
33
+ ENV NODE_ENV=production
34
+ COPY package.json package-lock.json ./
35
+ RUN npm ci --omit=dev
36
+ COPY --from=build /app/dist ./dist
37
+ USER node
38
+ CMD ["node", "dist/main.js"]
39
+ ```
40
+
41
+ ### Python example
42
+
43
+ ```dockerfile
44
+ # Good: copy only the installed dependency output into the runtime stage
45
+ FROM python:3.12-slim AS build
46
+ WORKDIR /app
47
+ RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
48
+ && rm -rf /var/lib/apt/lists/*
49
+ COPY requirements.txt ./
50
+ RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
51
+
52
+ FROM python:3.12-slim AS runtime
53
+ WORKDIR /app
54
+ COPY --from=build /install /usr/local
55
+ COPY . .
56
+ RUN useradd --create-home --shell /usr/sbin/nologin appuser
57
+ USER appuser
58
+ CMD ["python", "-m", "app"]
59
+ ```
60
+
61
+ Keep compiler packages such as build-essential confined to the build stage. Only bring the artifacts installed via `--prefix` into the runtime stage.
62
+
63
+ ### PHP-FPM example
64
+
65
+ ```dockerfile
66
+ # Good: resolve dependencies in a composer stage, ship only vendor/ into the runtime image
67
+ FROM composer:2 AS vendor
68
+ WORKDIR /app
69
+ COPY composer.json composer.lock ./
70
+ RUN composer install --no-dev --prefer-dist --no-scripts --no-interaction
71
+
72
+ FROM php:8.3-fpm AS runtime
73
+ WORKDIR /var/www/html
74
+ RUN docker-php-ext-install pdo_mysql opcache
75
+ COPY --from=vendor /app/vendor ./vendor
76
+ COPY . .
77
+ RUN chown -R www-data:www-data /var/www/html
78
+ USER www-data
79
+ CMD ["php-fpm"]
80
+ ```
81
+
82
+ This keeps the (tens-of-MB) Composer binary and any dev-only dependencies excluded via `--no-dev` out of the runtime image.
83
+
84
+ ## Layer Caching
85
+
86
+ Docker caches layers individually and re-executes every layer from the point where the copied content first changes. Dependency manifests (lock files) change far less often than source code, so copy them in on their own first, letting the dependency-install step ride the cache.
87
+
88
+ ```dockerfile
89
+ # Bad: even a one-line source change forces npm ci to run every time
90
+ COPY . .
91
+ RUN npm ci
92
+
93
+ # Good: npm ci stays cached as long as the lock file doesn't change
94
+ COPY package.json package-lock.json ./
95
+ RUN npm ci
96
+ COPY . .
97
+ ```
98
+
99
+ Files to copy first, by language:
100
+ - Node.js: package.json and package-lock.json (or pnpm-lock.yaml / yarn.lock)
101
+ - Python: requirements.txt (or pyproject.toml plus poetry.lock / uv.lock)
102
+ - PHP: composer.json and composer.lock
103
+
104
+ Additional notes:
105
+ - Always combine `apt-get update` and `apt-get install` into a single `RUN`. Splitting them lets a stale package-list layer get reused, causing the install to fail or pull outdated versions.
106
+ - Place instructions that change less often earlier in the Dockerfile.
107
+
108
+ ## Reducing Image Size
109
+
110
+ ### Choosing between slim and alpine
111
+
112
+ - slim (Debian-based): the default choice. glibc-compatible, so native extensions and prebuilt binaries work out of the box.
113
+ - alpine (musl-based): reserve this for when you truly need the smallest possible size. Watch out for:
114
+ - Prebuilt binaries that assume glibc may not run under musl libc
115
+ - Python loses access to manylinux wheels, forcing a source build that's slower and often needs extras like build-base
116
+ - Node.js native modules (sharp, bcrypt, etc.) may also need rebuilding
117
+ - Subtle behavioral differences (e.g., DNS resolution) that are hard to debug
118
+
119
+ When in doubt, choose slim. Reserve alpine for cases with no native dependencies, or where image size constraints are strict.
120
+
121
+ ### .dockerignore
122
+
123
+ Keeps the build context small and prevents unwanted or sensitive files from leaking into the image. Always place one at the project root.
124
+
125
+ ```
126
+ # baseline .dockerignore
127
+ .git
128
+ node_modules
129
+ vendor
130
+ __pycache__
131
+ *.log
132
+ .env
133
+ .env.*
134
+ dist
135
+ coverage
136
+ Dockerfile
137
+ docker-compose*.yml
138
+ ```
139
+
140
+ Without a `.dockerignore`, a Dockerfile using `COPY . .` will bake `.env` and `.git` straight into the image.
141
+
142
+ ## Security
143
+
144
+ ### Run as a non-root user
145
+
146
+ Running the container process as root widens the blast radius if a vulnerability is exploited.
147
+
148
+ ```dockerfile
149
+ # Bad: no USER directive (runs as root)
150
+ CMD ["node", "server.js"]
151
+
152
+ # Good: use the user bundled with the official image (node for Node.js, www-data for php-fpm)
153
+ USER node
154
+ CMD ["node", "server.js"]
155
+ ```
156
+
157
+ For images that don't ship a ready-made user, create one with `useradd` (see the Python example above). `chown` any directories that need write access before switching `USER`.
158
+
159
+ ### Never bake secrets into the image
160
+
161
+ Secrets passed via ARG / ENV / COPY can be recovered from `docker history` or by inspecting layers. Removing a file with `rm` in a later layer does not remove it from earlier layers.
162
+
163
+ ```dockerfile
164
+ # Bad: the build-arg value persists in the image history
165
+ ARG NPM_TOKEN
166
+ RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc \
167
+ && npm ci && rm .npmrc
168
+
169
+ # Good: BuildKit secret mounts never land in a layer
170
+ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
171
+ ```
172
+
173
+ Pass the secret at build time with something like `docker build --secret id=npmrc,src=$HOME/.npmrc .`. For runtime secrets, use environment variables (Compose's `env_file`) or a secrets-management backend, and add `.env` files to both `.dockerignore` and `.gitignore`.
174
+
175
+ ### Pin versions (never use `latest`)
176
+
177
+ ```dockerfile
178
+ # Bad: content changes depending on when the image is pulled, breaking reproducibility
179
+ FROM node:latest
180
+ FROM python
181
+
182
+ # Good: pin at least major.minor
183
+ FROM node:22-slim
184
+ FROM python:3.12-slim
185
+
186
+ # Good: pin by digest when you need strict reproducibility
187
+ FROM node:22-slim@sha256:<digest>
188
+ ```
189
+
190
+ Pin versions for apt/pip packages too whenever reproducibility matters.
191
+
192
+ ## Healthchecks
193
+
194
+ ### Dockerfile HEALTHCHECK
195
+
196
+ ```dockerfile
197
+ # Good: poll the app's health endpoint on a schedule
198
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
199
+ CMD curl -fsS http://localhost:3000/healthz || exit 1
200
+ ```
201
+
202
+ On slim-family images that lack `curl`, fall back to `wget` or a check written in the language runtime itself (e.g. `CMD ["node", "healthcheck.js"]`).
203
+
204
+ ### Compose healthcheck and depends_on
205
+
206
+ By default, `depends_on` only guarantees *startup order*, not readiness. To wait until a database is actually ready to accept connections, combine `healthcheck` with a `condition`.
207
+
208
+ ```yaml
209
+ services:
210
+ db:
211
+ image: postgres:16
212
+ healthcheck:
213
+ test: ["CMD-SHELL", "pg_isready -U app"]
214
+ interval: 5s
215
+ timeout: 3s
216
+ retries: 10
217
+ start_period: 10s
218
+
219
+ api:
220
+ build: .
221
+ depends_on:
222
+ db:
223
+ condition: service_healthy # don't start api until db is healthy
224
+ ```
225
+
226
+ Condition types:
227
+ - `service_started`: wait only for the container to start (default)
228
+ - `service_healthy`: wait until the healthcheck succeeds
229
+ - `service_completed_successfully`: wait for a one-shot task (e.g. a migration) to exit successfully
230
+
231
+ ## Compose-Based Development Environments
232
+
233
+ ### Choosing between volume types
234
+
235
+ - Named volumes: for persistent data you want to survive container rebuilds, such as database data
236
+ - Bind mounts: for things like source code, where you want host-side edits reflected immediately
237
+
238
+ ```yaml
239
+ services:
240
+ api:
241
+ build: .
242
+ volumes:
243
+ - .:/app # source via bind mount (for hot reload)
244
+ - /app/node_modules # don't let the host's node_modules overwrite the container's
245
+ db:
246
+ image: postgres:16
247
+ volumes:
248
+ - db-data:/var/lib/postgresql/data # data via named volume
249
+
250
+ volumes:
251
+ db-data:
252
+ ```
253
+
254
+ ### Override files
255
+
256
+ Keep the base definition in `docker-compose.yml`, and isolate dev-only differences (bind mounts, debug ports, dev commands) in `docker-compose.override.yml`. Since `docker compose up` merges the override file automatically, production and CI should explicitly pass `-f docker-compose.yml` only, excluding the dev-only overrides.
257
+
258
+ ### Bind ports to localhost
259
+
260
+ ```yaml
261
+ # Bad: exposed on all interfaces, reachable from the LAN or beyond
262
+ ports:
263
+ - "5432:5432"
264
+
265
+ # Good: bind a dev database to the loopback interface only
266
+ ports:
267
+ - "127.0.0.1:5432:5432"
268
+ ```
269
+
270
+ For services the host never needs to reach directly, skip `ports` entirely and rely on container-to-container communication (via service-name resolution).
271
+
272
+ ### Network isolation
273
+
274
+ Minimize the reachable surface. Put only the reverse proxy on the outward-facing network, and keep the database reachable solely from the app.
275
+
276
+ ```yaml
277
+ services:
278
+ proxy:
279
+ networks: [frontend]
280
+ api:
281
+ networks: [frontend, backend]
282
+ db:
283
+ networks: [backend] # unreachable from proxy
284
+
285
+ networks:
286
+ frontend:
287
+ backend:
288
+ ```
289
+
290
+ ## Anti-Patterns
291
+
292
+ - Specifying a base image as `FROM xxx:latest` (breaks reproducibility)
293
+ - Writing `COPY . .` before the dependency-install step (invalidates the cache on every build)
294
+ - Running `COPY . .` without a `.dockerignore` (leaks `.git` or `.env` into the image)
295
+ - Passing secrets via ARG / ENV (recoverable from `docker history`)
296
+ - Running the app as root because `USER` was never set
297
+ - Splitting `apt-get update` and `apt-get install` into separate `RUN` instructions
298
+ - Leaving the apt cache (`/var/lib/apt/lists`) or pip cache in the image instead of clearing it
299
+ - Cramming multiple processes into one container (app + cron + nginx, etc.)
300
+ - Assuming `depends_on` alone guarantees the database is ready (no `condition` specified)
301
+ - Exposing a dev database port on `0.0.0.0`
302
+ - Reaching for alpine without considering native extension compatibility
303
+ - Writing logs to a file instead of stdout/stderr (let the runtime handle log collection)
304
+ - Running `git clone` inside the image instead of `COPY`-ing through the build context
305
+
306
+ ## Related
307
+
308
+ - Use `/code-review` when reviewing Dockerfile / Compose file changes