Containerized development
Use this skill when Docker or Docker Compose is installed and available. If
Docker tooling is available, switch to containerized development instead of using
host runtimes, package managers, compilers, test runners, or project
dependencies directly.
Before using Docker, check for available Docker tooling, for example with
docker --version and docker compose version. Prefer Compose when available.
If Docker is missing, unavailable, or not running, do not scaffold Docker files
as a workaround unless the user explicitly asks for Docker setup.
When this skill applies, keep the host clean: avoid installing language
runtimes, project dependencies, or build toolchains locally just to complete the
task. Everything for that project run should happen in the container; the host
provides Docker and an editor.
This applies to local dev work whenever Docker tooling is available — first-time
setup, running the app, builds, tests, and installing/updating dependencies. If
Docker is available and the Docker files don't exist yet, scaffold them
first, then proceed inside the container.
Stack-agnostic. This is not a Node.js convention — it applies to any
language or toolchain: C/C++ (gcc/clang, CMake, Make), Rust (cargo), Go, Python,
Java, Ruby, etc. "Dependencies," "build," and "dev server" mean whatever they
mean for the project's stack (compilers and headers, a build system, a compiled
binary, an interpreter, a long-running server — or just a CLI that builds and
exits). When this procedure applies, the rule is the same regardless: the
toolchain and build happen in the container, not on the host.
The model
Dockerfile — defines the dev image: base runtime, system packages, and a
dependency-install step. This is where the toolchain lives.
compose.yaml (aka docker-compose.yml) — defines the dev service(s):
the build context, port mappings, env, and the bind mount of the project
source into the container's working directory.
docker compose watch — the preferred dev loop. It syncs source changes
into the running container (and rebuilds when dependency manifests change) so
you edit on the host and the change takes effect in the container immediately.
Plain docker compose up is the fallback when watch isn't configured.
- An in-container reloader (nodemon,
node --watch, uvicorn --reload,
air, cargo watch, …) — the second half of the loop. watch gets the
files into the container; the reloader restarts the process so they take
effect. See "Live reload" below — this is the nodemon-style auto-update.
Procedure
- Check whether this skill applies. If Docker or Docker Compose is
available, use this procedure instead of host tools. Prefer Compose when it is
available.
- Check for Docker files. If
Dockerfile and a compose file are absent,
scaffold them (see below) before doing containerized work.
- Build the image.
docker compose build (or let up build on first run).
- Bring it up. Prefer
docker compose watch; fall back to
docker compose up.
- Install dependencies inside the container for this procedure. Either bake
the install into the
Dockerfile, or run it in the running container:
docker compose exec <service> <install command>. Adding a dependency means
updating the manifest and reinstalling in the container, then letting the
image rebuild.
- Run project commands in the container — dev server, builds, tests, REPLs,
one-off scripts — via the container (see below).
Live reload: docker compose watch + an in-container reloader
Auto-update is two layers, and both must be present or edits won't take effect:
- Get the file into the container —
docker compose watch syncs the changed
host file into the running container (it does not need a bind mount; sync
copies the file in).
- Restart the process so it picks up the file — a running
node/python/
compiled server won't notice a swapped file on its own. Either a process-level
reloader inside the container restarts it (nodemon-style), or watch restarts the
whole container for you. Pick one of these per service.
The develop.watch actions
In compose.yaml, each service gets a develop.watch list. Each entry has a
path (host), an action, and usually a target (container path) and ignore:
sync — copy changed files into the running container. The file lands, but
the process is unchanged — so this only auto-updates if something inside the
container is watching and restarting, i.e. a nodemon-style reloader (or a
framework with built-in HMR like Vite/Next). This is the classic combo.
sync+restart — sync the file and restart the container's main process.
No in-container reloader needed — Compose itself is your "nodemon." Best for
config files, or any service whose entrypoint starts fresh quickly. Restarts the
container, does not rebuild the image.
sync+exec — sync, then run a command in the container (Compose ≥ v2.32).
Good for "file changed → run a migration / regenerate" without a full restart.
rebuild — rebuild the image and recreate the container. Use for dependency
manifests (package.json, requirements.txt, go.mod, Cargo.toml) and the
Dockerfile itself — anything that changes the image, not just source.
A typical Node service uses two entries: sync on the source dir (nodemon
restarts the process) and rebuild on package.json.
Two valid setups — choose one
sync + a reloader in the container (true nodemon-style): the container's
command is the reloader (nodemon, node --watch, tsx watch, uvicorn --reload, air, cargo watch -x run, …). Fastest inner loop — only the app
process restarts, the container keeps running. Preferred for an active dev loop.
sync+restart, no reloader: simpler, no extra dev dependency; Compose
restarts the whole container on each change. Slightly slower per change but
nothing to configure inside the image. Good default when you don't want a
reloader, or for non-Node stacks without a great one.
The filesystem-events gotcha
A reloader watching synced/bind-mounted files inside a container sometimes misses
inotify events (common with bind mounts, and on macOS/Windows hosts). If saves
don't trigger a restart, switch the reloader to polling:
- nodemon:
nodemon -L (a.k.a. --legacy-watch).
- chokidar-based (Vite, webpack, many JS tools): env
CHOKIDAR_USEPOLLING=true.
- Python watchdog / uvicorn reload: generally works; if not, use polling-based
watchers (
watchmedo --debounce/poll) or fall back to sync+restart.
Polling costs some CPU — only enable it if event-based watching actually fails.
Running it
docker compose watch (or docker compose up --watch to see app logs and watch
in one terminal). Edit a file on the host → watch syncs it → the reloader (or
sync+restart) restarts the process → the change is live.
Concrete, copy-pasteable per-stack configs (Node/nodemon, Vite, Python/uvicorn,
Go/air, Rust/cargo-watch) live in reference.md — read it when scaffolding watch
for a specific stack.
Running commands: exec vs run
Both run a command in the container; pick by whether the service is already up.
docker compose exec <service> <command> — runs inside the
already-running container started by up/watch. Use this for the normal
dev loop (tests, shells, scripts) while the stack is up. Fails if the service
isn't running.
docker compose run --rm <service> <command> — spins up a fresh one-off
container, runs the command, and removes it (--rm). Use when nothing is
running yet, or for isolated tasks (a migration, a single test run, a throwaway
shell). Note it does not publish the service's ports by default — add
--service-ports if you need them.
- Prefer the compose forms over raw
docker exec <container> ..., which needs the
container name/ID instead of the service name from compose.yaml.
Scaffolding when files are missing
Create a minimal, stack-appropriate Dockerfile and compose.yaml:
Dockerfile: pick a base image matching the project's runtime, set a working
directory, copy the dependency manifest(s), run the install step, then copy the
rest of the source.
compose.yaml: define the dev service with build: ., the relevant published
ports, env, and a develop.watch block for docker compose watch — sync the
source path and rebuild on changes to the dependency manifest (see "Live
reload" above and reference.md for full configs).
- Dev command + reload strategy: set the service
command to a reloader
(e.g. nodemon) paired with action: sync, or use action: sync+restart and
a plain start command. Don't scaffold sync alone with a non-reloading command
— files would sync but the app would never restart.
Keep it minimal and idiomatic for the stack. Confirm the runtime/version and
exposed ports from the project before generating, rather than guessing.
Keep Docker stuff in a .docker/ dir
Contain all Docker-related artifacts in a .docker/ directory that lives at
the root of each individual project (one per project — not in your home dir
or any global location), instead of scattering them across the project root:
- Put supporting Docker config there — extra Dockerfiles, env files, init
scripts, named-volume data, and any local Docker state the project generates.
- Point bind/named volumes at paths under
.docker/ (e.g. a database's data dir),
so persisted container state lives in one predictable place.
- Gitignore the data under
.docker/ (volume contents, local env) while
keeping the config files tracked. Never commit volume data or secrets.
Dockerfile and compose.yaml can stay at the project root (tooling expects
them there) and reference .docker/ for the rest; or move them in and point
compose at them — either is fine, just keep the artifacts and state
corralled in .docker/.
Clean up when done
Containers, images, and volumes accumulate and consume real disk. When finished
with a session or a project, tear down what you created:
- Stop and remove the stack:
docker compose down removes the containers and
the default network. Add -v (docker compose down -v) to also remove named
volumes declared in the compose file — do this when you want a clean slate
(e.g. resetting a dev database).
- Remove the built image when you no longer need it:
docker compose down --rmi local (removes images built by this compose
project), or docker image rm <image> for a specific one.
- Prune dangling/old artifacts periodically:
docker image prune (dangling
images), docker container prune (stopped containers), docker volume prune
(unused volumes). docker system prune does all of these at once; add
--volumes to include unused volumes and -a to also remove unused (not just
dangling) images.
- Careful with
-v / --volumes and prune — they delete persisted data
(databases, caches). Only run them when you actually want that state gone. Data
you keep under .docker/ makes it obvious what's about to disappear.
Why
- The host machine stays free of per-project runtimes and dependencies — no
version conflicts, no global pollution, trivially reproducible environments.
- The container is the single source of truth for "how this project runs," so it
works the same on any machine and matches CI/production more closely.
docker compose watch keeps the edit-on-host / run-in-container loop fast.
Don't
- Don't use this skill when Docker is missing, unavailable, or not running.
- Don't choose host tools over Docker just because the host tools are already
installed.
- Once this procedure applies, don't install deps or toolchains on the host to
"just get it working" —
npm install, pip install, cargo build,
apt install g++, cmake, go mod download, bundle install, etc. belong
in the container.
- Once this procedure applies, don't run the dev server, compiler/build, or tests
directly on the host.
- Once this procedure applies, don't skip scaffolding the Docker files and develop
locally as a shortcut — set the container up first.
1---2name: containerized-development3description: Develop inside Docker containers when Docker or Docker Compose is installed and available. Prefer containerized development over host tooling for local development work whenever Docker tooling is available, even if the needed host tools are also installed. Use a Dockerfile + docker compose (with `docker compose watch` for live sync/reload), bind-mount or sync the project source, keep Docker artifacts/state in a project `.docker/` dir, and install dependencies and run dev work inside the container.4---56# Containerized development78**Use this skill when Docker or Docker Compose is installed and available.** If9Docker tooling is available, switch to containerized development instead of using10host runtimes, package managers, compilers, test runners, or project11dependencies directly.1213Before using Docker, check for available Docker tooling, for example with14`docker --version` and `docker compose version`. Prefer Compose when available.15If Docker is missing, unavailable, or not running, do not scaffold Docker files16as a workaround unless the user explicitly asks for Docker setup.1718When this skill applies, keep the host clean: avoid installing language19runtimes, project dependencies, or build toolchains locally just to complete the20task. Everything for that project run should happen in the container; the host21provides Docker and an editor.2223This applies to local dev work whenever Docker tooling is available — first-time24setup, running the app, builds, tests, and installing/updating dependencies. If25Docker is available and the Docker files don't exist yet, **scaffold them26first**, then proceed inside the container.2728**Stack-agnostic.** This is not a Node.js convention — it applies to any29language or toolchain: C/C++ (gcc/clang, CMake, Make), Rust (cargo), Go, Python,30Java, Ruby, etc. "Dependencies," "build," and "dev server" mean whatever they31mean for the project's stack (compilers and headers, a build system, a compiled32binary, an interpreter, a long-running server — or just a CLI that builds and33exits). When this procedure applies, the rule is the same regardless: the34toolchain and build happen in the container, not on the host.3536## The model3738- **`Dockerfile`** — defines the dev image: base runtime, system packages, and a39 dependency-install step. This is where the toolchain lives.40- **`compose.yaml`** (aka `docker-compose.yml`) — defines the dev service(s):41 the build context, port mappings, env, and the **bind mount** of the project42 source into the container's working directory.43- **`docker compose watch`** — the preferred dev loop. It syncs source changes44 into the running container (and rebuilds when dependency manifests change) so45 you edit on the host and the change takes effect in the container immediately.46 Plain `docker compose up` is the fallback when watch isn't configured.47- **An in-container reloader** (nodemon, `node --watch`, uvicorn `--reload`,48 `air`, `cargo watch`, …) — the second half of the loop. `watch` gets the49 *files* into the container; the reloader restarts the *process* so they take50 effect. See "Live reload" below — this is the nodemon-style auto-update.5152## Procedure53541. **Check whether this skill applies.** If Docker or Docker Compose is55 available, use this procedure instead of host tools. Prefer Compose when it is56 available.572. **Check for Docker files.** If `Dockerfile` and a compose file are absent,58 scaffold them (see below) before doing containerized work.593. **Build the image.** `docker compose build` (or let `up` build on first run).604. **Bring it up.** Prefer `docker compose watch`; fall back to61 `docker compose up`.625. **Install dependencies inside the container** for this procedure. Either bake63 the install into the `Dockerfile`, or run it in the running container:64 `docker compose exec <service> <install command>`. Adding a dependency means65 updating the manifest and reinstalling **in the container**, then letting the66 image rebuild.676. **Run project commands in the container** — dev server, builds, tests, REPLs,68 one-off scripts — via the container (see below).6970## Live reload: `docker compose watch` + an in-container reloader7172Auto-update is **two layers**, and both must be present or edits won't take effect:73741. **Get the file into the container** — `docker compose watch` syncs the changed75 host file into the running container (it does *not* need a bind mount; sync76 copies the file in).772. **Restart the process so it picks up the file** — a running `node`/`python`/78 compiled server won't notice a swapped file on its own. Either a process-level79 reloader inside the container restarts it (nodemon-style), or watch restarts the80 whole container for you. Pick **one** of these per service.8182### The `develop.watch` actions8384In `compose.yaml`, each service gets a `develop.watch` list. Each entry has a85`path` (host), an `action`, and usually a `target` (container path) and `ignore`:8687- **`sync`** — copy changed files into the running container. The file lands, but88 the process is unchanged — so this **only auto-updates if something inside the89 container is watching and restarting**, i.e. a nodemon-style reloader (or a90 framework with built-in HMR like Vite/Next). This is the classic combo.91- **`sync+restart`** — sync the file **and restart the container's main process**.92 No in-container reloader needed — Compose itself is your "nodemon." Best for93 config files, or any service whose entrypoint starts fresh quickly. Restarts the94 container, does **not** rebuild the image.95- **`sync+exec`** — sync, then run a command in the container (Compose ≥ v2.32).96 Good for "file changed → run a migration / regenerate" without a full restart.97- **`rebuild`** — rebuild the image and recreate the container. Use for dependency98 manifests (`package.json`, `requirements.txt`, `go.mod`, `Cargo.toml`) and the99 `Dockerfile` itself — anything that changes the image, not just source.100101A typical Node service uses two entries: `sync` on the source dir (nodemon102restarts the process) and `rebuild` on `package.json`.103104### Two valid setups — choose one105106- **`sync` + a reloader in the container** (true nodemon-style): the container's107 command is the reloader (`nodemon`, `node --watch`, `tsx watch`, `uvicorn108 --reload`, `air`, `cargo watch -x run`, …). Fastest inner loop — only the app109 process restarts, the container keeps running. Preferred for an active dev loop.110- **`sync+restart`, no reloader**: simpler, no extra dev dependency; Compose111 restarts the whole container on each change. Slightly slower per change but112 nothing to configure inside the image. Good default when you don't want a113 reloader, or for non-Node stacks without a great one.114115### The filesystem-events gotcha116117A reloader watching synced/bind-mounted files inside a container sometimes misses118inotify events (common with bind mounts, and on macOS/Windows hosts). If saves119don't trigger a restart, switch the reloader to **polling**:120121- **nodemon:** `nodemon -L` (a.k.a. `--legacy-watch`).122- **chokidar-based** (Vite, webpack, many JS tools): env `CHOKIDAR_USEPOLLING=true`.123- **Python watchdog / uvicorn reload:** generally works; if not, use polling-based124 watchers (`watchmedo` `--debounce`/poll) or fall back to `sync+restart`.125126Polling costs some CPU — only enable it if event-based watching actually fails.127128### Running it129130`docker compose watch` (or `docker compose up --watch` to see app logs and watch131in one terminal). Edit a file on the host → watch syncs it → the reloader (or132`sync+restart`) restarts the process → the change is live.133134**Concrete, copy-pasteable per-stack configs** (Node/nodemon, Vite, Python/uvicorn,135Go/air, Rust/cargo-watch) live in `reference.md` — read it when scaffolding watch136for a specific stack.137138## Running commands: `exec` vs `run`139140Both run a command in the container; pick by whether the service is already up.141142- **`docker compose exec <service> <command>`** — runs inside the143 **already-running** container started by `up`/`watch`. Use this for the normal144 dev loop (tests, shells, scripts) while the stack is up. Fails if the service145 isn't running.146- **`docker compose run --rm <service> <command>`** — spins up a **fresh one-off**147 container, runs the command, and removes it (`--rm`). Use when nothing is148 running yet, or for isolated tasks (a migration, a single test run, a throwaway149 shell). Note it does **not** publish the service's ports by default — add150 `--service-ports` if you need them.151- Prefer the compose forms over raw `docker exec <container> ...`, which needs the152 container name/ID instead of the **service name** from `compose.yaml`.153154## Scaffolding when files are missing155156Create a minimal, stack-appropriate `Dockerfile` and `compose.yaml`:157158- `Dockerfile`: pick a base image matching the project's runtime, set a working159 directory, copy the dependency manifest(s), run the install step, then copy the160 rest of the source.161- `compose.yaml`: define the dev service with `build: .`, the relevant published162 ports, env, and a `develop.watch` block for `docker compose watch` — `sync` the163 source path and `rebuild` on changes to the dependency manifest (see "Live164 reload" above and `reference.md` for full configs).165- **Dev command + reload strategy:** set the service `command` to a reloader166 (e.g. `nodemon`) paired with `action: sync`, *or* use `action: sync+restart` and167 a plain start command. Don't scaffold `sync` alone with a non-reloading command168 — files would sync but the app would never restart.169170Keep it minimal and idiomatic for the stack. Confirm the runtime/version and171exposed ports from the project before generating, rather than guessing.172173## Keep Docker stuff in a `.docker/` dir174175Contain all Docker-related artifacts in a **`.docker/`** directory that lives at176the **root of each individual project** (one per project — not in your home dir177or any global location), instead of scattering them across the project root:178179- Put supporting Docker config there — extra Dockerfiles, env files, init180 scripts, named-volume data, and any local Docker state the project generates.181- Point bind/named volumes at paths under `.docker/` (e.g. a database's data dir),182 so persisted container state lives in one predictable place.183- **Gitignore the data** under `.docker/` (volume contents, local env) while184 keeping the config files tracked. Never commit volume data or secrets.185- `Dockerfile` and `compose.yaml` can stay at the project root (tooling expects186 them there) and reference `.docker/` for the rest; or move them in and point187 `compose` at them — either is fine, just keep the *artifacts and state*188 corralled in `.docker/`.189190## Clean up when done191192Containers, images, and volumes accumulate and consume real disk. When finished193with a session or a project, tear down what you created:194195- **Stop and remove the stack:** `docker compose down` removes the containers and196 the default network. Add `-v` (`docker compose down -v`) to **also remove named197 volumes** declared in the compose file — do this when you want a clean slate198 (e.g. resetting a dev database).199- **Remove the built image** when you no longer need it:200 `docker compose down --rmi local` (removes images built by this compose201 project), or `docker image rm <image>` for a specific one.202- **Prune dangling/old artifacts** periodically: `docker image prune` (dangling203 images), `docker container prune` (stopped containers), `docker volume prune`204 (unused volumes). `docker system prune` does all of these at once; add205 `--volumes` to include unused volumes and `-a` to also remove unused (not just206 dangling) images.207- **Careful with `-v` / `--volumes` and `prune`** — they delete persisted data208 (databases, caches). Only run them when you actually want that state gone. Data209 you keep under `.docker/` makes it obvious what's about to disappear.210211## Why212213- The host machine stays free of per-project runtimes and dependencies — no214 version conflicts, no global pollution, trivially reproducible environments.215- The container is the single source of truth for "how this project runs," so it216 works the same on any machine and matches CI/production more closely.217- `docker compose watch` keeps the edit-on-host / run-in-container loop fast.218219## Don't220221- Don't use this skill when Docker is missing, unavailable, or not running.222- Don't choose host tools over Docker just because the host tools are already223 installed.224- Once this procedure applies, don't install deps or toolchains **on the host** to225 "just get it working" — `npm install`, `pip install`, `cargo build`,226 `apt install g++`, `cmake`, `go mod download`, `bundle install`, etc. belong227 in the container.228- Once this procedure applies, don't run the dev server, compiler/build, or tests229 directly on the host.230- Once this procedure applies, don't skip scaffolding the Docker files and develop231 locally as a shortcut — set the container up first.