Skip to main content

Command Palette

Search for a command to run...

Tooling Stack for a Containerized Workflow

Designing a coherent toolchain for Docker dev.

Updated
5 min read
Tooling Stack for a Containerized Workflow
N
Senior-level Fullstack Web Developer with 10+ years experience, including 2 years of Team Lead position. Specializing in responsive design and full-stack web development across the Vue.js and .NET ecosystems. Skilled in Azure/AWS cloud infrastructure, focused on DevOps techniques such as CI/CD. Experienced in system design, especially with software architecture patterns such as microservices, BFF (backend-for-frontend). Hands-on with Agile practices in team leading, and AI-assisted coding.

Series: Containers, Actually: Building Real Local Dev Environments
ACT II — Docker on Windows
Previous: WSL 2 as a First-Class Development Environment
Next: Containerized: Architecture Overview of my Humhub Stack

Most local development pain doesn’t come from Docker itself.
It comes from tooling sprawl.

A terminal here. A package manager there. Some things installed locally, others inside containers. Versions drifting quietly. Scripts half-automated. Eventually, the setup becomes a Frankenstein stack—alive, technically, but fragile and unpleasant to work with.

A containerized workflow only works when tools cooperate, each staying in its proper lane. This article explains how to assemble a tooling stack that feels intentional rather than accidental.


Tools Are Roles, Not Accessories

Before naming tools, it helps to name responsibilities.

Every development tool answers one of three questions:

  1. Where does code run?

  2. Where does code live?

  3. Who orchestrates everything?

Confusion happens when a tool tries to answer all three.

A healthy stack assigns clear roles:

  • Containers execute workloads

  • WSL provides a Linux environment

  • Windows hosts UI and orchestration

  • Editors and package managers bridge human intent to machine execution

With that framing, the stack starts to design itself.


Docker Desktop: The Orchestrator, Not the Star

Docker Desktop is often treated as “Docker itself”. It isn’t.

Docker Desktop’s real job is:

  • Running the Docker daemon

  • Managing Linux integration (via WSL 2)

  • Providing networking, volumes, and lifecycle control

  • Offering a UI for inspection and debugging

It is infrastructure, not a daily interaction surface.

Best practice:

  • Configure Docker Desktop once

  • Let it run quietly

  • Interact with Docker via CLI, not UI

When Docker Desktop is constantly opened, something upstream is unclear.


docker-compose: Declaring Systems, Not Scripts

docker-compose is the backbone of a multi-service local environment.

It is not:

  • A shell script

  • A deployment tool

  • A replacement for documentation

It is:

  • A declarative system map

  • A contract between services

  • A reproducible environment definition

A good docker-compose.yml reads like architecture, not instructions.

Key principles:

  • Services are named by responsibility

  • Networks express communication intent

  • Volumes express persistence

  • Environment variables express configuration

If you can’t explain a compose file without running it, it’s doing too much.


VS Code Remote Extensions: Context Matters

The single most important VS Code concept in containerized workflows is context.

VS Code can run:

  • On Windows

  • Inside WSL

  • Inside a container

The Remote extensions exist to make that explicit.

Remote WSL

Use this when:

  • Editing code that lives in WSL

  • Running Linux-based tools

  • Working with Docker on Windows

This should be the default for containerized workflows on Windows.

Remote Containers

Use this when:

  • You want tooling to run inside a container

  • The container defines the dev environment

  • You need strict parity with CI or production

Most teams overuse Remote Containers early. Remote WSL is usually the better foundation.


Node.js in a Dockerized World

Node.js causes more confusion than almost any other tool in containerized setups.

The key question is:

Is Node part of your application runtime, or just a build tool?

Node as a Runtime

If your app runs on Node:

  • Node belongs in the container

  • The container defines the Node version

  • The host Node version is irrelevant

This guarantees parity and avoids version drift.

Node as a Build Tool

If Node is only used for:

  • Asset compilation

  • Linting

  • Formatting

  • Frontend builds

Then installing Node in WSL is reasonable.

The rule:

  • Runtime Node → container

  • Tooling Node → WSL

Mixing the two without intention leads to subtle version conflicts.


Package Managers: Local Convenience, Global Discipline

Package managers are not just installers. They encode assumptions.

npm / yarn

For JavaScript:

  • Lock files are non-negotiable

  • Global installs should be rare

  • Versions should be pinned per project

When used inside containers, package managers become deterministic. When used globally without version control, they become drift engines.

composer

For PHP:

  • Belongs inside the container when PHP runs there

  • Should not depend on host PHP

  • Benefits enormously from containerized consistency

Composer is happiest when it never sees Windows.


Automation Scripts: Bootstrap Once, Not Always

Automation tools like Chocolatey and setup scripts solve one problem:

“How fast can a new machine become usable?”

They are not runtime dependencies. They are onboarding accelerators.

Use them to:

  • Install Docker Desktop

  • Install WSL

  • Install VS Code

  • Install Windows Terminal

Do not use them to:

  • Manage app dependencies

  • Replace container builds

  • Hide architecture complexity

If automation scripts are required every day, something is mislocated.


Host vs Container: Drawing the Boundary

This boundary is the heart of a sane workflow.

What Belongs on the Host (Windows + WSL)

  • Editors

  • Terminals

  • Git

  • Docker CLI

  • General-purpose tooling

What Belongs in Containers

  • Application runtimes

  • Databases

  • Caches

  • Queues

  • Search engines

  • App-specific dependencies

The host should be stable and boring. Containers should be disposable and precise.


Version Pinning: Time Is the Enemy

Unpinned versions guarantee future pain.

Pin:

  • Node versions (nvm, Docker images)

  • Package manager lock files

  • Docker base images

  • Database versions

Avoid:

  • latest tags

  • Global package installs

  • Undocumented upgrades

Reproducibility is not about perfection. It’s about repeatability under time pressure.


Avoiding the Frankenstein Stack

A Frankenstein stack has recognizable symptoms:

  • “Run this script if it breaks”

  • “It works after restarting everything”

  • “Use this Node version locally, but another in Docker”

  • “Don’t touch that container, it’s fragile”

A coherent toolchain:

  • Has clear boundaries

  • Rebuilds cleanly

  • Explains itself through structure

  • Breaks loudly and predictably

When tools cooperate, complexity becomes manageable instead of mysterious.


Where This Takes Us Next

At this point, we have:

  • A realistic model of modern apps

  • A working understanding of Docker

  • A Windows-friendly Linux foundation

  • A coherent tooling philosophy

Now it’s time to apply all of this to something real.

In the next article, we’ll step into an actual full-stack implementation and examine its architecture—not as a template, but as a case study shaped by these exact constraints.

Tools don’t save you.
Systems do—when they’re designed deliberately.

Containers, Actually: Building Real Local Dev Environments

Part 12 of 18

This series explores full-stack local development with Docker—from core concepts and best practices to a real Windows implementation. The goal is to understand how things run, why they work, and how to build reproducible production environment.

Up next

WSL 2 as a First-Class Development Environment

Turning WSL 2 into a real daily workspace