Dev Containers skill for Codex
Clone, open, tests pass, same as CI. Codex builds dev containers that match CI tool versions, install from lockfiles in onCreate, run real services through Compose, and keep dependency folders in volumes for speed. One idempotent setup script feeds the dev container, CI and the Codex cloud environment, so tests that pass locally also pass in agent tasks where network may be off after setup.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- devcontainers Installs to ~/.agents/skills/devcontainers/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install devcontainers The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project devcontainers Installs to .agents/skills/devcontainers/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/devcontainers.zipunzip -o devcontainers.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) devcontainers PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $devcontainers, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Creates and maintains .devcontainer/devcontainer.json setups - image or Dockerfile builds, Dev Container Features, Docker Compose multi-service environments, lifecycle hooks (onCreate, postCreate, postStart), forwarded ports, non-root users, mounts and caches, prebuilt images, the devcontainer CLI for CI, and keeping one setup script shared with Codex cloud environments so local, CI and agent runs match. Use when standardizing a dev environment, onboarding takes too long, "works on my machine" bugs appear, or Codex cloud tasks fail because tools differ from local.
Use it when
- A repository needs a reproducible, one-step development environment
- Onboarding takes hours or tool versions drift between machines
- Codex cloud or CI runs behave differently from local development
- A project needs databases or other services available while developing
Use something else when
- You want reproducible toolchains without containers: use nix-flakes
- You need to configure the Codex cloud environment itself: use codex-cloud-environments
How to check the result
devcontainer up succeeds from a clean clone, the AGENTS.md test command passes via devcontainer exec, and a Codex cloud task runs the same command successfully.
The mistake it prevents
Installing dependencies during tests instead of in the setup step, which breaks as soon as the agent or CI runs without network access.
A good dev container opens to a working environment in one step: toolchain versions match CI, dependencies install from lockfiles, services the app needs are running, and tests pass with the documented command. The same setup script feeds the dev container, CI and Codex cloud, so a task that passes in one passes in the others.
Inspect first
- Read
AGENTS.md,README,.tool-versions,.nvmrc,.python-version,rust-toolchain.toml,go.modand CI workflows: these define the versions to match. - Look for an existing
.devcontainer/(or.devcontainer.jsonat the root) anddocker-compose*.yml. - List services the app needs locally (Postgres, Redis, a queue) and their versions in production.
- Check the host: Docker Desktop, Podman or Colima; Apple silicon means images must have
arm64variants or builds slow down under emulation.
Layout
.devcontainer/
├── devcontainer.json
├── Dockerfile # only when Features are not enough
└── compose.yaml # only when services are needed
scripts/
└── setup.sh # shared by devcontainer, CI and Codex cloud
Single-container baseline
// .devcontainer/devcontainer.json
{
"name": "api",
"image": "mcr.microsoft.com/devcontainers/base:bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "22" },
"ghcr.io/devcontainers/features/python:1": { "version": "3.13" },
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"remoteUser": "vscode",
"containerEnv": { "PIP_DISABLE_PIP_VERSION_CHECK": "1" },
"mounts": [
"source=api-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
],
"onCreateCommand": "bash scripts/setup.sh",
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"forwardPorts": [3000],
"portsAttributes": { "3000": { "label": "web", "onAutoForward": "notify" } },
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint", "ms-python.python"],
"settings": { "editor.formatOnSave": true }
}
}
}
- Pin Feature major versions (
:1) and tool versions;latestdrifts away from CI. - Put
node_modules(or.venv,target/) in a named volume on macOS and Windows; bind mounts of large dependency trees are slow. - The
vscodeuser in the official base images matches host UID on Linux throughupdateRemoteUserUID(default true), avoiding root-owned files in the repo.
Lifecycle hooks
| Hook | Runs | Use for |
|---|---|---|
initializeCommand | On the host, before build | Creating .env from example; avoid heavy work |
onCreateCommand | Once, when container is created (also during prebuilds) | Install dependencies |
updateContentCommand | After create and when content updates in prebuilds | Refresh deps after lockfile change |
postCreateCommand | Once, after create, with user secrets available | Steps needing user credentials |
postStartCommand | Every start | Start background services, fix permissions |
postAttachCommand | Every editor attach | Rarely needed |
Keep hooks idempotent; they rerun on rebuild.
Multi-service with Compose
{
"name": "api",
"dockerComposeFile": "compose.yaml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"shutdownAction": "stopCompose",
"onCreateCommand": "bash scripts/setup.sh"
}
# .devcontainer/compose.yaml
services:
app:
build: { context: .., dockerfile: .devcontainer/Dockerfile }
volumes: ["..:/workspaces/api:cached"]
command: sleep infinity
environment:
DATABASE_URL: postgres://dev:dev@db:5432/api_dev
db:
image: postgres:17
environment: { POSTGRES_USER: dev, POSTGRES_PASSWORD: dev, POSTGRES_DB: api_dev }
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev"]
interval: 5s
Match the database major version to production. The app reaches services by service name
(db), not localhost.
The shared setup script
#!/usr/bin/env bash
# scripts/setup.sh - one source of truth for dev container, CI and Codex cloud
set -euo pipefail
corepack enable
npm ci
python -m pip install --upgrade uv
uv sync --frozen
- Make it idempotent, non-interactive and offline-tolerant after the first run.
- Call it from
onCreateCommand, from CI before tests, and pastebash scripts/setup.shinto the Codex cloud environment’s setup script field.
Codex cloud parity
- Codex cloud runs tasks in its own container (based on the
codex-universalimage) and does not readdevcontainer.json. Parity comes from sharing the setup script and pinning the same language versions in the environment settings. - Codex cloud runs the setup script with network access; the agent phase may have network off. Everything tests need (dependencies, generated code, test fixtures) must be installed in setup, not fetched during tests.
- Services such as Postgres are not started for you there; either start them in setup (if installable in the image), use lightweight fallbacks such as SQLite for unit tests, or mark integration tests to skip when the service is absent.
- Record the test command in
AGENTS.mdso local Codex, the dev container and cloud tasks all run the same thing.
Prebuilt images and CI
npm install -g @devcontainers/cli # needs network
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . npm test
devcontainer build --workspace-folder . --image-name ghcr.io/acme/api-devcontainer:latest
- Prebuild the image in CI when setup takes more than a few minutes, then reference it with
"image"(orcacheFrom) so contributors pull instead of build. - Run the test suite inside the dev container in CI at least weekly to catch drift.
- Pushing images to a registry is outward-facing; confirm the registry and tag with the user.
Gotchas
- Secrets in
containerEnvor committed.envfiles end up in the repo; useremoteEnvwith${localEnv:VAR}or the editor’s secret store. - Features install in declaration order only when dependencies are declared; use
overrideFeatureInstallOrderif one Feature needs another. - Docker-in-Docker Feature needs privileged mode; prefer
docker-outside-of-dockerfor local use, and know that both widen the container’s access to the host. - Changing
devcontainer.jsonrequires “Rebuild Container”; changes to the setup script alone do not rerunonCreateCommand.
Verification
-
devcontainer up --workspace-folder .succeeds from a clean clone -
devcontainer exec --workspace-folder . <test command from AGENTS.md>passes - Tool versions inside (
node -v,python --version) match CI - A Codex cloud task in the same repo runs the test command successfully
- Time from clone to green tests recorded (target under 10 minutes cold, under 1 warm)
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Dev Containers"
short_description: "Clone, open, tests pass, same as CI"
default_prompt: "$devcontainers Add a dev container for this repo with Node 22, Python 3.13 and Postgres" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.