Draft / Scheduled Content
This article is a draft or scheduled for future publication. The content is subject to change.
Your Local Development Environment Should Not Run on Docker
The Containerized Default
If you join a new engineering team today, the onboarding documentation will likely start with:
- Clone the repository.
- Install Docker.
- Run
docker-compose up. - Wait 15 minutes for the images to build and start.
This is presented as the holy grail of onboarding. We are told: “It guarantees everyone has the exact same environment! No more ‘it works on my machine’ bugs! Development is identical to production!”
It sounds wonderful. But the reality is that containerizing your local application code—mounting your working directory into a container, running your compilers and test runners inside that container, and accessing it through mapped ports—is a massive developer velocity bottleneck.
Docker is a deployment technology. Using it as a local development environment is a mistake.
The Performance Tax (Especially on macOS and Windows)
If you are running Linux natively, Docker’s performance overhead is minimal because containers share the host kernel.
But the vast majority of developers work on macOS or Windows.
On these platforms, Docker cannot run natively. It runs inside a virtual machine (VM). This VM must be allocated a fixed chunk of your host RAM, CPU, and disk space. If you assign 8GB of RAM to Docker, that is 8GB your system cannot use for your IDE, browser, or other tools.
The biggest bottleneck, however, is file system synchronization.
When you write code locally, your IDE changes files on your host machine. Docker must synchronize these changes into the VM, and then into the container, so your development server (like Vite, Webpack, or Django) can hot-reload.
For modern codebases with thousands of files (hello, node_modules or venv), this file synchronization is incredibly slow.
- A cold startup that takes 2 seconds natively takes 30 seconds in Docker.
- A hot-reload that is instantaneous natively takes 3-5 seconds in Docker.
- Running your unit tests takes 5 times longer because the container must boot, load dependencies across the virtualized file system, and execute.
A developer runs their tests dozens of times a day. If your test suite takes 30 seconds instead of 5, they run it less often. They write more bugs, wait longer for feedback, and lose their flow.
The Debugging and Tooling Friction
When your code runs inside a container, it is isolated. This isolation makes debugging significantly harder.
Want to attach a debugger from VS Code or IntelliJ? You have to configure remote debugging, expose debugging ports (9229, 5678), set up path mappings in your editor configuration, and pray that the firewall doesn’t block it.
Want to run a quick script or profile your application’s CPU usage? You can’t just run node profile.js or go tool pprof. You have to:
docker exec -it [container_id] sh- Install the profiling tools inside the container (if you have permission, and if the container isn’t running a stripped-down Alpine image).
- Run the tool, export the profile data, and copy it back to your host machine.
You have built a wall between your editor and your runtime. You are working through a peephole.
The “Works in Production” Illusion
The main argument for Docker is that it eliminates environmental differences. “If it runs in Docker locally, it will run in Kubernetes in production.”
This is a lie.
Local development Docker configurations are completely different from production:
- You mount host directories (
-v .:/app) locally; in production, you copy the build artifacts into a static image. - You run dev servers with debug configurations; in production, you run compiled binaries or optimized runtimes.
- You run databases inside the cluster; in production, you use managed cloud databases (RDS, Cloud SQL) with completely different connection limits, SSL settings, and latency profiles.
You have not replicated production. You have just built a third environment—the local Docker environment—which has its own unique, frustrating bugs.
+-------------------------------------------------------------+
| The Onboarding Lie |
| |
| "Just run docker compose up!" |
| |
| *30 minutes later* |
| Error: Mount path does not exist. |
| Error: M1 Mac architecture mismatch for dependency X. |
| Error: Port 5432 is already in use by local Postgres. |
| |
| Result: Developer spends their first week debugging |
| Docker network routes instead of writing code. |
+-------------------------------------------------------------+
The Hybrid Approach: A Better Way
To be clear: you should not run everything natively.
Installing PostgreSQL, Redis, Elasticsearch, and RabbitMQ natively on your local machine is indeed a nightmare. They clutter your system, run background daemons that eat battery, and have conflicting version requirements.
The solution is the Hybrid Environment:
- Run databases and external dependencies in Docker: Use Docker Compose to spin up your database, cache, and queue. These are static dependencies. You don’t edit their code, so they don’t suffer from file mount synchronization issues.
- Run your application code natively: Run your server, your frontend dev server, and your test runners directly on your host machine. Connect them to the containerized databases via localhost (
localhost:5432).
Here is a simple example. You start your database using Docker:
docker run --name local-db -p 5432:5432 -e POSTGRES_PASSWORD=secret -d postgres
And then you run your Node.js application natively:
npm run dev
Your app is incredibly fast because it is running directly on your CPU. Hot-reloading takes milliseconds. Your IDE integration is perfect. Debugging is a single click. But your database is still safely sandboxed in a container.
Keep It Native
The most valuable asset an engineering team has is developer focus.
Do not trade your team’s daily iteration speed for the minor, one-time convenience of onboarding. If your application can run natively, run it natively. Reserve Docker for your external dependencies and your production CI/CD pipelines.
Make local development fast again.
Related Content
No, You Don't Need a Service Mesh
Service meshes like Istio or Linkerd promise advanced traffic management, mutual TLS, and observability for microservices. In reality, they add massive networking complexity, consume significant CPU and RAM, and make debugging network issues a nightmare. Keep your cluster networking simple.
Why ORMs are the Worst Anti-Pattern in Modern Backend Development
Object-Relational Mapping (ORM) tools promise to free developers from SQL. In exchange, they introduce the N+1 query problem, hide database performance realities behind black-box abstractions, and create models that couple application state to database schemas. It's time to write SQL again.
Kubernetes is the Ultimate Developer Money Pit for Startups
Startups are adopting Kubernetes because Big Tech does. In doing so, they inherit massive complexity, exorbitant cloud bills, and dedicated infrastructure team requirements before they even have product-market fit. You probably just need a single VPS.