Docker Containers: The Complete Guide for DevOps Engineers

You’ve heard Docker solves the "works on my machine" problem. But after your first Dockerfile, you realize there’s a lot more to it — images, layers, volumes, networks, multi-stage builds, Compose. This guide covers everything in one place, with links to deeper dives on each topic.

What Docker Actually Does (and What It Doesn’t)

Docker packages your application and its dependencies into a container — an isolated, reproducible unit that runs the same way on any machine with Docker installed.

It’s not a VM. Containers share the host OS kernel. That makes them:

  • Faster to start (seconds, not minutes)
  • Lighter (MBs, not GBs)
  • More portable across environments

What Docker doesn’t solve: it won’t fix a badly architected app, and for a single small service, it may add unnecessary complexity. Don’t containerize just because you can.


Core Concepts

Images vs Containers

Image = read-only blueprint. Think of it as a class in OOP. Container = running instance of an image. Think of it as an object.

# Pull an image from Docker Hub
docker pull nginx:alpine

# Run a container from that image
docker run -d -p 8080:80 nginx:alpine

# List running containers
docker ps

# List all containers (including stopped)
docker ps -aCode language: Bash (bash)

If youre using Compose for multi-service apps, heres the one flag to treat as data-destructive: docker compose down -v. We break down exactly what it removes (and give safe reset recipes) here: Docker Compose Down -v / –volumes.

Layers and the Build Cache

Every instruction in a Dockerfile creates a layer. Docker caches these layers — if nothing changed in a layer, it reuses the cache on next build.

Implication: order matters. Put instructions that change frequently (like COPY . .) at the bottom. Put instructions that change rarely (like apt-get install) near the top.

# Good: dependencies cached separately from source code
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./       # rarely changes → cached
RUN npm install             # cached unless package.json changes
COPY . .                    # changes every build
CMD ["node", "server.js"]Code language: Dockerfile (dockerfile)

Volumes and Persistent Data

Containers are ephemeral. When a container stops, its data disappears — unless you use volumes.

# Named volume (managed by Docker)
docker run -v mydata:/app/data postgres:15

# Bind mount (maps host path to container path)
docker run -v $(pwd)/data:/app/data postgres:15

# List volumes
docker volume ls

# Remove unused volumes
docker volume pruneCode language: Bash (bash)

Use named volumes for databases and persistent data in production. Use bind mounts during development when you want to edit files on the host and see changes immediately in the container.

Networks

By default, containers can’t talk to each other unless they’re on the same network.

# Create a custom network
docker network create myapp-network

# Run containers on the same network
docker run -d --network myapp-network --name db postgres:15
docker run -d --network myapp-network --name api my-api:latest

# Containers on the same network can reach each other by name
# The API container can connect to "db:5432"Code language: Bash (bash)

Dockerfile Essentials

The Dockerfile is where you define your image. Five instructions cover 90% of use cases:

Instruction Purpose
FROM Base image to build on
RUN Execute commands during build
COPY Copy files from host to image
ENV Set environment variables
CMD Default command when container starts

RUN vs CMD vs ENTRYPOINT

This is where most beginners get confused.

  • RUN — executes during build time. Used to install packages, compile code.
  • CMD — default command at run time. Can be overridden.
  • ENTRYPOINT — also at run time, but harder to override. Use when the container has one clear purpose.
# Install packages at build time
RUN apt-get update && apt-get install -y curl

# Default command (easily overridden with: docker run myimage bash)
CMD ["python", "app.py"]

# Or as entrypoint (the container IS this command)
ENTRYPOINT ["python", "app.py"]Code language: Dockerfile (dockerfile)

Rule of thumb: use CMD for flexibility, ENTRYPOINT for dedicated tools.

For the full breakdown, see: Understanding RUN vs CMD vs ENTRYPOINT in Dockerfile.

Multi-Stage Builds

The most underused Docker feature. Build your app in one stage, copy only the output to a smaller final image.

# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production image (no build tools, much smaller)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]Code language: Dockerfile (dockerfile)

Result: production image without compilers, dev dependencies, or source code. Often 5–10x smaller. See the full guide: Understanding Multistage Dockerfiles.

Other Dockerfile Instructions


Essential CLI Commands

# Build an image from current directory
docker build -t myapp:1.0 .

# Build with no cache (force fresh build)
docker build --no-cache -t myapp:1.0 .

# Run container in background, map ports, set env var
docker run -d -p 3000:3000 -e NODE_ENV=production myapp:1.0

# Execute a command inside a running container
docker exec -it <container_id> bash

# View logs
docker logs <container_id>
docker logs -f <container_id>  # follow (live)

# Stop and remove
docker stop <container_id>
docker rm <container_id>

# Remove image
docker rmi myapp:1.0

# Clean up everything unused
docker system prune -aCode language: Bash (bash)

For running specific commands inside containers, see: How to Run Commands Inside Docker Containers.


Docker Compose

For multi-container setups (app + database + cache), Docker Compose defines everything in one YAML file.

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    depends_on:
      - db
    networks:
      - app-network

  db:
    image: postgres:15-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=pass
      - POSTGRES_USER=user
      - POSTGRES_DB=mydb
    networks:
      - app-network

volumes:
  pgdata:

networks:
  app-network:Code language: YAML (yaml)
# Start all services
docker compose up -d

# View logs for all services
docker compose logs -f

# Stop and remove containers (keeps volumes)
docker compose down

# Stop and remove everything including volumes
docker compose down -vCode language: Bash (bash)

Docker Best Practices

Keep Images Small

  • Use Alpine-based images (node:18-alpine instead of node:18)
  • Use multi-stage builds
  • Add a .dockerignore file to exclude node_modules, .git, logs
# .dockerignore
node_modules
.git
*.log
.env
distCode language: CSS (css)

One Process Per Container

Don’t run nginx + app + database in one container. Separate concerns. If a process crashes, you want Docker to restart just that container, not everything.

Never Store Secrets in Images

Don’t hardcode passwords in Dockerfiles or commit .env files with secrets. Use Docker secrets, environment variables at runtime, or a secrets manager.

# Pass secrets at runtime, not in the image
docker run -e DB_PASSWORD=$DB_PASSWORD myapp:1.0Code language: Bash (bash)

Use Specific Image Tags

# Bad: unpredictable, breaks builds when "latest" updates
FROM node:latest

# Good: reproducible builds
FROM node:18.20.4-alpine3.19Code language: Dockerfile (dockerfile)

Common Mistakes

1. Running as root inside containers By default, processes run as root. Add a non-root user:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuserCode language: Dockerfile (dockerfile)

2. Not using .dockerignore Copying node_modules into the build context slows builds dramatically and increases image size.

3. Installing packages without cleaning up

# Bad: leaves apt cache in the layer
RUN apt-get update && apt-get install -y curl

# Good: clean up in the same RUN instruction
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*Code language: Dockerfile (dockerfile)

4. Using CMD for tasks that must succeed If your entrypoint script fails silently, the container keeps running. Use proper error handling.

5. Not tagging images properly Relying on :latest in production makes rollbacks hard. Always tag with version or git commit hash.


Troubleshooting

# Why is the container exiting immediately?
docker logs <container_id>

# Inspect container configuration
docker inspect <container_id>

# Check resource usage
docker stats

# Get a shell in a running container
docker exec -it <container_id> sh

# Run a shell in a stopped/failing container (override CMD)
docker run -it --entrypoint sh myapp:1.0Code language: Bash (bash)

For the Docker manifest error specifically, see: How to Fix the Docker Manifest Unknown Error.


FAQ

Q: What’s the difference between Docker and a virtual machine? A: VMs virtualize hardware and run a full OS. Docker containers share the host OS kernel, making them much lighter and faster to start. A VM might take minutes and use GBs of RAM; a Docker container starts in seconds and uses MBs.

Q: When should I use Docker Compose vs Kubernetes? A: Docker Compose is for local development and simple single-host deployments. Kubernetes is for production multi-node orchestration — auto-scaling, self-healing, rolling deployments. Don’t use Kubernetes for a 2-service app; don’t use Compose when you need to scale across multiple machines.

Q: Can Docker containers communicate with each other? A: Yes, when they’re on the same Docker network. Containers reference each other by service name. In Docker Compose, all services are automatically on a shared network.

Q: How do I persist database data in Docker? A: Use named volumes. Mount the database data directory to a volume — Docker manages it and it survives container restarts and removals (unless you explicitly delete the volume with docker volume rm).

Q: Why is my Docker image so large? A: Common causes: using a full OS base image instead of Alpine, not using multi-stage builds, copying node_modules or build artifacts into the final image, not cleaning up package manager caches in RUN instructions.

# Syntax highlighting test block
docker build -t myapp:latest .
docker run -d -p 8080:80 --name mycontainer myapp:latest
docker logs -f mycontainerCode language: Bash (bash)

Related Developer Tools

Working with code often involves formatting text for documentation. TitleCaseConverter.online – a free tool for converting text to title case, sentence case, camelCase, and more.

Nathan Cole Avatar

Leave a Reply

Your email address will not be published. Required fields are marked *