Docker & Containers: Stop "It Works on My Machine" Once and For All
Code works on your machine but breaks in production. Docker solves this by packaging your entire app – code, dependencies, runtime – into portable containers that work everywhere. Learn Dockerfile basics, Docker Compose for multi-service setups, best practices, and real-world examples. Build once, run anywhere. From development to production with confidence. Includes full-stack example with frontend, backend, and database.
Docker & Containers: Stop "But It Works on My Machine" Once and For All
You've been there. Code works perfectly on your laptop. You ship it to production. It breaks. The ops team says "it works fine on our servers." Hours of debugging later, someone mentions Python version mismatch.
Enter Docker. It's the magic spell that makes code work everywhere.
🎯 The Problem Docker Solves
Imagine you're shipping a physical product. Without Docker, it's like:
Without Docker:
- "Here's my code"
- "What Python version?"
- "Uh... 3.9?"
- "We have 3.10"
- "That's not compatible!"
- debugging intensifies
With Docker:
- "Here's my code... in a container with Python 3.9, all dependencies, everything"
- "It runs"
- done
Docker packages your entire application – code, dependencies, runtime, everything – into a portable box that works on any machine.
📦 What Is a Container?
A container is a lightweight, standalone package that includes everything your app needs to run.
Think of it like a shipping container:
- Your code is the cargo
- Dependencies are packing materials
- Docker is the container itself
- It works the same whether it's in Shanghai or New York
Key difference from VMs:
- VM: Entire operating system + your app = 2GB+ each
- Container: Just your app + minimal OS files = 100MB
Containers are faster, lighter, and more practical.
🐳 Docker Basics
Images vs Containers
Image: Blueprint (like a class in OOP) Container: Running instance (like an object)
1
Dockerfile → Image → Container
Your First Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Start from Python 3.9 image
FROM python:3.9
# Set working directory
WORKDIR /app
# Copy your code
COPY . .
# Install dependencies
RUN pip install -r requirements.txt
# Run the app
CMD ["python", "app.py"]
This says:
- Start with Python 3.9
- Create /app directory
- Copy my code there
- Install my Python packages
- When container starts, run
python app.py
Building an Image
1
2
# Build image named "my-app" with tag "v1"
docker build -t my-app:v1 .
Running a Container
1
2
# Run the image, map port 8000 on host to 5000 in container
docker run -p 8000:5000 my-app:v1
Now your app is accessible at
localhost:8000.🔧 Practical Example: Node.js App
app.js:
1
2
3
4
5
6
7
8
9
10
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello from Docker!' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
package.json:
1
2
3
4
5
6
7
8
9
10
{
"name": "docker-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.0"
},
"scripts": {
"start": "node app.js"
}
}
Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Build and run:
1
2
docker build -t my-node-app .
docker run -p 3000:3000 my-node-app
Visit
localhost:3000 and boom – your app works!🚀 Docker Compose: Multiple Containers
Real apps need multiple services: app, database, cache, etc.
Docker Compose runs them all together.
docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://db:5432/myapp
depends_on:
- db
db:
image: postgres:14
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=myapp
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Run everything:
1
docker-compose up
This starts:
- PostgreSQL database
- Your Node.js app
- Links them together
- Exposes ports
No manual installation, no environment setup. Just works.
📝 Best Practices
1. Use Specific Base Images
Bad:
1
FROM node # What version? Latest? Could break
Good:
1
FROM node:18.17-alpine # Specific version, small image
2. Multi-Stage Builds (Smaller Images)
1
2
3
4
5
6
7
8
9
10
11
12
13
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
# Runtime stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package.json .
RUN npm install --production
CMD ["npm", "start"]
This reduces image size because build tools aren't included in final image.
3. Don't Run as Root
1
2
3
4
5
6
7
8
# Create a user
RUN addgroup --system appgroup && adduser --system appuser -G appgroup
# Switch to that user
USER appuser
# Run app as appuser (safer)
CMD ["npm", "start"]
4. Use .dockerignore
1
2
3
4
5
node_modules/
.git
.gitignore
README.md
.env
Don't copy unnecessary files into image.
5. Health Checks
1
2
3
4
5
6
7
8
9
10
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
CMD node healthcheck.js
CMD ["npm", "start"]
Docker automatically restarts unhealthy containers.
🐛 Common Docker Issues
Issue 1: "Port Already in Use"
1
2
3
4
5
# Kill the process using that port
docker kill $(docker ps -q --filter ancestor=my-app)
# Or use a different port
docker run -p 8001:3000 my-app
Issue 2: "Can't Connect to Database"
Services need to use container names, not localhost.
Wrong:
1
const db = new Database('localhost:5432');
Right (in docker-compose):
1
const db = new Database('db:5432'); // 'db' is the service name
Issue 3: "Changes Not Showing Up"
You need to rebuild the image after code changes.
1
2
docker build -t my-app .
docker run -p 3000:3000 my-app
Or use volumes for development:
1
docker run -v $(pwd):/app -p 3000:3000 my-app
Issue 4: Image Too Large
Use multi-stage builds and Alpine images.
1
2
3
4
5
# Bad: 900MB
FROM ubuntu:22.04
# Good: 150MB
FROM node:18-alpine
📊 Real-World Example: Full Stack App
docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- REACT_APP_API_URL=http://localhost:5000
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "5000:5000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/myapp
- NODE_ENV=development
depends_on:
- db
volumes:
- ./backend:/app
db:
image: postgres:14
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
One command runs everything:
1
docker-compose up
Frontend on 3000, backend on 5000, database on 5432. Done.
🎯 Docker Workflow
Development
1
2
3
docker-compose up -d # Start services in background
docker-compose logs -f # Watch logs
docker-compose down # Stop services
Testing
1
2
docker build -t my-app:test .
docker run my-app:test npm test
Production
1
2
3
docker build -t my-app:v1.0.0 .
docker tag my-app:v1.0.0 myregistry.azurecr.io/my-app:v1.0.0
docker push myregistry.azurecr.io/my-app:v1.0.0
Push to registry (Docker Hub, AWS ECR, Azure ACR) and deploy from there.
🚀 From Container to Production
Once you have an image, deploying is easy:
AWS ECS:
1
2
3
4
5
6
7
# Push image to ECR
aws ecr get-login-password | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
docker tag my-app:v1 $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/my-app:v1
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/my-app:v1
# Deploy
aws ecs create-service --cluster production --service-name my-app --task-definition my-app:1 --desired-count 3
Kubernetes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:v1
ports:
- containerPort: 3000
✅ Docker Checklist
Before shipping to production:
- [ ] Dockerfile builds without errors
- [ ] App runs correctly in container
- [ ] All services defined in docker-compose
- [ ] Environment variables properly set
- [ ] Volumes for persistent data
- [ ] Health checks implemented
- [ ] Image size optimized
- [ ] Security best practices followed
- [ ] Tested locally with docker-compose
- [ ] Ready to push to registry
🎓 Key Takeaways
Docker solves the "works on my machine" problem by:
✅ Packaging everything consistently ✅ Making deployment predictable ✅ Enabling easy local development ✅ Supporting scaling with orchestration tools ✅ Making CI/CD pipelines reliable
From "I don't know why it doesn't work" to "It works everywhere."
Wrap-up
Docker isn't magic, it's just consistency. Once you containerize your app:
- Your local machine ≈ staging ≈ production
- Onboarding new developers is instant (
docker-compose up) - Deploying is reliable (same image everywhere)
- Scaling is manageable (run multiple containers)
The time you invest learning Docker pays back immediately.
Start with a simple Dockerfile, move to docker-compose when you have multiple services, and scale to Kubernetes when needed.
Your ops team will love you. Your users will get reliable deployments. Your sanity will improve.
P.S.: The first time you run a container and it works perfectly without any environment setup, you'll understand why developers are obsessed with Docker.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article