A Dev Container gives every contributor a documented, repeatable development environment instead of asking each person to assemble compatible runtimes, command-line tools, and services by hand. This guide explains how to add one to an open-source project, configure the editor and container, connect optional services with Docker Compose, and maintain the setup as the codebase changes.
Overview
A development container is a containerized workspace designed for coding rather than production deployment. The project repository stores its setup instructions as configuration files, usually inside a .devcontainer directory. When a contributor opens the project with a compatible Dev Containers client, the client builds or downloads the defined image, mounts the repository, installs the requested tools, and runs project-specific setup commands.
This approach is useful for open-source projects because it turns undocumented workstation assumptions into reviewable project files. A contributor can use the same language runtime, package manager, linters, formatters, and supporting services as the rest of the team. It does not remove every source of variation: operating systems, editor settings, credentials, and host-level Docker configuration can still matter. However, it provides a clear baseline that is easier to test and improve.
Before starting, identify the project’s actual requirements:
- Which language runtimes and versions are supported?
- Which package manager installs dependencies?
- Which editor extensions or language servers are useful?
- Which ports must be available for local development?
- Does the application depend on a database, cache, queue, or other service?
- Which environment variables are safe to provide as examples?
Keep secrets out of the container configuration. Commit a documented example such as .env.example, and explain which values a contributor must provide locally. For broader repository practices, see the open-source repository maintenance checklist.
Checklist by scenario
Scenario 1: A single-language project
For a small application or library with one primary runtime, begin with a single devcontainer.json. Choose a maintained base image that matches the project’s language and required toolchain. Pin the major or minor runtime version deliberately rather than relying on an unspecified latest tag. The exact image tag should reflect the versions tested by the project.
Create the directory and configuration file:
{
"name": "project-dev",
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"editor.formatOnSave": true
}
}
},
"forwardPorts": [3000],
"postCreateCommand": "npm ci"
}
Treat this as a pattern, not a universal copy-and-paste answer. Replace the image, extensions, port, and install command with values that belong to the project. For Python, Ruby, Go, Rust, Java, or another ecosystem, use the corresponding runtime image and package-manager command. If the project uses a lockfile, prefer the install command that honors it.
Scenario 2: A project with environment variables
Separate non-sensitive defaults from user-specific configuration. A committed .env.example can document required names and harmless local values:
APP_PORT=3000
LOG_LEVEL=debug
DATABASE_URL=postgres://app:app@db:5432/app
Document the setup step in the README, for example by instructing contributors to copy the example file to .env. Do not place access tokens, private keys, production connection strings, or personal credentials in the repository or image. If the application reads variables from a file, ensure the file is ignored by version control when it contains local secrets.
Scenario 3: A project with database or supporting services
Use Docker Compose when the application needs services that should run beside the development container. A typical layout contains .devcontainer/devcontainer.json and .devcontainer/compose.yaml:
services:
app:
image: mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm
volumes:
- ..:/workspaces/project:cached
command: sleep infinity
depends_on:
- db
networks:
- devnet
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- devnet
volumes:
postgres-data:
networks:
devnet:
Reference the Compose service from devcontainer.json rather than treating the database as an undocumented prerequisite:
{
"name": "project-compose-dev",
"dockerComposeFile": "compose.yaml",
"service": "app",
"workspaceFolder": "/workspaces/project",
"forwardPorts": [3000, 5432],
"postCreateCommand": "npm ci"
}
Inside the Compose network, the application should normally connect to the database using the service name, such as db, rather than localhost. Explain this distinction in the project documentation because it is a common first-run obstacle.
Scenario 4: An existing project with several workflows
Do not force every contributor into one configuration if the repository supports different stacks or operating systems. You can provide separate Dev Container definitions for a backend, frontend, or full-stack workflow, with clear names and instructions. Keep shared decisions consistent where possible, but let each configuration install only the tools it needs. This reduces build time and avoids making a simple documentation or frontend task depend on unrelated infrastructure.
What to double-check
- Runtime alignment: Confirm that the container’s language and operating-system versions match the versions used in tests and documented support guidance.
- Dependency installation: Verify that the post-create command works from a clean checkout and respects the lockfile. If installation is expensive, consider whether it belongs in the image build instead.
- Editor support: Add only extensions that improve the project workflow, such as a language server, formatter, linter, or test runner. Avoid silently imposing personal preferences.
- Ports: List application, documentation, debugging, and service ports that contributors actually need. Make sure the application binds to an interface reachable from the container environment when required.
- File permissions: Check who owns files created inside the mounted workspace. A container process running as root can leave contributors with files they cannot easily edit on the host.
- Startup behavior: Decide whether services should start automatically or remain available for an explicit command. Automatic startup is convenient, but it can hide which process failed.
- Build repeatability: Rebuild from a clean state after changing the image, feature, Compose file, or post-create command. A configuration that works only because of an old cached layer is not reproducible.
- Documentation: Add a short “Dev Container” section to the README with prerequisites, opening instructions, first-run commands, test commands, and known limitations.
The container is a local development aid, not a replacement for continuous integration. Run the project’s normal checks in CI as well, using an explicitly maintained workflow. If the project is adding automated checks or deployment, the related guides on self-hosted CI/CD tools and deploying a web app from a Git repository provide useful surrounding context.
Common mistakes
Using an unpinned latest image. A moving tag can change the runtime or system packages without a corresponding project change. Choose a deliberate tag and review it when the supported runtime changes.
Putting secrets in configuration. Dev Container files are source-controlled and may be visible to every contributor. Use examples and local secret injection instead.
Installing everything during every rebuild. Large post-create commands make onboarding slow. Put stable system dependencies in a custom image or feature, while keeping project dependencies in a step that can be rerun safely.
Confusing host and container addresses. localhost refers to the current network namespace. An application in one Compose service should generally use the other service’s name; a browser on the host should use a forwarded port.
Assuming the container fixes platform differences completely. Native modules, file watching, line endings, permissions, and Docker resource limits may still behave differently across hosts. Record workarounds rather than hiding them.
Skipping a clean-checkout test. A contributor should not need undocumented files from the maintainer’s machine. Test the complete flow with a new clone or a clean workspace.
When to revisit
Review the Dev Container whenever the project changes its language runtime, package manager, database version, test command, editor support, or service topology. Also revisit it when a contributor joins and encounters a setup step that is not documented, when a workstation is rebuilt, or when a base image or feature reaches a major release change.
A practical maintenance checklist is:
- Open the repository from a clean checkout.
- Build or reopen the Dev Container without relying on stale caches.
- Run the documented install, lint, test, and development commands.
- Start each required supporting service and verify the connection strings.
- Check forwarded ports from the host and confirm that file edits, formatting, and tests behave as expected.
- Review image tags, features, extensions, and setup commands against the project’s current toolchain.
- Update the README and example environment file whenever the workflow changes.
Make changes in small, reviewable commits and explain why a runtime, image, or service definition changed. When the container setup is treated as part of the source code rather than a convenience file, it becomes a durable onboarding tool for the open-source project.