A new engineer’s first week used to go like this. Clone the repo. Install PHP, but not the current one: prod runs PHP 7.3, so you need that, with the right extensions. Ask someone for their config files. Get it mostly working, then move on to the next project, which needs Node 10 for its AngularJS frontend, while the one after that needs Node 20.
Nobody’s fault really.
Each project made sense on its own. Together they meant setup took days and “it works on my machine” was a daily sentence. VS Code dev containers fixed most of it. Now you clone, click “Reopen in Container”, and get the same environment as everyone else in minutes.
1. Match prod, not the newest version
A dev container is just the environment described in the repo, in a devcontainer.json. The editor runs inside it, so the terminal and debugger see the same tools. The real win is that the environment is now code, reviewed and versioned like everything else.
Our main HR backend runs PHP 7.3 in prod. The tempting mistake is to develop on a modern PHP and hope for the best, and then the version-specific bugs show up after deployment. So the container builds from php:7.3-apache with the same extensions and a pinned Composer version.
{
"name": "HC Portal Backend",
"dockerComposeFile": "./docker-compose.yml",
"service": "hcportal_api",
"workspaceFolder": "/workspace",
"forwardPorts": [8000],
"postCreateCommand": "./.devcontainer/post-create.sh"
}
A second image on PHP 8.2 sits next to it as the upgrade path.
Then post-create.sh does all the small manual steps once: Composer install, copy the the example configs to their real names, fix folder permissions, create the folders the app expects. Secrets never go in the image or the repo, the example files only have placeholders. When we found a live registry token commited in one branch, it got moved out of git, and the script now prints a warning on a missing credential instead of breaking the whole container.
The same idea works across very different stacks. The legacy AngularJS app needs Node 10, Bower and Gulp, which are hard to even install on a modern laptop; inside its container they’re pinned and just work.
2. What I’d do better
Pin everything. Where we pinned (Composer, air at a specific release, Features locked by digest in devcontainer-lock.json) rebuilds were boring in the best way. Where we didn’t, e.g. a MinIO image tagged latest, the environment could quietly drift between two engineers who built it a week apart. That’s the first thing I’d tighten.
Also match CI, not just prod. A test that passes in one and fails in the other wastes everyone’s time.
Dev containers didn’t make anyone a better engineer tbh. They removed a week of friction that had nothing to do with engineering, so day one is reading code and shipping a small change instead of installing PHP 7.3.