Stop Using Package Managers, Use Containers

Package managers are broken. You know it, I know it, and yet we keep pretending that npm install, pip install, and bundle install are acceptable solutions for managing dependencies in 2025.

The Package Manager Problem

Every developer has been here: you clone a project, run the install command, and immediately hit version conflicts. Python’s virtual environments don’t save you from system library conflicts. Node’s node_modules folder becomes a black hole of disk space. Ruby gems conflict with system libraries. Go modules work until you need CGO.

The fundamental issue isn’t with specific package managers. It’s with the entire concept of installing dependencies directly on your development machine.

Python’s False Solution

Python’s solution to dependency hell was virtual environments. Create isolated spaces for each project’s dependencies. Sounds great in theory. In practice, you’re still sharing system libraries, dealing with different Python versions, and managing virtual environment activation across multiple projects.

# The Python developer's daily ritual
cd project-a
source venv/bin/activate
# Work on project A
deactivate

cd ../project-b  
source venv/bin/activate
# Work on project B
# Oh wait, now I need a different Python version

Virtual environments are a band-aid on a broken system.

Node.js: The Disk Space Apocalypse

Node.js doesn’t even pretend to solve the problem. Every project gets its own node_modules directory containing hundreds of megabytes of dependencies. Your 500GB SSD fills up with duplicate copies of lodash and left-pad.

# This should not be normal
$ du -sh node_modules/
847M    node_modules/
$ find node_modules -name "lodash" | wc -l
47

Ruby’s System Integration Nightmare

Ruby gems love to compile native extensions and integrate deeply with your system. Everything works fine until you upgrade macOS and suddenly nothing compiles. Or you need different versions of the same gem for different projects.

Containers: The Actual Solution

Containers solve dependency management by eliminating the host system from the equation entirely. Instead of installing Python, Node, Ruby, and their associated package managers on your development machine, you define the exact environment your code needs and run it in isolation.

Language-Agnostic Development

With containers, your development workflow becomes consistent regardless of language:

# Python project
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# Node.js project  
FROM node:18-alpine
WORKDIR /app
COPY package*.json .
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
# Ruby project
FROM ruby:3.2-alpine
WORKDIR /app
COPY Gemfile* .
RUN bundle install --deployment --without development test
COPY . .
CMD ["ruby", "app.rb"]

The development workflow is identical: docker build, docker run. No language-specific toolchain installation, no version management, no dependency conflicts.

Real Isolation

Unlike virtual environments, containers provide actual isolation. Each container has its own filesystem, network namespace, and process space. Dependencies in one container cannot conflict with dependencies in another.

# Run multiple projects simultaneously
docker run -p 3000:3000 project-a
docker run -p 3001:3001 project-b  
docker run -p 3002:3002 project-c

No deactivation rituals. No PATH management. No system library conflicts.

Reproducible Environments

Package managers promise reproducible builds but fail to deliver. Lock files help, but they don’t capture system dependencies, OS differences, or library versions. Containers capture the entire environment.

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
    depends_on:
      - database
      
  database:
    image: postgres:15
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: developer
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

This works identically on Linux, macOS, and Windows. New team members can be productive in minutes, not hours.

Practical Implementation

Development Containers

Use development containers with volume mounts for live code editing:

# Mount source code for live editing
docker run -it --rm \
  -v $(pwd):/app \
  -w /app \
  -p 3000:3000 \
  python:3.11 \
  bash

Your IDE works with files on the host system, but execution happens in the container.

Docker Aliases

Create shell aliases to hide container complexity:

# ~/.bashrc or ~/.zshrc
alias python='docker run -it --rm -v $(pwd):/app -w /app python:3.11 python'
alias node='docker run -it --rm -v $(pwd):/app -w /app node:18 node'  
alias ruby='docker run -it --rm -v $(pwd):/app -w /app ruby:3.2 ruby'

Now python script.py works exactly as expected, but uses the containerized Python.

Build Tools Integration

Modern IDEs support development containers out of the box. VS Code’s Dev Containers extension automatically detects Docker configurations and runs your development environment in containers.

Why This Matters

Package managers create artificial complexity. They force you to think about Python versions, Node versions, Ruby versions, virtual environments, and dependency conflicts instead of focusing on your actual problem.

Containers reduce cognitive overhead. You define your environment once in a Dockerfile and forget about it. New team members don’t spend their first day installing toolchains, they run docker-compose up and start coding.

This isn’t about being trendy or following the latest DevOps fad. This is about choosing tools that solve problems instead of creating them.

Stop fighting with package managers. Use containers.