M365 Show -  Microsoft 365 Digital Workplace Daily
M365 Show with Mirko Peters - Microsoft 365 Digital Workplace Daily
CI/CD With Dev Containers: Flawless Victory Or Epic Fail?
0:00
-19:19

CI/CD With Dev Containers: Flawless Victory Or Epic Fail?

Imagine queuing up for raid night, but half your guild’s game clients are patched differently. That’s what building cloud projects feels like without Dev Containers—chaos, version drift, and way too many ‘works-on-my-machine’ tickets. If you work with Azure and teams, you care about one thing: consistent developer environments. Before we roll initiative on this boss fight, hit subscribe and toggle notifications so you’ve got advantage in every future run.

In this session, you’ll see exactly how a devcontainer.json works, why Templates and Features stop drift, how pre-building images cuts startup lag, and how to share Git credentials safely inside containers. The real test—are Dev Containers in CI/CD your reliable path to synchronized builds, or do they sometimes roll a natural 1?

Let’s start with what happens when your party can’t sync in the first place.

When Your Party Can’t Sync

When your squad drifts out of sync, it doesn’t take long before the fight collapses. Azure work feels the same when every engineer runs slightly different toolchains. What starts as a tiny nudge—a newer SQL client here, a lagging Node version there—snowballs until builds misfire and pipelines redline.

The root cause is local installs. Everyone outfits their laptop with a personal stack of SDKs and CLIs, then crosses their fingers that nothing conflicts. It only barely works. CI builds splinter because one developer upgrades Node without updating the pipeline, or someone tests against a provider cached on their own workstation but not committed to source. These aren’t rare edge cases; the docs flag them as common drift patterns that containers eliminate. A shared image or pre‑built container means the version everyone pulls is identical, so the problem never spawns.

Onboarding shows it most clearly. Drop a new hire into that mess and you’re handing them a crate of random tools with no map. They burn days installing runtimes, patching modules, and hunting missing dependencies before they can write a single line of useful code. That wasted time isn’t laziness—it’s the tax of unmanaged drift.

Even when veterans dig in, invisible gaps pop up at the worst moments. Running mismatched CLIs is like casting spells with the wrong components—you don’t notice until combat starts. With Azure, that translates into missing Bicep compilers, outdated PowerShell modules, or an Azure CLI left to rot on last year’s build. Queries break, deployments hang, and the helpdesk gets another round of phantom tickets.

The real‑world fallout isn’t hypothetical. The docs call out Git line‑ending mismatches between host and container, extension misfires on Alpine images, and dreaded SSH passphrase hangs. They’re not application bugs; they’re tool drift unraveling the party mid‑dungeon.

This is where Dev Containers flatten the field. Instead of everyone stacking their own tower of runtimes, you publish one baseline. The devcontainer.json in the .devcontainer folder is the contract: it declares runtimes, extensions, mounts. That file keeps all laptops from turning into rogue instances. You don’t need to trust half‑remembered setup notes—everyone pulls the same container, launches VS Code inside it, and gets the same runtime, same extensions, same spelling of reality.

It also kills the slow bleed of onboarding and failing CI. When your whole team spawns from the same image, no one wastes morning cycles copying config files or chasing arcane errors. Your build server gets the same gear loadout as your laptop. A junior engineer’s VM rolls with the same buffs as a senior’s workstation. Instead of firefighting mismatches, you focus on advancing the quest.

The measurable payoff is speed and stability. Onboarding shrinks from days to hours. CI runs stop collapsing on trivial tool mismatches. Developers aren’t stuck interpreting mysterious error logs—they’re working against the same environment, every single time. Even experiments become safer: you can branch a devcontainer to test new tech without contaminating your base loadout. When you’re done, you roll back, and nothing leaks into your daily kit.

So the core takeaway is simple: containers stop the desync before it wipes the group. Every player hits the dungeon on the same patch level, the buffs are aligned, and the tools behave consistently. That’s the baseline you need before any real strategy even matters.

But synchronizing gear is just the first step. Once everyone’s in lockstep, the real advantage comes from how you shape that shared foundation—because no one wants to hand‑roll a wizard from scratch every time they log in.

Templates as Pre-Built Classes

In RPG terms, picking a class means you skip the grind of rolling stats from scratch and jump right into the fight with a kit that already works. That’s what Dev Container Templates do for your projects—they’re the pre-built classes of the dev world, baked with sane defaults and ready to run.

Without them, you’re forcing every engineer to cobble their own sheet. One dev kludges together Docker basics, another scavenges an old runtime off the web, and somebody pastes in a dusty config file from a blog nobody checks anymore. Before writing a single piece of app code, you’ve already burned a day arguing what counts as “the environment.”

Templates wipe out that thrash. In VS Code, you hit the Command Palette and choose “Dev Containers: Add Dev Container Configuration Files….” From there you pull from a public template index—what containers.dev calls the gallery. Select an Azure SQL Database template and VS Code auto-generates a .devcontainer folder with a devcontainer.json tuned for database work. Extensions, Docker setup, and baseline configs are already loaded. It’s the equivalent of spawning your spellcaster with starter gear and a couple of useful cantrips already slotted.

Same deal with the .NET Aspire template. You can try duct taping runtimes across everyone’s laptops, or you can start projects with one standard template. The template lays down identical versions across dev machines, remote environments, and CI. Instead of builds diverging into chaos, you get consistency down to the patch level. Debugging doesn’t mean rerolling saves every five minutes, because every player is using the same rulebook.

And it’s not just about the first spin-up. Templates continue to pay off daily. For Node in Azure, one template can define the interpreter, pull in the right package manager, and configure Docker integration so that every build comes container-ready. No scavenger hunt, no guesswork. Think of it like a class spec: you can swap one skill or weapon, but you aren’t forced to reinvent “what magic missile even does” every session.

Onboarding is where it’s most obvious. With a proper template, adding a new engineer shifts from hours of patching runtimes and failed installs to minutes of opening VS Code and hitting “Reopen in Container.” As soon as the environment reloads, they’re running on the exact stack everyone else is using. Instead of tickets about missing CLIs or misaligned versions, they’re ready to commit before the coffee cools.

Because templates live in repos, they evolve without chaos. When teams update a base runtime, fix a quirk, or add a handy extension, the change hits once and everyone inherits it. That’s like publishing an updated character guide—suddenly every paladin gets higher saves without each one browsing a patch note forum. Nothing is left to chance, and nobody gets stuck falling behind.

Templates also scale with your team’s growth. Veteran engineers don’t waste time re-explaining local setup, and new hires don’t fight mystery configs. Everyone uses the same baseline loadout, the same devcontainer.json, the same reproducible outcome. In practice, that prevents drift from sneaking in and killing your pipeline later.

The nutshell benefit: Templates transform setup from a dice roll into a repeatable contract. Every project starts on predictable ground, every laptop mirrors the same working environment, and your build server gets to play by the same rules. Templates give you stability at level one instead of praying for lucky rolls.

But these base classes aren’t the whole story. Sometimes you want your kit tuned just a little tighter—an extra spell, a bonus artifact, the sort of upgrade that changes how your character performs. That’s when it’s time to talk about Features.

Features: Loot Drops for Your Toolkit

Features are the loot drops for your environment—modular upgrades that slot in without grind or guesswork. Clear the room, open the chest, and instead of a random rusty sword you get a tool that actually matters: Git, Terraform, Azure CLI, whatever your project needs. Technically speaking, a Feature is a self-contained install unit referenced under the "features" property in devcontainer.json and can be published as an OCI artifact (see containers.dev/features). That one line connects your container to a specific capability, and suddenly your characters all roll with the same buff.

The ease is the point. Instead of writing long install scripts and baking them into every Dockerfile, you just call the Feature in your devcontainer.json and it drops into place. One example: you can reference ghcr.io/devcontainers/features/azure-cli:1 in the features section to install the Azure CLI. No scribbling apt-get commands, no worrying which engineer fat-fingered a version. It’s declarative, minimal, and consistent across every environment.

Trying to work without Features means dragging your party through manual setup every time you need another dependency. Every container build turns into copy-paste scripting, apt-get loops, and the slow dread of waiting while installs grind. Worse, you still risk different versions sneaking in depending on base image or local cache. It’s fragile and when it breaks, you lose hours you didn’t budget. Features sidestep that. They’re like slotting a power-up you know will always spawn correctly, no dice roll required.

Once you understand them as building blocks, the strategy becomes clear. Want Terraform ready by default? Declare the Terraform Feature. Need Git to stop the “fatal: command not found” tickets? Add the Git Feature. Working against Azure daily? Equip the Azure CLI Feature. Think of them as your baseline spells—always on the bar, always present, so you don’t forget the crucial buff mid-fight.

Features also cover a longer game. Instead of pulling packages one by one across repos, your team can design custom Features for internal toolchains. You author it once, publish it to a registry, and reuse it everywhere. The documentation spells it out: internal Features reduce duplication and let teams distribute consistent tooling across projects. It’s like your guild forging a signature artifact—one crafted item, but everyone can now equip it without having to smith it from scratch.

Distribution is flexible too, because Features are packaged as OCI Artifacts. That means they can live in GitHub Container Registry, Docker Hub, or your Azure Container Registry. Whether public or private, the storage pattern is the same. Pick it from the Feature index or call it out directly, and the integration happens automatically during container build.

There’s even a quality-of-life setting for when you don’t want to think about it at all. With dev.containers.defaultFeatures, you can make sure common tools are always present across all containers you build. Same idea applies to defaultExtensions in VS Code—set them once, and they ride along in every workspace. It’s baseline consistency baked into the ecosystem.

A word of caution though: Features aren’t magic wands. They install during the container’s build or creation process, and order can matter. If multiple Features overlap in what they configure, you may need to adjust overrideFeatureInstallOrder so the right one wins. It’s not common, but when that natural 1 shows up, it’s usually because two Features tried to write over the same slot.

Automation is where Features level up. By referencing them directly in CI/CD pipelines, the environments spun up in GitHub Actions or Azure DevOps mirror your local dev setup exactly. Instead of guessing if the pipeline has the right tooling, you know it’s pulling the same Features defined in the config. That alignment turns drift into a non-issue: local developers, new hires, and build servers all roll identical gear.

Onboarding also shrinks. A newcomer doesn’t have to run ten installs before they contribute. They clone the repo, VS Code reads the devcontainer.json, Features snap in, and they’re ready on the same day. Less chaos, fewer helpdesk tickets, and no wasted sprints explaining why their linter won’t run.

So the payoff is a modular, repeatable kit. You define your loadout once, extend it cleanly with Features, and distribute it everywhere. No mystery installs, no version drift, no reinventing setup from project to project. You build your environment like a curated loot table instead of scavenging random gear.

Of course, once the team is kitted out with the right loadouts, there’s still one boss to deal with: nothing kills momentum faster than waiting through painful startup lag. And just like game night, no one enjoys standing around while a teammate downloads a massive patch.

Pre-Building: No More Loading Screens

That brings us to pre-building, the simple trick that keeps your environments from acting like they’re booting off a floppy disk every morning. Pre-Building: No More Loading Screens. Instead of letting every container build itself on demand—with all the installs, patches, and version roulette that entails—you frontload the work once and save everyone else from slow starts.

For a small repo, a cold spin-up might not sting. But scale that across an Azure team spawning containers dozens of times a day, and the wasted time stacks up fast. Every pipeline, every test job, every local spin repeats the same expensive setup. Pre-building shifts that cycle: you produce a ready-to-use image ahead of time, so developers and pipelines launch from a finished state instead of waiting for installs.

Think of on-demand builds as rolling into a dungeon where the loot table is shuffled every time. Sometimes you get the right gear, sometimes you get junk, and you always wait around to see what drops. Pre-building fixes the roll. You bake in the runtimes, CLIs, and libraries, then pin them for consistency. Nobody’s hoping today’s install script runs the same way it did yesterday—you’re pulling a prepared image where the outcome is certain.

The best part is that the tools already exist to automate it. Pre-build with the Dev Container CLI or CI (for example, a scheduled GitHub Action) and push the image to a registry such as Azure Container Registry; the image can include Dev Container metadata so devcontainer.json settings are picked up automatically. That’s a repeatable pipeline: your config defines the loadout, the CLI builds it, CI triggers a refresh when you schedule it or update dependencies, and the registry delivers the artifact.

Automation is critical because keeping images current shouldn’t be manual labor. A simple pattern is to rebuild nightly or whenever dependency versions bump. CI kicks off, produces the updated image, and pushes it to your registry. The next morning, every developer pulls down a fresh, consistent environment without losing time downloading tools one by one. Updates stop being Slack messages begging teammates to upgrade their CLI, and instead arrive quietly through the pipeline.

Stability becomes the default. Every spawn is uniform, with no mystery versions hiding in the shadows. You don’t hit a failed deploy because someone used a newer Node to regenerate the lockfile while CI is stuck on an outdated runtime. You don’t troubleshoot bugs that only appear for one unlucky teammate. The same container image feeds local dev, build agents, and test harnesses, so everyone rolls the same gear.

In Azure-heavy work, this consistency pays off more than you might think. A machine learning engineer firing up a Jupyter notebook inside a container doesn’t wait for GPU libraries to compile—they’re baked into the image, ready to go. An infra pipeline doesn’t waste cycles pulling Terraform or Bicep every run—it references the pre-built image with those tools already pinned. The work starts at the first task, not an hour later.

Metadata makes pre-building even more powerful. Dev Container images can carry labels that declare configuration, features, and extensions. When your devcontainer.json references that image directly, it inherits those settings automatically. That keeps individual repos clean. Instead of using heavy project-by-project Dockerfiles, you centralize complexity in the image itself and leave your repo configs slim. Update the image once, and multiple teams pick up the change simply by referencing the new tag.

This pattern reduces chaos across a portfolio of projects. You aren’t chasing drift in ten different repos or copy-pasting install scripts everywhere. You’re maintaining a single, authoritative image where the environment rules live. When you bump Python or patch a CLI, it happens in one recipe, and the pipeline rebuilds it everywhere. Troubleshooting narrows down because everyone runs from the same base artifact.

So the real win with pre-building is cutting dead time and removing guesswork. Containers start fast because the heavy lifting is already done. Teams stay in sync because the metadata and dependencies stay locked. Pipelines accelerate because they aren’t babysitting installs. It’s about trading random delays for predictable speed.

But speed alone doesn’t win the campaign. Once your builds come up fast and consistent, you still need strong mechanics to protect the valuables you’re carrying. And nothing undermines a guild faster than sloppy ways of passing keys around.

Securing the Guild Hall

Securing the Guild Hall means keeping your dev environment safe without slowing the party down. In Dev Containers, that starts with Workspace Trust. VS Code won’t just let any folder run unchecked—it prompts you to confirm trust when opening a workspace or attaching to a container. Until you say yes, it runs in restricted mode, which blocks automatic code execution. That guardrail keeps unverified scripts from firing before you’ve had a chance to decide if the folder is safe.

This behavior matters because containers still execute commands. In Azure work, those commands often interact with sensitive pieces like subscription IDs, service principals, or private Git repos. Without guardrails, one careless clone could launch scripts you didn’t audit or pull in dependencies you weren’t expecting. Workspace Trust forces a conscious decision point: only after you grant trust can background tasks and extensions execute, minimizing the chance of silent surprises.

When you attach to an existing container, VS Code asks again—“do you trust this container?” The same applies if you clone a repo into a volume. Restricted mode is the default, and you have to make the call on when to allow execution. It’s a light pause, but it ensures you’re the one setting the boundaries, not the environment itself.

Now let’s get into Git credentials, because pushing and pulling without solid patterns is where real risks appear. The simplest, documented method is mounting your local `~/.ssh` folder into the Dev Container. In a devcontainer.json, you add a `mounts` property and describe it. Spoken, it looks like: “source equals localEnv:HOME/.ssh, target equals /home/vscode/.ssh, type equals bind.” That way, your container reuses the same SSH credentials your host already trusts. Nothing extra copied into the image, no stray keys floating in source control.

If you’d rather keep mounts in a compose file, Docker Compose volumes work too. Both methods ensure your container gets the credentials it needs for GitHub or Azure repos, but only as a mapped resource. The keys never live permanently in the container, so you don’t end up multiplying secrets across machines.

There is a caveat. If your SSH key is guarded by a passphrase, VS Code sync can hang because the agent process doesn’t always align perfectly inside containers. The docs flag this, and there are straightforward workarounds. You can clone over HTTPS. You can fall back on running `git push` and `pull` in a local terminal. Or you can initialize an ssh-agent inside the container at startup and add the key there. Each approach avoids sync freezes without weakening security.

For personal tweaks, dotfiles are the safe play. VS Code supports pointing your Dev Containers extension at a dotfiles repo. Every time a new container spins up, those preferred shell settings, aliases, and prompt configurations copy in. You get a familiar environment without baking secrets into images. It’s personalization layered on top of a secure baseline.

Handled correctly, this stack protects your team while keeping workflows smooth. Workspace Trust controls when code can act. SSH mounts or compose volumes make credential sharing safe and repeatable. Dotfiles bring comfort without exposing sensitive keys. Each step is a guardrail against drift or exposure, but doesn’t grind onboarding to a halt.

For Azure developers, that balance pays out daily. You can push and pull against private repos with confidence, connect to cloud resources without plaintext hacks, and hand new teammates an environment that’s both functional and trustworthy. Security stops being a guess and becomes part of the environment itself.

With the guild hall locked down and credentials managed cleanly, what once felt like a constant threat turns into routine. The groundwork is steady, the protections are in place, and now the real quest—the work you actually came here to do—can progress without panic. From here, it’s clear how the bigger picture comes together.

Conclusion

Dev Containers flip the script: what used to be party wipes—mismatched runtimes, broken pipelines, rogue configs—becomes a team that actually runs the raid together. The stack works cleanly when you standardize with templates, modularize with Features, and pre-build with the Dev Container CLI or CI. Add Workspace Trust and SSH mounts to keep your creds locked down, and you’ve got an environment that rolls the same way for every player, every time.

If this helped you roll a natural 20 on onboarding, subscribe to keep the loot flowing. And before you log off, open VS Code, hit F1, and run “Dev Containers: Add Dev Container Configuration Files...” to try a template yourself.

Discussion about this episode

User's avatar