Skip to content

04 - Container Lifecycle

What this session is

About 30 minutes. The container's life: create, start, pause, stop, restart, exec into, view logs, remove. Plus how to debug a container that won't start.

Container states

A container is in one of these states at any given time:

  • created - Docker has set it up but not started it.
  • running - main process is alive.
  • paused - all processes frozen (uncommon).
  • restarting - Docker is restarting it.
  • exited - main process finished (with some exit code).
  • dead - broken, can't recover. Remove and recreate.

docker ps shows running. docker ps -a shows all states.

List

docker ps                              # only running
docker ps -a                           # all states
docker ps -q                           # only IDs (useful in scripts)
docker ps --filter "status=exited"     # filter
docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Status}}"   # custom columns

The default columns: ID, image, command, created, status, ports, name.

Start, stop, restart

When you docker run, it creates + starts. You can also do them separately:

docker create --name foo nginx          # created but not running
docker start foo                        # starts (returns immediately)
docker start -a foo                     # starts AND attaches stdout/stderr

For an already-running container:

docker stop foo                         # SIGTERM, then SIGKILL after 10s
docker stop -t 30 foo                   # custom grace period (30s)
docker kill foo                         # immediate SIGKILL
docker restart foo                      # stop + start

docker stop is the polite option: it sends SIGTERM, gives the process up to 10 seconds to clean up, then SIGKILLs. docker kill is immediate. Use stop unless the process is wedged.

Logs

docker logs foo                         # all stdout/stderr from the main process
docker logs -f foo                      # follow (like tail -f)
docker logs --tail 100 foo              # last 100 lines
docker logs --since 10m foo             # last 10 minutes
docker logs -f --tail 20 foo            # follow, starting from last 20

-f is the most-used. Open a second terminal, run docker logs -f myservice, watch as you exercise the service.

Exec: get a shell inside a running container

docker exec -it foo sh                  # shell into the container
docker exec foo cat /etc/hostname       # one-off command
docker exec -e KEY=val foo env          # one-off with extra env

exec is one of the most useful debugging tools. Container's web server returning 500s? docker exec -it web bash and look around. Database container won't accept connections? docker exec -it db psql and check from inside.

Many minimal images don't have bash. Use sh instead.

Stats

docker stats                           # live CPU/memory/network for all running
docker stats --no-stream               # one-shot snapshot
docker stats foo                       # one container

Useful for quick "is this container hot?" checks.

Inspect a container

docker inspect foo

Long JSON: configuration, mounts, network settings, environment, restart policy. Useful when "why is this container behaving like X?"

Specific fields with --format:

docker inspect --format='{{.State.Status}}' foo
docker inspect --format='{{.NetworkSettings.IPAddress}}' foo
docker inspect --format='{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' foo

Cleanup

docker rm foo                          # remove a stopped container
docker rm -f foo                       # force (stops + removes a running one)
docker container prune                  # remove all stopped containers

Containers using --rm clean themselves up. Without --rm, they accumulate. Periodic docker container prune is fine.

Debugging "container won't start"

A container that exits immediately is the most common debug case. Workflow:

  1. Check logs:

    docker logs foo
    
    The last few lines of stdout/stderr almost always tell you why. Missing env var? Bad config file? Permission denied?

  2. Check exit code:

    docker ps -a --filter "name=foo"
    
    Exited (0) - finished normally. Exited (1) - error. Exited (139) - segfault. Exited (137) - killed (often OOM).

  3. Try running interactively - bypass the image's default command:

    docker run -it --rm --entrypoint sh IMAGE
    
    Now you have a shell in a fresh container of the same image. Inspect: are the files there? Is the script executable? Does the binary even run?

  4. Recreate the exact env, then exec into it:

    docker run -d --name debug IMAGE sleep 3600       # override cmd to sleep
    docker exec -it debug sh
    # try to manually do what the image's default command does
    

  5. Read the Dockerfile (page 05) - find the original repo on Docker Hub, look at the ENTRYPOINT / CMD. Sometimes the image expects environment variables you didn't set.

A real session

Imagine the issue: "my Postgres container exits immediately."

docker run -d --name pg postgres:16
docker ps                       # not there!
docker ps -a                    # shows Exited (1)
docker logs pg

Output ends with:

You must specify POSTGRES_PASSWORD ...

Fix:

docker rm pg
docker run -d --name pg -e POSTGRES_PASSWORD=secret postgres:16
docker ps                       # running now
docker logs pg                  # confirm clean startup

This loop (run → check logs → fix → run again) is most of container debugging.

Restart policies

For containers you want to survive crashes or host reboots:

docker run -d --restart=unless-stopped --name pg postgres:16

Policies: - no (default) - never restart. - on-failure - restart if exit code != 0. - on-failure:3 - at most 3 times. - always - always restart (even if you docker stop it). - unless-stopped - always restart, EXCEPT if you manually stopped it.

unless-stopped is the right policy for most services.

Exercise

  1. Run, inspect, exec, log:

    docker run -d --name web nginx
    docker ps
    docker logs web
    docker exec -it web bash
    ls /etc/nginx
    exit
    docker stop web
    docker rm web
    

  2. Watch stats while running a CPU-bound container:

    docker run -d --rm --name busy alpine sh -c 'while true; do :; done'
    docker stats busy        # in another terminal
    docker stop busy
    

  3. Debug a "container won't start":

    docker run -d --name brokenpg postgres:16       # no password env var
    docker ps -a                                    # exited
    docker logs brokenpg                            # see the error
    docker rm brokenpg
    docker run -d --name pg -e POSTGRES_PASSWORD=secret postgres:16
    docker ps                                       # running
    docker stop pg && docker rm pg
    

  4. Auto-restart:

    docker run -d --restart=on-failure --name flap alpine sh -c 'sleep 5 ; exit 1'
    docker ps                                       # see RESTARTING in status
    # wait a moment, check again
    docker ps -a
    docker rm -f flap
    

What you might wonder

"My container exits with code 137 - what is that?" SIGKILL (9) + 128 = 137. Either out of memory (Docker OOM-killed it) or someone docker killed it. Check docker inspect --format='{{.State.OOMKilled}}' foo.

"My container exits with 139?" SIGSEGV (11) + 128 = 139. Segfault - the program crashed. Look at logs; check if you're missing a library or running the wrong architecture image.

"Can I attach my terminal to a running container instead of exec?" docker attach foo connects your stdin/stdout to the main process. Different from exec (which starts a new process inside). Less useful in practice; people use exec more.

Done

  • Understand container states.
  • List, start, stop, restart, kill containers.
  • Read logs (and follow with -f).
  • Exec into running containers.
  • Inspect for configuration details.
  • Debug containers that won't start.
  • Set restart policies.

Next: Building images with Dockerfile →

Comments