Guidance for AI coding agents (including the GitHub Copilot coding agent) working in this repository.
This repo contains the official Dev Container Templates.
Each template lives in src/<template-id>/ and is mirrored by tests in test/<template-id>/.
A template is made of:
src/<id>/devcontainer-template.json— template metadata and user-selectableoptions.src/<id>/.devcontainer/— thedevcontainer.json(and optionalDockerfile/docker-compose.yml) that the template generates.src/<id>/NOTES.md,src/<id>/README.md— docs (README is auto-generated, do not hand-edit; see below).
Build/validation helpers live in build/:
build/check-image-tags.ts— compares the image tags referenced by templates against the tags actually published by devcontainers/images. Run:npx tsx build/check-image-tags.ts <path-to-images-repo>.build/list-template-images.ts— lists every fully-qualified image a template produces.build/increment-patch.sh— bumps the patch version of everydevcontainer-template.json.
- Formatting is enforced by Prettier using
.prettierrc(4 spaces, no tabs, single quotes in JS/TS). Runnpx prettier --write <files>after editing JSON/TS. - Do not hand-edit
src/*/README.md— they are regenerated by theUpdate Documentationworkflow. - Every
devcontainer-template.jsonmust retain non-empty string values for the mandatoryid,version, andnamefields. Never remove these fields. Preserve the existingidandnameunless the task explicitly requires changing them, and verify all three fields after editing the file. - Every change to a template must bump the
versionfield in that template'sdevcontainer-template.json(semantic versioning; a variant add/remove is a patch bump). - Keep changes minimal and scoped to the templates that actually need updating.
Templates pin a base image and expose its variants through a single string option
(usually named imageVariant). The image reference contains a placeholder that is
substituted with the chosen option value:
// src/java/devcontainer-template.json
"options": {
"imageVariant": {
"type": "string",
"proposals": ["25-trixie", "21-trixie", "...", "8-bookworm"],
"default": "25-trixie"
}
}So the concrete image tag is prefix + option value, e.g.
java:3- + 25-trixie = java:3-25-trixie. The proposals array is a curated
subset of {version}-{os} variants — it intentionally does not list every tag the
images repo publishes (floating tags such as java:3-25, OS-only tags such as
java:3-trixie, JDK/-jdk aliases, etc. are deliberately omitted).
The checker scans every devcontainer.json, Dockerfile, and compose file for image
references, so a single image's variant change can surface as MISSING/UNUSED tags in more
than one template. Before concluding you are done, account for all three ways a template
can reference an image:
- Primary templates — expose the variants via their own
options.imageVariant.proposals(e.g.javascript-node,rust,ruby,go,debian,jekyll). - Dependent templates — build
FROM mcr.microsoft.com/devcontainers/<image>:...-${templateOption:imageVariant}in theirDockerfile, tracking the same base image's variants. These have their ownimageVariantoption that must be edited in lockstep with the primary template. Known dependents (non-exhaustive — always re-derive from the checker output):javascript-node-mongo,javascript-node-postgresbuildFROM devcontainers/javascript-node.rust-postgresbuildsFROM devcontainers/rust.ruby-rails-postgresbuildsFROM devcontainers/ruby.
- Hardcoded / static pins — pin a fixed tag with no
imageVariantoption, usuallybase:<os>indevcontainer.jsonor aFROM devcontainers/base:<os>in aDockerfile. When that OS tag stops being published, repoint the literal tag (e.g.base:bullseye→base:bookworm); there are noproposalsto edit. Known base-pinned templates (non-exhaustive):docker-in-docker,docker-outside-of-docker,docker-outside-of-docker-compose,kubernetes-helm,kubernetes-helm-minikube,markdown.
Do not stop after editing only the primary templates. A variant that disappears from
javascript-node also disappears from javascript-node-mongo/-postgres; a base OS tag
that disappears affects every hardcoded pin above. Re-run the checker (see "Validating your
change") and keep iterating until zero MISSING tags remain across all templates.
When devcontainers/images adds or removes an
image variant, the affected templates must be updated. The scheduled
"Compare Templates against Images" workflow only runs the checks; a maintainer must trigger
it manually (workflow_dispatch) to open a tracking issue (assigned to the coding agent)
containing the output of build/check-image-tags.ts, which classifies tags as:
- MISSING — referenced by a template but no longer published by images.
→ Action: remove that variant from the matching template's
options.imageVariant.proposals. Low-noise signal, but see the caveat below: a tag can be published by a differently-named image directory, so confirm it is genuinely gone from the images repo before removing it. - UNUSED — published by images but not referenced by any template.
→ Mostly intentional (aliases, floating/OS-only tags), but do not blanket-dismiss the
whole list. Scan it for a genuinely new concrete
{version}-{os}variant — typically a newly released major/minor (e.g. Go1.27-trixie/1.27-bookworm) that matches the template's existing naming convention. Such a variant must be added. Distinguish it from aliases by cross-checking the image'ssrc/<image>/manifest.jsonin the images repo: a genuine new version appears in thevariantsarray (and is often the newlatest), whereas floating majors (3-trixie), OS-only tags (3-bookworm), and-jdk/language aliases are not new versions. When in doubt, add concrete{version}-{os}tags and ignore the rest.
- Identify the template(s) from the tag's image name (the part before
:). Map the tag back to a template by finding thesrc/*/.devcontainer/*file whose image reference shares that prefix. A single tag often maps to several templates — the primary template plus its dependents and any hardcoded pins (see "One image change fans out to several templates" above). Some templates map to several images (e.g.phpandphp-mariadb).- A single image tag may be published from more than one image directory. The image
name (before
:) does not always match the images-repo directory that publishes the tag. Notably, thejavatemplate's8-trixie/8-bookwormvariants producejava:3-8-trixie/java:3-8-bookworm, which are published by the separatesrc/java-8image directory — not bysrc/java. Do not remove these variants just because they are absent fromsrc/java/manifest.json; verify againstsrc/java-8/manifest.jsonfirst. The same applies tojava-postgres, which builds on the samejavaimage.
- A single image tag may be published from more than one image directory. The image
name (before
- Removals (MISSING): delete the obsolete value from
proposalsonly after you have confirmed the tag is not published by any image directory (grep everysrc/*/manifest.jsonin the images repo for the tag, not just the one whose name matches the image prefix). Ifdefaultequals a removed value, setdefaultto the new newest variant (see ordering below). - Additions (UNUSED, only genuine new variants): insert the new value into
proposalsin the correct position (see ordering below).- If every variant for an image is reported MISSING, the image's pinned major in the
.devcontainerimage reference has changed (e.g.php:3-→php:4-). Update that prefix in the template'sdevcontainer.json/Dockerfileas well, then re-derive theproposals.
- If every variant for an image is reported MISSING, the image's pinned major in the
- Proposals ordering convention (match the existing files exactly):
- Group by OS, newest OS first. Observed priority:
trixie→bookworm→bullseye(Debian); for OS-named variants, Debian newest→oldest then Ubuntu newest→oldest (e.g.debian13,debian12,ubuntu24.04,ubuntu22.04). - Within an OS group, list versions descending (newest first).
- A floating major (e.g. Python's
3-trixie) comes first within its OS group.
- Group by OS, newest OS first. Observed priority:
- Default convention: the newest concrete version on the newest OS
(e.g.
25-trixie,1.26-trixie; Python uses the concrete3.14-trixie, not the floating3-trixie). - Do not touch options that are not image variants (e.g. cpp's
reinstallCmakeVersionFromSource, boolean feature toggles). - Preserve required metadata: image synchronization must not remove or rewrite the
top-level
idorname. Confirm thatid,version, andnameremain present as non-empty strings in every editeddevcontainer-template.json. - Bump
version(patch) in each editeddevcontainer-template.json.
Run the checker against the images repo and confirm there are no remaining MISSING tags for the templates you touched:
# Clone the images repo somewhere, then:
npx tsx build/check-image-tags.ts ../imagesA successful sync produces zero MISSING tags. Remaining UNUSED tags are expected and
acceptable (they are mostly intentional aliases) — but first confirm none of them is a
genuinely new concrete {version}-{os} variant that should have been added. If the checker
still reports MISSING tags, you have not covered every referencing template yet (commonly a
missed dependent or hardcoded pin); fix those and re-run until MISSING is empty. Finish
by running npx prettier --write src/**/devcontainer-template.json.