and Using Them Productively
A devcontainer moves the entire development environment into a container, so every developer uses the exact same PHP version, the same extensions and the same tools, regardless of their own machine. Instead of hours of onboarding documentation, a single click on "Reopen in Container" is enough.
Table of Contents
- 1. What a devcontainer really solves
- 2. Basic structure of devcontainer.json
- 3. Building a custom Dockerfile for the devcontainer
- 4. Shipping extensions and editor settings automatically
- 5. Configuring a non root user and port forwarding correctly
- 6. Devcontainers with Docker Compose for multi service setups
- 7. Devcontainer features instead of custom Dockerfile layers
- 8. Setting up debugging and Xdebug inside the devcontainer
- 9. Devcontainers compared to other setup approaches
- 10. Summary
- 11. FAQ
1. What a devcontainer really solves
A devcontainer is a standardized description of a complete development environment that VS Code opens directly inside a running container via the Dev Containers extension. Instead of installing PHP, Composer, Node and all extensions locally, a single configuration file defines which base image is used, which additional tools get installed and which editor extensions are automatically active. The effect: a new colleague clones the repository, opens it in VS Code, clicks "Reopen in Container", and a few minutes later is working in an environment identical to the rest of the team.
The actual problem a devcontainer solves is the drift of local environments over time. Without a devcontainer, every developer installs PHP, Xdebug and Node independently, with slightly different versions, different php.ini settings, and eventually small, hard to reproduce bugs where "it works on my machine". A devcontainer turns the environment itself into a versioned artifact in the repository that travels with every commit and is identical for everyone.
This article walks through the complete path from the first devcontainer.json to a productive multi service setup with PHP, MySQL and Redis, including debugging configuration for Xdebug inside the devcontainer setup.
2. Basic structure of devcontainer.json
Every devcontainer starts with a file at .devcontainer/devcontainer.json in the project root. This file is technically JSON with comment support (JSONC) and describes which image or Dockerfile serves as the base, which ports are forwarded, which environment variables are set and which lifecycle hooks run after the container is created. VS Code reads this file automatically as soon as the Dev Containers extension is installed and a matching project is opened.
{
"name": "mironsoft-shop-dev",
"dockerFile": "Dockerfile",
"context": "..",
"forwardPorts": [8080, 3306, 6379],
"portsAttributes": {
"8080": { "label": "Web", "onAutoForward": "notify" }
},
"remoteUser": "developer",
"postCreateCommand": "composer install && npm install",
"customizations": {
"vscode": {
"extensions": [
"bmewburn.vscode-intelephense-client",
"xdebug.php-debug",
"esbenp.prettier-vscode"
]
}
}
}
The key postCreateCommand is especially important for a productive devcontainer: it runs exactly once after the container is first created, making it ideal for composer install, npm install or loading a database fixture. The key remoteUser defines which user VS Code operates as inside the container, which directly affects file permissions between host and container.
3. Building a custom Dockerfile for the devcontainer
A generic base image is rarely enough for PHP projects, because extensions like pdo_mysql, intl or opcache need to be installed per project. A custom Dockerfile in the .devcontainer directory gives full control over the PHP version, installed extensions and additional CLI tools like ShellCheck or the PHP CS Fixer, needed in daily development but that should not end up in the production image.
# .devcontainer/Dockerfile — development-only image, not used in production
FROM php:8.4-fpm
RUN apt-get update && apt-get install -y \
git unzip libzip-dev libicu-dev \
&& docker-php-ext-install pdo_mysql intl opcache zip \
&& pecl install xdebug \
&& docker-php-ext-enable xdebug
# Composer for dependency management
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Non-root user matching the host UID to avoid file permission issues
ARG USERNAME=developer
ARG USER_UID=1000
RUN useradd -m -u ${USER_UID} -s /bin/bash ${USERNAME}
USER ${USERNAME}
WORKDIR /workspace
Important for daily devcontainer use: this Dockerfile is deliberately separate from the production Dockerfile. Development tools like Xdebug or Git do not belong in a production image, but should naturally be available inside the devcontainer. The separation prevents development dependencies from accidentally ending up in a deployment.
4. Shipping extensions and editor settings automatically
One of the underrated features of a devcontainer setup is the automatic installation of VS Code extensions directly inside the container. Through the customizations.vscode.extensions key in devcontainer.json, VS Code automatically installs every listed extension inside the container on first start, without a developer having to search for and install them manually. This drastically reduces onboarding friction, since PHP Intelephense, Xdebug integration and linter extensions are ready to use right away.
Project specific editor settings can also be shipped directly via customizations.vscode.settings, for example the formatting rule for PHP files or the preferred tab behavior. These settings only apply inside the devcontainer context and do not override any global VS Code settings on the host, avoiding conflicts between personal preferences and project standards.
5. Configuring a non root user and port forwarding correctly
A common stumbling block with a new devcontainer: files created inside the container suddenly belong to root, because the container runs as root by default. This causes permission problems on the host as soon as developers try to edit the same files outside the container. The solution is a dedicated non root user in the Dockerfile whose UID exactly matches the host user's UID, combined with the remoteUser key in devcontainer.json.
Port forwarding is the second important building block: the forwardPorts key ensures VS Code automatically forwards ports from the container to the host, with no manual -p flags needed. For a devcontainer with a web server, a database and Redis, several ports can be forwarded at once, with individual labels and behavior via portsAttributes, for example an automatic browser preview for the web server port.
6. Devcontainers with Docker Compose for multi service setups
As soon as a project needs more than one container, for example PHP-FPM, MySQL and Redis together, a plain Dockerfile becomes too inflexible. For this case, the devcontainer standard directly supports Docker Compose: instead of dockerFile, the devcontainer.json references a docker-compose.yml, and an additional service key defines which service VS Code opens into.
{
"name": "mironsoft-shop-dev",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"shutdownAction": "stopCompose"
}
# .devcontainer/docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- ../..:/workspace:cached
command: sleep infinity
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: shop_dev
volumes:
- db_data:/var/lib/mysql
redis:
image: redis:7.4-alpine
volumes:
db_data:
The sleep infinity command in the app service is a deliberate pattern: the container should not start a web server like in production, but simply stay alive while VS Code works inside it via a remote shell. The actual web server or PHP-FPM process is then started manually or through a separate postStartCommand as needed, giving the devcontainer more control over the lifecycle of individual processes.
7. Devcontainer features instead of custom Dockerfile layers
Devcontainer features are reusable, versioned building blocks that add commonly needed tools like Git, GitHub CLI or Node.js to an existing image, without a custom Dockerfile having to maintain that logic itself. Instead of writing apt-get install git again in every project, devcontainer.json references an official feature maintained by the community that automatically brings current best practices for installation and configuration.
{
"name": "mironsoft-shop-dev",
"dockerFile": "Dockerfile",
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "20" },
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"configureZshAsDefaultShell": true
}
}
}
The advantage of features over manual Dockerfile layers: they are maintained and updated independently of the base image, keeping a devcontainer setup overall lower maintenance. A team that pulls in Node and Git through features instead of custom RUN lines only needs to bump the feature version on security updates, instead of going through the entire Dockerfile.
8. Setting up debugging and Xdebug inside the devcontainer
Debugging is where a devcontainer shows its biggest practical benefit, since Xdebug configuration is usually one of the most error prone parts of a local PHP setup. Since Xdebug is already installed in the Dockerfile, only the php.ini configuration for the client host needs to be set correctly, along with a matching VS Code launch configuration entry.
; .devcontainer/xdebug.ini — mounted into the container
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.idekey=VSCODE
On the VS Code side, a matching launch.json with type php and port 9003 is enough, combined with a path mapping between the container path /workspace and the local project path. Since the entire devcontainer is already versioned in the repository, debugging works identically for every team member, without individual per machine Xdebug troubleshooting.
9. Devcontainers compared to other setup approaches
Devcontainers are not the only way to standardize a local development environment, but they offer the tightest integration with the editor itself.
| Approach | Editor integration | Onboarding effort | Reproducibility |
|---|---|---|---|
| Local installation | No particular integration | High, many manual steps | Low, drifts over time |
| Plain Docker Compose | Manual attach required | Medium | High |
| Devcontainer | Native in VS Code | Low, one click | High |
| Virtual machine | None | High, large download | Medium to high |
The clear advantage of a devcontainer setup lies in direct editor integration: extensions, debugging and the terminal feel exactly like a local installation, yet run fully isolated inside the container. Plain Docker Compose without devcontainer integration offers the same reproducibility, but requires manually attaching the editor to the running container.
Mironsoft
Devcontainer setup, onboarding automation and PHP development environments
Get new developers productive in minutes?
We build devcontainer configurations for your PHP and Node projects, including Xdebug, multi service Compose setups and automatic extension installation, turning onboarding into a click instead of half a day.
Devcontainer setup
devcontainer.json, Dockerfile and Compose integration for your project
Debugging integration
Setting up Xdebug and launch configuration consistently for the whole team
Onboarding docs
Quick start guide for new team members based on the devcontainer setup
10. Summary
A devcontainer is more than another Docker setup, it is a versioned agreement, carried along in the repository, about how a project is developed. The devcontainer.json describes base image, ports, environment variables and extensions in a single file, a custom Dockerfile brings project specific PHP extensions, and devcontainer features reduce maintenance effort for commonly needed tools like Git or Node.
For multi service projects with a database and cache, the devcontainer standard integrates seamlessly with Docker Compose, while debugging via Xdebug works identically for every team member thanks to versioned configuration. Anyone wanting to reduce onboarding time and avoid environment drift between developer machines will find the devcontainer approach currently offers the tightest integration between editor and container.
Devcontainers for VS Code — The Essentials at a Glance
devcontainer.json
Central configuration file for base image, ports, environment variables, extensions and lifecycle hooks.
Multi service setups
dockerComposeFile plus service key ties PHP, MySQL and Redis into a shared devcontainer setup.
Features
Reusable, community maintained building blocks instead of manual Dockerfile layers for Git, Node and CLI tools.
Debugging
Xdebug configuration and VS Code launch file versioned in the repository, working identically for every team member.