Docker & Containerization

Running Containers

18 min Lesson 3 of 30

Running Containers

Knowing how to pull an image is table stakes. The real skill is knowing exactly how to launch, inspect, control, and debug a container in production — and why each flag exists. At big-tech scale, a misunderstood flag means a runaway process, a secret leaked into logs, or a container that silently exits and never restarts. This lesson covers the full container lifecycle through docker run and its companions.

The Full docker run Anatomy

Every invocation of docker run creates a new container from an image, runs a single process (PID 1), and exits when that process exits. The syntax is:

docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

The most important flags to internalize:

  • -d / --detach — Run in the background; print container ID and return to shell. This is the default for long-running services.
  • -it — Allocate a pseudo-TTY (-t) and keep STDIN open (-i). Use together for interactive shells. Never combine with -d for services.
  • --name — Assign a deterministic name. Without it, Docker generates a random adjective-noun pair — unmanageable at scale.
  • --rm — Remove the container automatically when it exits. Critical for one-off debugging containers and CI runners to prevent disk exhaustion.
  • -p HOST:CONTAINER — Publish a port. -p 8080:80 binds host port 8080 to container port 80.
  • -e KEY=VALUE — Inject an environment variable. The preferred way to pass configuration; never bake secrets into images.
  • --env-file FILE — Bulk-inject env vars from a file. Use for long config lists or secrets in CI.
  • --restart — Restart policy: no (default), always, on-failure[:N], unless-stopped. Production services use unless-stopped so they survive host reboots but can be deliberately stopped.
  • --memory / --cpus — Hard resource limits. Without them, one runaway container can starve neighbors on the same host.
  • --user — Override the UID/GID at runtime. Use to avoid running as root inside the container.
  • --read-only — Mount the container filesystem as read-only. Forces the process to write only to explicitly mounted volumes — a defense-in-depth security control.
# Detached, named, restart policy, resource limits, env var docker run -d \ --name api-server \ --restart unless-stopped \ --memory 512m \ --cpus 1.5 \ -p 8080:8080 \ -e DATABASE_URL=postgres://user:pass@db:5432/app \ --user 1000:1000 \ myorg/api-server:v2.4.1 # One-off interactive debugging shell (auto-removed on exit) docker run --rm -it ubuntu:24.04 bash # Run a specific command inside an image without modifying it docker run --rm alpine:3.20 sh -c "apk add --no-cache curl && curl -sI https://example.com"
Image pinning is a production requirement. Always use a specific tag (preferably a digest like @sha256:abc123…) in production manifests. latest is a mutable pointer that changes silently and makes rollbacks impossible.

Container Lifecycle

A container transitions through well-defined states. Understanding these is essential for writing health checks and restart logic.

Docker container lifecycle state machine Created Running Paused Exited Removed start pause unpause stop/kill restart rm
Docker container lifecycle: Created → Running → Paused / Exited → Removed.

Key lifecycle commands:

# Start/stop/restart by name docker start api-server docker stop api-server # Sends SIGTERM, waits 10s, then SIGKILL docker stop --time 30 api-server # Give process 30s to drain connections docker restart api-server docker kill api-server # Immediate SIGKILL -- avoid in production # Pause/unpause (SIGSTOP -- freezes cgroups, useful for snapshot debugging) docker pause api-server docker unpause api-server # List containers docker ps # Running only docker ps -a # All, including stopped docker ps --filter status=exited --filter name=api # Remove stopped containers docker rm api-server docker rm -f api-server # Force-remove even if running docker container prune # Remove ALL stopped containers
docker stop vs docker kill: Always prefer stop. It sends SIGTERM first, giving your application time to flush buffers, close database connections, and finish in-flight requests. kill sends SIGKILL immediately and can corrupt state. The 10-second default timeout is often too short for databases — tune it per service with --time.

Streaming Logs

The docker logs command reads from the container's configured log driver. The default driver (json-file) writes stdout/stderr to a JSON file on the host. In production, you switch to a driver that ships logs to a centralized system (Loki, Splunk, CloudWatch), but docker logs remains invaluable for local debugging.

# Tail and follow (like tail -f) docker logs -f api-server # Last 100 lines docker logs --tail 100 api-server # Since a relative duration docker logs --since 10m api-server docker logs --since "2025-10-01T08:00:00" api-server # Include timestamps (added by Docker, not the app) docker logs -t api-server # Combine: follow with timestamps from last 50 lines docker logs -ft --tail 50 api-server
Log to stdout/stderr — always. Twelve-factor apps write logs to stdout; the runtime (Docker, Kubernetes, systemd) routes them to whatever sink is configured. Never write logs to a file inside the container — you lose them on container removal and they silently fill the overlay filesystem.

Executing Commands in a Running Container

docker exec spawns a new process inside an already-running container's namespaces. It does not touch PID 1. This is the correct tool for live debugging — not restarting the container.

# Open an interactive shell docker exec -it api-server bash docker exec -it api-server sh # If bash is not installed (Alpine images) # Run a one-off command non-interactively docker exec api-server env | grep DATABASE docker exec api-server ps aux # Run as a specific user (e.g., root for privileged inspection) docker exec -u root api-server cat /etc/passwd # Check open ports from inside the container docker exec -it api-server ss -tlnp # Inspect cgroup memory usage from inside docker exec api-server cat /sys/fs/cgroup/memory.current
Never install debugging tools into production images to enable exec sessions. Use distroless or minimal base images, and plan debugging via ephemeral containers (Kubernetes) or a sidecar. Installing curl, strace, or tcpdump into a running production container significantly expands the attack surface. Pre-plan your observability strategy before an incident occurs.

Inspecting Container State

docker inspect returns the full JSON representation of a container or image — mounts, network config, environment variables, resource limits, health check status, exit codes, and more. It is the authoritative source of truth about a container's live configuration.

# Full JSON dump docker inspect api-server # Extract specific fields with Go templates docker inspect --format '{{.State.Status}}' api-server docker inspect --format '{{.State.ExitCode}}' api-server docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' api-server docker inspect --format '{{json .HostConfig.Resources}}' api-server | python3 -m json.tool # Real-time resource consumption (CPU, memory, network I/O, block I/O) docker stats api-server docker stats --no-stream api-server # Single snapshot, good for scripting docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
Production debugging workflow: When a container behaves unexpectedly, the standard sequence is: (1) docker ps to confirm state, (2) docker logs to read recent output, (3) docker inspect to verify config (env vars, mounts, restart count), (4) docker stats to rule out resource exhaustion, (5) docker exec for live investigation if the container is still running.

Copying Files to and from Containers

Occasionally you need to pull a config file or a generated artifact out of a running container for inspection, or push a temporary file in for testing. docker cp handles this without a volume mount.

# Copy from container to host docker cp api-server:/app/config/settings.json ./local-settings.json # Copy from host into container docker cp ./debug.conf api-server:/app/config/debug.conf

Putting It Together: A Production-Hardened Run Command

Here is what a security-hardened, production-ready docker run for a web API looks like — every flag is intentional:

docker run -d \ --name payments-api \ --restart unless-stopped \ --memory 1g --memory-swap 1g \ --cpus 2 \ --pids-limit 200 \ --read-only \ --tmpfs /tmp:size=100m \ --cap-drop ALL \ --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges \ -p 127.0.0.1:8080:8080 \ --env-file /etc/secrets/payments.env \ --user 1001:1001 \ myorg/payments-api:sha256-abc123

This pattern — minimal Linux capabilities, read-only filesystem, explicit resource ceilings, loopback-only port binding — is the baseline that security-conscious engineering organizations apply to every production container before they even consider the orchestrator (Kubernetes) layer on top.