04 - Docker Containers

What Is a Container?

A container is a running instance of an image. Think of it like this:

  • Image = class definition
  • Container = object instance

You can run multiple containers from the same image, each isolated.

Container Lifecycle

        docker create         docker start
Image ──────────────► Created ─────────────► Running
                         │                      │
                         │     docker stop       │
                         │  ◄───────────────────┘
                         │         │
                         │    docker start
                         │  ►───────────────────┐
                         │                      │
                         ▼                      ▼
                      Stopped ◄──────────── Running
                         │                      │
                    docker rm              docker kill
                         │                      │
                         ▼                      ▼
                      Deleted              Stopped → Deleted

States:

  • Created: Container exists but hasn't started
  • Running: Main process is active
  • Paused: Process frozen (SIGSTOP)
  • Stopped: Process exited (exit code available)
  • Deleted: Container removed from disk

Essential Container Commands

Creating and Running

bash
# Run a container (create + start) docker run nginx # Run detached (background) docker run -d nginx # Run with a name docker run -d --name webserver nginx # Run interactive (terminal attached) docker run -it ubuntu bash # Run and auto-remove when stopped docker run --rm -it alpine sh # Run with port mapping docker run -d -p 8080:80 nginx # Host port 8080 → Container port 80 # Run with multiple port mappings docker run -d -p 8080:80 -p 8443:443 nginx # Run with environment variables docker run -d -e MYSQL_ROOT_PASSWORD=secret mysql:8 # Run with resource limits docker run -d --memory=512m --cpus=1.5 nginx # Run with restart policy docker run -d --restart=unless-stopped nginx

Inspecting Containers

bash
# List running containers docker ps # List ALL containers (including stopped) docker ps -a # Show only container IDs docker ps -q # Detailed container info docker inspect mycontainer # Get specific fields docker inspect --format='{{.State.Status}}' mycontainer docker inspect --format='{{.NetworkSettings.IPAddress}}' mycontainer # See container logs docker logs mycontainer docker logs -f mycontainer # Follow (tail -f) docker logs --tail 100 mycontainer # Last 100 lines docker logs --since 1h mycontainer # Last hour # See resource usage (live) docker stats docker stats mycontainer # See running processes inside container docker top mycontainer # See port mappings docker port mycontainer # See filesystem changes vs the image docker diff mycontainer

Interacting with Running Containers

bash
# Execute a command in a running container docker exec mycontainer ls /app # Get an interactive shell docker exec -it mycontainer bash docker exec -it mycontainer sh # Alpine doesn't have bash # Execute as a specific user docker exec -u root mycontainer whoami # Copy files between host and container docker cp mycontainer:/app/logs ./logs # Container → Host docker cp ./config.json mycontainer:/app/ # Host → Container # Attach to container's main process (stdin/stdout) docker attach mycontainer # Ctrl+P, Ctrl+Q to detach without stopping

Stopping and Removing

bash
# Stop gracefully (SIGTERM, then SIGKILL after 10s) docker stop mycontainer # Stop with custom timeout docker stop -t 30 mycontainer # Kill immediately (SIGKILL) docker kill mycontainer # Remove a stopped container docker rm mycontainer # Force remove a running container docker rm -f mycontainer # Remove all stopped containers docker container prune # Stop and remove ALL containers docker stop $(docker ps -q) docker rm $(docker ps -aq)

Port Mapping In Depth

Host Machine
┌──────────────────────────────────────┐
│                                      │
│  Browser → localhost:8080            │
│              │                       │
│              ▼                       │
│  ┌─── Port Mapping (iptables) ───┐   │
│  │    8080 → 172.17.0.2:80       │   │
│  └───────────────────────────────┘   │
│              │                       │
│  ┌───────── Container ──────────┐    │
│  │  nginx listening on :80      │    │
│  │  IP: 172.17.0.2              │    │
│  └──────────────────────────────┘    │
└──────────────────────────────────────┘
bash
# Syntax: -p [host_ip:]host_port:container_port[/protocol] # Map to all interfaces docker run -d -p 8080:80 nginx # Map to specific interface (localhost only) docker run -d -p 127.0.0.1:8080:80 nginx # Map UDP port docker run -d -p 5000:5000/udp myapp # Random host port (Docker picks one) docker run -d -p 80 nginx docker port <container> # See which port was assigned # Map all EXPOSE'd ports to random host ports docker run -d -P nginx

Environment Variables

bash
# Single variable docker run -e DATABASE_URL=postgres://db:5432/mydb myapp # From a file # .env file: # DATABASE_URL=postgres://db:5432/mydb # REDIS_URL=redis://cache:6379 # SECRET_KEY=mysecret docker run --env-file .env myapp # Pass host environment variable export API_KEY=abc123 docker run -e API_KEY myapp # Passes host's API_KEY value

Restart Policies

PolicyBehavior
noNever restart (default)
on-failure[:max]Restart only on non-zero exit code
alwaysAlways restart, even on manual stop (starts on daemon restart)
unless-stoppedLike always but not after manual docker stop
bash
# Restart up to 5 times on failure docker run -d --restart=on-failure:5 myapp # Always restart (good for production services) docker run -d --restart=unless-stopped myapp # Update restart policy on existing container docker update --restart=unless-stopped mycontainer

Resource Limits

Memory

bash
# Hard memory limit (OOM killed if exceeded) docker run -d --memory=512m myapp # Memory + swap limit docker run -d --memory=512m --memory-swap=1g myapp # Memory reservation (soft limit) docker run -d --memory=1g --memory-reservation=512m myapp

CPU

bash
# Limit to 1.5 CPUs docker run -d --cpus=1.5 myapp # CPU shares (relative weight, default 1024) docker run -d --cpu-shares=512 myapp # Half priority docker run -d --cpu-shares=2048 myapp # Double priority # Pin to specific CPU cores docker run -d --cpuset-cpus="0,1" myapp # Use cores 0 and 1

PIDs

bash
# Limit number of processes (prevent fork bombs) docker run -d --pids-limit=100 myapp

Health Checks

bash
# In Dockerfile HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Override at runtime docker run -d \ --health-cmd="wget -q --spider http://localhost:8080/health || exit 1" \ --health-interval=30s \ --health-timeout=3s \ --health-retries=3 \ myapp # Check health status docker inspect --format='{{.State.Health.Status}}' mycontainer # healthy, unhealthy, or starting # See health check log docker inspect --format='{{json .State.Health}}' mycontainer | jq

Container Logging

bash
# Default log driver: json-file docker logs mycontainer # Configure log driver per container docker run -d \ --log-driver=json-file \ --log-opt max-size=10m \ --log-opt max-file=3 \ myapp # Available log drivers # json-file - Default, stored on disk as JSON # syslog - Send to syslog daemon # journald - Send to systemd journal # fluentd - Send to Fluentd # awslogs - Send to CloudWatch # gcplogs - Send to Google Cloud Logging # none - Disable logging

Container as Processes

Containers are just Linux processes with extra isolation:

bash
# On the host, you can see container processes ps aux | grep nginx # The container's PID 1 maps to a regular PID on the host docker inspect --format='{{.State.Pid}}' mycontainer # Returns: 12345 # You can inspect the process's namespaces ls -la /proc/12345/ns/

PID 1 and Signal Handling

The container's main process (PID 1) has special behavior:

  • Receives all signals (SIGTERM, SIGINT, etc.)
  • Must handle SIGTERM for graceful shutdown
  • If PID 1 exits, the container stops
dockerfile
# BAD: Shell form -- /bin/sh is PID 1, your app doesn't get signals CMD npm start # GOOD: Exec form -- your app is PID 1, receives signals directly CMD ["node", "server.js"] # Or use tini/dumb-init as PID 1 (handles zombie processes) RUN apk add --no-cache tini ENTRYPOINT ["tini", "--"] CMD ["node", "server.js"]

FAANG Interview Angle

Common questions:

  1. "What happens when a container's PID 1 process crashes?"
  2. "How do you debug a container that keeps restarting?"
  3. "Explain the difference between docker stop and docker kill"
  4. "How would you limit a container's resources in production?"
  5. "What's the difference between exec and attach?"

Key answers:

  • Container stops when PID 1 exits; restart policy determines what happens next
  • Debug: check logs, exec into container, inspect events, check health
  • stop sends SIGTERM (graceful), kill sends SIGKILL (immediate)
  • Use --memory, --cpus, and health checks; set restart policies
  • exec starts a new process; attach connects to PID 1's stdio

Official Links