AWS Builder Center

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:
  1. Start with Python 3.9
  2. Create /app directory
  3. Copy my code there
  4. Install my Python packages
  5. 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:
  1. PostgreSQL database
  2. Your Node.js app
  3. Links them together
  4. 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article