Skip to main content

Command Palette

Search for a command to run...

Containerized: Lessons Learned & What I’d Do Differently

What worked, what didn’t, and why it matters

Updated
6 min read
Containerized: Lessons Learned & What I’d Do Differently
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 III — Real Implementation: My Humhub Stack
Previous: Performance, Stability, and Resource Management of my Containerized Stack

Disclaimer (NDA Notice)
This article reflects real experience from a long-running, production-adjacent project.
Due to NDA constraints, some internal decisions, metrics, service counts, and tooling details are generalized or anonymized.

The lessons, failure modes, trade-offs, and conclusions are real.
Treat this as distilled experience, not a post-mortem of a specific company system.

This is the article that most Docker guides never write.

Not because the authors don’t have opinions—but because opinions require time, mistakes, reversals, and humility. By the time you can write this article honestly, you no longer believe there is one right setup.

What follows is not a victory lap.
It’s a calibration.


The Biggest Unexpected Pain Points

Some problems were obvious in hindsight. Others only revealed themselves after weeks of daily use.

1. Filesystem Performance Was the Real Bottleneck

Not Docker.
Not PHP.
Not MySQL.

The filesystem.

Early on, we underestimated how many performance issues would trace back to where code lived, not how it ran. Any accidental regression into /mnt/c brought:

  • Sluggish hot reloads

  • High CPU usage

  • Flaky file watchers

  • “Docker is slow” complaints

No amount of container tuning fixed that.

Lesson:
Filesystem placement is architecture, not a detail.

2. Logs Become a Problem Before You Expect

Log growth felt harmless—until it wasn’t.

Containers that ran for weeks accumulated:

  • Massive JSON log files

  • Slow disk operations

  • Increased Docker metadata overhead

The fix was simple, but late:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

Lesson:
Logs are unbounded by default. Production discipline matters locally too.

3. Background Workers Fail Quietly

Queues don’t scream when something is wrong. They whisper.

We hit cases where:

  • Workers were running but stalled

  • Jobs were retrying forever

  • Queues slowly backed up

The system “worked”, but user-facing behavior degraded subtly.

The fix was explicit limits:

php artisan queue:work --tries=3 --timeout=90

Lesson:
Anything asynchronous must be bounded, or it will rot invisibly.


Trade-Offs That Weren’t Obvious at the Start

Some decisions only reveal their cost later.

Convenience vs Correctness Is Not Binary

Early on, we assumed:

  • “Correct” setups are painful

  • “Convenient” setups are fragile

Reality was more nuanced.

Some conveniences paid off:

  • Bind mounts for app code

  • Local .env overrides

  • Lightweight tooling in WSL

Others caused compounding pain:

  • Shared “god” containers

  • Host-installed runtimes

  • Magic scripts that hid complexity

Lesson:
Convenience is fine—when it doesn’t hide system boundaries.

Structural Parity Beats Exact Parity

We initially aimed for near-perfect production parity.

That turned out to be unnecessary.

What mattered was:

  • Same service boundaries

  • Same runtime versions

  • Same communication patterns

  • Same failure modes

What didn’t matter locally:

  • Scale

  • Security hardening

  • Observability depth

Lesson:
Structural parity buys confidence. Exact parity buys complexity.


What Scaled Well

Some choices paid dividends as the project grew.

Clear Service Separation

One concern per container wasn’t dogma—it was leverage.

Benefits:

  • Faster debugging

  • Safer rebuilds

  • Clear ownership

  • Predictable restarts

docker-compose files stayed readable even as services increased.

Treating WSL as a First-Class OS

Once the team stopped treating WSL as “that Docker thing” and started treating it as the dev OS, friction dropped sharply.

Developers:

  • Installed tools confidently

  • Knew where commands belonged

  • Stopped fighting Windows/Linux boundaries

Lesson:
Ambiguity is more expensive than learning curves.

Routine Rebuilds and Resets

Rebuilds stopped being emergencies once they became routine.

docker-compose build app
docker-compose up -d app

Resetting state became intentional instead of terrifying.

docker-compose down -v

Lesson:
If rebuilding feels scary, the system is already broken.


What Didn’t Scale Well

Some patterns aged poorly.

Overloading .env

At first, environment variables felt like a clean abstraction.

Over time:

  • .env files became overloaded

  • Architecture decisions leaked into variables

  • Behavior became implicit instead of visible

Lesson:
Environment variables configure values—not structure.

“Helpful” Automation Scripts

Bootstrap scripts were great for onboarding.
They were terrible as long-term dependencies.

When scripts:

  • Hid Docker errors

  • Masked version mismatches

  • Replaced documentation

They became liabilities.

Lesson:
Automation should accelerate understanding—not replace it.


Comparison: This Setup vs Alternatives

No system exists in a vacuum. Here’s how this approach compares honestly.

DevContainers (VS Code)

Pros

  • Strong onboarding

  • Editor-integrated

  • Reproducible tooling

Cons

  • Editor lock-in

  • Hidden Docker complexity

  • Harder to debug outside VS Code

Verdict
Excellent for small teams or standardized stacks. Less flexible for heterogeneous systems.

Tilt

Pros

  • Powerful live reload

  • Kubernetes-native workflows

  • Great for microservices

Cons

  • Steep learning curve

  • Heavy mental overhead

  • Overkill for many projects

Verdict
Fantastic for Kubernetes-first teams. Too heavy for most local dev stacks.

Local Native Installs

Pros

  • Fast startup

  • Familiar tooling

  • Minimal abstraction

Cons

  • Environment drift

  • Onboarding pain

  • OS-specific bugs

Verdict
Fine for solo work. Fails under team scale.

This Docker + WSL Approach

Pros

  • Clear system boundaries

  • Windows-friendly

  • Reproducible

  • Debuggable

Cons

  • Requires discipline

  • Requires learning Docker properly

  • Slower initial setup

Verdict
Best when correctness, parity, and long-term stability matter.


What I’d Do Differently Next Time

With hindsight:

  • Introduce log limits earlier

  • Document filesystem rules more aggressively

  • Set resource limits from day one

  • Add health checks sooner

  • Treat queues as first-class citizens earlier

None of these invalidate the approach. They refine it.


The Real Lesson

Docker was never the point.

The point was:

  • Making systems legible

  • Making failures diagnosable

  • Making environments boring

When local development feels boring, predictable, and slightly unremarkable—you’ve succeeded.


Closing the Series

This series wasn’t about commands.
It was about thinking in systems.

If you take one thing away, let it be this:

Good development environments don’t remove complexity.
They put it where you can see it—and control it.

That’s not magic.
That’s engineering.

🌟 Optional Extensions

With the core stack in place, the remaining topics aren’t about getting things to run—they’re about refinement and reach. The following optional extensions explore how this foundation can be adapted for faster onboarding, tighter CI/CD alignment, multiple environments, stronger local security, and even a conceptual path toward Kubernetes. None are required, but each shows how a solid local setup becomes a platform rather than a one-off solution.

Containers, Actually: Building Real Local Dev Environments

Part 6 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

Performance, Stability, and Resource Management of my Containerized Stack

Keeping Docker dev environments fast over time