You know how to run containers. Now let’s learn to manage them — view logs, debug issues, control resources, and keep things running reliably.

Code for this chapter. See 04-containers.

Container lifecycle

A container moves through these states:

Container Lifecycle

Commands to control state:

# Create without starting
docker create --name my-app my-image:v1

# Start a created/stopped container
docker start my-app

# Pause (freeze processes, don't stop)
docker pause my-app
docker unpause my-app

# Stop gracefully (SIGTERM, then SIGKILL after 10s)
docker stop my-app

# Stop immediately (SIGKILL)
docker kill my-app

# Remove a stopped container
docker rm my-app

# Remove a running container (force)
docker rm -f my-app

Viewing logs

docker logs is your first debugging tool:

# View all logs
docker logs my-app

# Follow logs in real-time
docker logs -f my-app

# Last 100 lines
docker logs --tail 100 my-app

# Since a timestamp
docker logs --since 2024-01-15T10:00:00 my-app

# With timestamps
docker logs -t my-app

Logs are stdout and stderr from the container’s main process. If your app logs to files, you won’t see them here — log to stdout in containers.

Executing commands inside containers

docker exec runs a command in a running container:

# Run a single command
docker exec my-app cat /etc/hosts

# Interactive shell
docker exec -it my-app bash

# As a specific user
docker exec -u root my-app whoami

# Set environment variables
docker exec -e DEBUG=true my-app ./script.sh

The -it flags are interactive + TTY — you need both for an interactive shell.

Common debugging patterns:

# Check what's running
docker exec my-app ps aux

# Check network configuration
docker exec my-app cat /etc/resolv.conf

# Check environment variables
docker exec my-app env

# Install debug tools (in development)
docker exec -u root my-app apt-get update && apt-get install -y curl
docker exec my-app curl http://localhost:8080/health

Copying files

Move files between your host and containers:

# Copy to container
docker cp config.json my-app:/app/config.json

# Copy from container
docker cp my-app:/app/logs/error.log ./error.log

# Copy entire directory
docker cp my-app:/app/data ./backup/

Inspecting containers

docker inspect dumps everything about a container:

docker inspect my-app

That’s a lot of JSON. Query specific fields:

# Get IP address
docker inspect -f '{{.NetworkSettings.IPAddress}}' my-app

# Get environment variables
docker inspect -f '{{.Config.Env}}' my-app

# Get mounted volumes
docker inspect -f '{{.Mounts}}' my-app

# Get restart count
docker inspect -f '{{.RestartCount}}' my-app

Resource limits

By default, a container can use unlimited CPU and memory. In production, that’s dangerous — one runaway container can starve everything else.

Memory limits

# Hard limit — container killed if exceeded
docker run -d --memory=512m my-app

# Soft limit (memory reservation)
docker run -d --memory=1g --memory-reservation=512m my-app

When a container exceeds its memory limit, the kernel OOM-kills it. You’ll see exit code 137 (128 + 9, killed by SIGKILL).

CPU limits

# Limit to 0.5 CPUs
docker run -d --cpus=0.5 my-app

# Limit to specific CPU cores
docker run -d --cpuset-cpus="0,1" my-app

# CPU shares (relative weight, default 1024)
docker run -d --cpu-shares=512 my-app

View resource usage

docker stats
CONTAINER ID   NAME      CPU %    MEM USAGE / LIMIT   MEM %    NET I/O          BLOCK I/O
a1b2c3d4e5f6   my-app    0.50%    128MiB / 512MiB     25.00%   1.5MB / 500KB    10MB / 0B

This is real-time. Add --no-stream for a snapshot.

Restart policies

Containers die. Restart policies bring them back:

# Never restart (default)
docker run -d --restart=no my-app

# Always restart
docker run -d --restart=always my-app

# Restart unless manually stopped
docker run -d --restart=unless-stopped my-app

# Restart on failure, max 3 times
docker run -d --restart=on-failure:3 my-app

Which to use:

  • Development: no — you want to see failures
  • Production services: unless-stopped — survives reboots, respects manual stops
  • One-time jobs: on-failure:3 — retry transient failures, give up on real errors

Health checks

Docker can monitor your container’s health:

docker run -d \
  --health-cmd="curl -f http://localhost:8080/health || exit 1" \
  --health-interval=30s \
  --health-timeout=10s \
  --health-retries=3 \
  my-app

Or define in Dockerfile:

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Health status appears in docker ps:

CONTAINER ID   IMAGE     STATUS
a1b2c3d4e5f6   my-app    Up 5 minutes (healthy)

Unhealthy containers can trigger alerts or orchestrator restarts.

Container cleanup

Containers and images pile up:

# Remove all stopped containers
docker container prune

# Remove containers older than 24h
docker container prune --filter "until=24h"

# Remove unused images
docker image prune

# Remove all unused images (not just dangling)
docker image prune -a

# Nuclear option — remove everything unused
docker system prune -a --volumes

Check disk usage:

docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          15        5         5.2GB     3.1GB (59%)
Containers      8         3         500MB     200MB (40%)
Local Volumes   10        2         2GB       1.5GB (75%)
Build Cache     50                  1GB       1GB

Container patterns

Run and remove

For one-time tasks, auto-remove the container when it exits:

docker run --rm my-tool process-data

Run in background with logs

# Start
docker run -d --name my-app -p 8080:8080 my-image:v1

# Check logs
docker logs -f my-app

# Stop when done
docker stop my-app && docker rm my-app

Debug a failing container

Container keeps crashing? Override the entrypoint:

docker run -it --entrypoint bash my-app

Now you’re inside the container before it tries to start. Poke around, check permissions, verify files exist.

Summary

  • Container lifecycle: create → start → pause/unpause → stop → remove
  • docker logs -f for real-time logs
  • docker exec -it to get a shell inside
  • Set memory (--memory) and CPU (--cpus) limits in production
  • Use restart policies (--restart=unless-stopped) for reliability
  • Health checks let Docker monitor your app
  • Clean up regularly with docker system prune

In the next chapter we’ll connect containers together with Docker networking.