Setting Up Devcontainers for VS Code and Using Them Productively
AI generated
FROM
RUN
Docker · VS Code · Devcontainer · PHP
Setting Up Devcontainers for VS Code
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.

16 min read devcontainer.json · Features · Compose VS Code · Dev Containers Extension

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.

11. FAQ: Devcontainers for VS Code

1What exactly is a devcontainer?
A versioned devcontainer.json description of the development environment that VS Code opens directly inside a container, including tools and extensions.
2Do I need Docker Desktop for it?
No, any compatible runtime works, including alternatives like Colima or Podman with a Docker compatible socket.
3Difference from plain Docker Compose?
Native VS Code integration with automatic extension and debugging setup instead of manually attaching to the container.
4Are multiple services possible?
Yes, via dockerComposeFile plus a service key, while database and cache run as further services in the background.
5What are devcontainer features?
Reusable, community maintained installation building blocks for Git, Node and more, referenced via the features section.
6How does Xdebug work inside it?
Via host.docker.internal as client host and a launch.json in VS Code connecting through port 9003 to the container.
7Does the non root user cause problems?
Only with a UID mismatch. If the UID is set to match the host in the Dockerfile, no permission conflicts occur.
8Does it run in CI too?
Yes, via the devcontainer CLI outside of VS Code, so CI environments match the local development environment.
9How fast does it start versus local?
First build takes minutes, then thanks to layer caching a restart usually takes just seconds, much faster than a manual reinstall.
10Do personal settings stay intact?
Yes, via dotfiles repositories loaded alongside the project wide devcontainer.json, without changing the team configuration.