This guide is written for software engineers who want to understand Docker deeply and also use it correctly in production.
No buzzwords. Just what actually matters.
Docker does NOT create a virtual machine.
It simply runs your application as a:
Normal Linux process with isolation + custom filesystem
Thatβs it.
- Image β Read-only snapshot of your app + environment
- Container β Running process from that image
- Dockerfile β Instructions to build that snapshot
An image is a stack of layers:
Example:
Base OS (Alpine Linux)
+ Node.js installed
+ Dependencies installed
+ Your source code
Each step in Dockerfile = new layer.
Images are:
- Immutable (cannot change after build)
- Reproducible (same image = same behavior)
A container is:
A running process using that image
When it starts:
- Gets isolated filesystem
- Gets limited resources (CPU/RAM)
- Runs your app
If the process stops β container stops
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8080
CMD ["npm", "start"]Base filesystem + Node runtime (You are NOT creating an OS from scratch)
Sets working directory inside container
Copies only dependency files
Installs dependencies inside the image
π Important:
- Happens during build
- Creates
node_modulesinside image - NOT using your local
node_modules
Copies your source code
Runs when container starts
π Difference:
RUNβ build timeCMDβ runtime
Example:
node_modules
.git
.env
dist
Why?
- Prevents copying unnecessary files
- Avoids OS mismatch issues
- Keeps image small
- Protects secrets
docker build -t my-app .docker run -p 8080:8080 my-appNow your app runs on:
http://localhost:8080
- No separate OS
- Faster than VMs
- Same host kernel is used
- All containers use host RAM
- You can limit it:
docker run -m 512m my-app- Image = read-only layers
- Container = adds writable layer
- Changes disappear when container is deleted
- Data is lost unless stored externally
- Use volumes for persistence
Example:
docker run -v mydata:/app/data my-appNow data survives container restarts.
- Each container gets its own internal IP
- Docker creates a virtual network
Port mapping:
docker run -p 8080:8080 my-appMeans:
Host:8080 β Container:8080
Used when you have:
- Backend
- Database
- Redis
Example:
version: "3"
services:
app:
build: .
ports:
- "8080:8080"
db:
image: postgresRun everything:
docker-compose upNever install dependencies at runtime.
β Good:
RUN npm installβ Bad:
CMD npm install && npm startDO NOT hardcode secrets.
Example:
docker run -e DB_URL=xyz my-appIn code:
process.env.DB_URLNever include .env in image.
Use runtime injection:
- Docker run
- Docker Compose
- Cloud environment variables
Use lightweight base images:
node:alpineReduces final image size.
Use:
- Volumes
- External DB
Use:
CMD ["node", "server.js"]Instead of:
CMD npm startWhy:
- Proper shutdown handling
- Better process control
(Advanced but important)
- Build image
- Push to registry
- Pull on server
- Run container
Example:
docker build -t my-app .
docker push my-appOn server:
docker pull my-app
docker run -d -p 80:8080 my-app- Docker = Process isolation + filesystem snapshot
- Image = Blueprint
- Container = Running instance
- Build phase = prepare everything
- Run phase = just execute
If you follow this:
- Your app will run consistently everywhere
- No βworks on my machineβ issues
- Production deployments become predictable