Containers: Running, Managing, and Lifecycle

Difficulty

A representative real-world command

docker run -d \
  --name my-api \
  -p 8080:80 \
  -e NODE_ENV=production \
  -v api-data:/app/data \
  --network my-network \
  --restart unless-stopped \
  myapp:1.0

Flag by flag

  • -d (--detach) — runs the container in the background, returning control of your terminal immediately, rather than attaching your terminal to the container's stdout/stderr (the default, "foreground" mode). Almost always what you want for a long-running server process; foreground mode is more useful for quick, interactive, or debugging runs.
  • -p host_port:container_port — publishes a port, mapping a port on the host to a port inside the container (see the networking topic for exactly what this does internally). Without this, the container's port is only reachable from other containers on the same Docker network, not from the host machine or the outside world.
  • -e KEY=value — sets an environment variable inside the container, exactly as if it had been set via a Dockerfile ENV instruction, but specified at run time instead of baked into the image. This is the standard way to inject runtime configuration (see the security topic for how to handle sensitive values differently).
  • -v / --mount — attaches persistent storage (a named volume or a bind mount — see the storage topic) so data survives beyond this one container's lifetime, or so the container can access files from the host.
  • --name — assigns a specific, memorable name (my-api) instead of Docker's default random name (like happy_euler). This makes subsequent commands (docker logs my-api, docker stop my-api) much easier to work with.
  • --rm — automatically removes the container (and its writable layer) the moment it exits. This is ideal for short-lived, one-off tasks (running a quick script, a database migration, a debug shell) where you don't want leftover stopped containers cluttering docker ps -a afterward.
  • --network — attaches the container to a specific, named Docker network (see the networking topic), rather than the default bridge network — necessary for containers that need reliable DNS-based service discovery of each other.
  • --restart — sets the container's restart policy (see that question) — how Docker should behave if the container exits unexpectedly or the host reboots.

A useful one-off, throwaway invocation

docker run --rm -it ubuntu bash

-it combines -i (interactive — keep stdin open) and -t (allocate a pseudo-TTY). Together, these give you an interactive shell session inside a freshly started container. --rm ensures it's cleaned up automatically the moment you exit the shell, leaving nothing behind.

Why fluency with these specific flags matters

These flags collectively answer the handful of questions every container needs answered: how does it run (foreground/background), how is it reachable (ports, networks), what does it need to know (environment variables), where does its data live (volumes), what happens if it dies (restart policy), and how is it identified for later management (name). Real production docker run invocations (or their Compose/Kubernetes equivalents) are almost always some combination of exactly these concerns, expressed through exactly these flags or their higher-level equivalents.

Related Resources

The state diagram

docker create  ──▶  Created
                        │
                   docker startRunning ──── docker pause ───▶ Paused
                        │  ◀─── docker unpause ────────┘
                   docker stop / process exits
                        ▼
                    Exited
                        │
                   docker rm
                        ▼
                    (removed entirely)

Created — defined, but not started

docker create --name my-container myapp:1.0
docker ps -a
# STATUS: Created

The container object exists (its configuration is recorded), but its process hasn't been started yet. docker create alone doesn't run anything. docker start (or the combined docker run, which does both create and start in one step) is what actually launches the process.

Running — actively executing

docker start my-container
docker ps
# STATUS: Up 5 seconds

The container's main process is executing. As long as that process keeps running, the container stays in this state.

Paused — suspended, not stopped

docker pause my-container
docker ps
# STATUS: Paused

docker pause uses cgroup freezing to suspend all processes inside the container. They stop executing entirely (no CPU time at all), but remain in memory, fully intact, ready to resume instantly via docker unpause. This is distinct from stopping — a paused container's process state is preserved exactly as it was, mid-execution, rather than being terminated. Rarely used in typical production workflows, but useful for scenarios like briefly freezing a container to take a consistent filesystem snapshot without fully stopping it.

Exited — the process has stopped

docker stop my-container       # sends SIGTERM, waits, then SIGKILL if needed
docker ps -a
# STATUS: Exited (0) 3 seconds ago

The main process has terminated — either cleanly (exit code 0, whether from docker stop or the process finishing its work naturally) or due to a crash/error (a non-zero exit code). Critically, the container still exists in this state. Its filesystem (including its writable layer's contents), configuration, and logs are all still present and inspectable (docker logs, docker cp). It can be restarted with docker start again, picking up right where its writable layer left off.

Removed — gone entirely

docker rm my-container

This is the only truly destructive, final step — it deletes the container object and its writable layer permanently. Any data that lived only in that writable layer (not in a separate volume — see the storage topic) is now unrecoverable.

The critical distinction: stop vs. rm

A very common point of confusion, especially for people newer to Docker: docker stop does not delete anything — the container, its configuration, and its writable layer's data all persist, just not currently running. docker rm is the actual deletion step. Assuming docker stop alone cleans everything up (leaving stopped containers accumulating indefinitely, each still consuming disk space for their writable layers) is a common operational oversight. docker container prune is the standard way to clean up accumulated stopped containers in bulk once you're confident none of them are needed anymore.

Why this matters practically

Knowing precisely which commands are destructive (rm) versus merely pausing execution (stop, pause) is essential before running cleanup commands against anything that might hold data you still need. Always confirm whether a container's important data lives in its own writable layer (gone on rm) or in a mounted volume (survives rm, as covered in the storage topic) before removing it.

Related Resources

The four restart policies

docker run --restart no myapp           # default: never auto-restart
docker run --restart on-failure myapp    # restart only on non-zero exit
docker run --restart on-failure:5 myapp   # restart on failure, up to 5 attempts total
docker run --restart always myapp          # always restart, no matter how/why it exited
docker run --restart unless-stopped myapp   # like "always", but respects an explicit manual stop

no — the default, no automatic restart

If the container exits for any reason (clean exit, crash, or the daemon restarting), it simply stays stopped, requiring a manual docker start to run again. Appropriate for one-off tasks and anything you specifically want manual control over.

on-failure — restart only when something goes wrong

docker run --restart on-failure:3 myapp

Only restarts if the container's process exits with a non-zero status code (indicating an error) — a clean, intentional exit (status 0) is left alone. The optional :3 caps this at a maximum of 3 restart attempts. After that, Docker gives up and leaves the container in the Exited state, avoiding an infinite restart loop for a container that's persistently, repeatedly failing. This is conceptually similar to Kubernetes's CrashLoopBackOff handling, though Docker's own backoff/retry behavior is simpler.

always — unconditional restart, including on daemon/host restart

docker run -d --restart always myapp

Restarts the container regardless of why it stopped — even a clean, intentional exit gets restarted. Critically, always also restarts the container automatically when the Docker daemon itself restarts (e.g., after a host reboot, or dockerd being upgraded and restarted). This is what makes always the right choice for genuinely long-running services meant to be perpetually available, surviving host reboots without manual intervention.

unless-stopped — like always, but respects a deliberate manual stop

docker run -d --restart unless-stopped myapp
docker stop myapp     # this container will NOT auto-restart even after a daemon/host restart,
                        # because it was explicitly, deliberately stopped

The key difference from always is this: if you explicitly run docker stop on a container using unless-stopped, Docker remembers that deliberate action and won't bring it back, even across a subsequent daemon restart. A container using always, by contrast, genuinely would come back even in that scenario. This is often surprising and undesired: you stopped it on purpose, and you don't necessarily want it silently reappearing after the next host reboot. This makes unless-stopped the generally safer, more intuitive default for most long-running services compared to always.

Why restart policies matter operationally

Without an appropriate restart policy, a container that crashes (an unhandled exception, an OOM kill) simply stays down until a human notices and manually restarts it. For anything expected to run continuously, this is a real, easily-avoidable availability gap. on-failure with a sensible retry cap is a reasonable default for many application containers: it auto-recovers from transient failures, but doesn't loop forever on a genuinely broken deployment. unless-stopped is the standard choice for infrastructure/support services (a reverse proxy, a database container in a simpler non-orchestrated setup) that should always be running unless someone deliberately takes them down.

The relationship to orchestrators

In a Kubernetes-managed environment, restart behavior is instead governed by the Pod's restartPolicy and the surrounding controller's reconciliation logic (see the Kubernetes stack) — Docker's own --restart flag is primarily relevant when running plain Docker or Docker Compose directly, without a higher-level orchestrator making these decisions instead.

Related Resources

docker exec — a new, separate process inside the container

docker exec -it my-container sh

This starts a brand-new process (a shell, in this example) running inside the same namespaces as the container's existing main process. You get an independent, separate session for poking around — checking files, running diagnostic commands, inspecting environment variables — entirely alongside whatever the container's actual main process (say, a running web server) is doing. Exiting this shell session (exit, or Ctrl+D) simply ends that one exec'd process. The container's real main process, and the container itself, are completely unaffected and keep running exactly as before.

docker exec my-container ps aux
# shows the container's main process AND the exec'd command, as separate processes

docker attach — connects to the existing main process directly

docker attach my-container

This doesn't start anything new — it connects your terminal's stdin/stdout/stderr directly to the container's already-running main process (whatever was originally started by docker run/CMD/ENTRYPOINT). You're now seeing exactly what that one process is outputting, and if it accepts stdin input, whatever you type goes directly to it.

Why attach is riskier for casual investigation

docker attach my-container
# Ctrl+C
# this can send SIGINT directly to the container's main process --
# potentially stopping (or crashing) the very container you meant to just "look at"

Because attach connects to the actual main process, a Ctrl+C intended to just "detach and stop looking" can instead be interpreted as a signal sent to that process, potentially terminating it (and thus the container) entirely. This is a well-known, easy-to-trigger mistake for anyone using attach casually. (The correct way to detach without stopping the process is a specific escape sequence, Ctrl+P then Ctrl+Q, which most people don't remember under pressure.)

Why exec is almost always the right tool for debugging

docker exec -it my-container sh

Because exec starts an entirely separate process, there's no risk of accidentally disturbing the container's actual main workload just by exploring inside it. Exiting the exec'd shell is always safe, and never affects the container's real running process. This is exactly why docker exec -it <container> sh (or bash, if available) is the standard, go-to command for interactively debugging a running container. docker attach is reserved for the narrower, specific case where you genuinely want to interact with, or observe live output from, the container's actual main process itself — for example, a process that reads commands from stdin directly, where you specifically want to send it input. Remember the Ctrl+P, Ctrl+Q detach sequence if you do.

Related Resources

Basic usage

docker logs my-container                # show all accumulated logs
docker logs -f my-container               # follow/stream logs live, as they're written
docker logs --tail 100 my-container         # only the most recent 100 lines
docker logs --since 10m my-container          # only logs from the last 10 minutes
docker logs -t my-container                    # include timestamps for each line

These flags combine freely — docker logs -f --tail 50 --since 5m my-container follows live output, starting from the last 50 lines within the last 5 minutes.

Where these logs actually come from

Docker only captures what a container's main process writes to stdout and stderr. This is exactly why best practice (borrowed from the twelve-factor app methodology, see the production topic) is for containerized applications to log to stdout/stderr rather than writing to internal log files. Only stdout/stderr is automatically captured by Docker's logging mechanism, without any extra configuration.

docker inspect my-container --format='{{.HostConfig.LogConfig.Type}}'
# json-file   (the default logging driver)

By default, Docker uses the json-file logging driver, which writes each log line as a JSON object to a file on the host's disk (typically under /var/lib/docker/containers/<container-id>/) — this is what docker logs actually reads from.

The default's real limitations at scale

docker inspect my-container --format='{{.LogPath}}'

The default json-file driver has no automatic log rotation configured out of the box. A long-running, chatty container can, in principle, fill up the host's disk with an ever-growing log file if this isn't explicitly configured. It's also inherently tied to that one specific host and container. There's no built-in mechanism to search or aggregate logs across many containers or many hosts. This becomes a real operational gap the moment you're running more than a handful of containers.

Configuring log rotation for the default driver

docker run --log-opt max-size=10m --log-opt max-file=3 myapp

This caps each log file at 10MB, keeping at most 3 rotated files — a reasonable minimum safeguard for any container running the default driver in a context where unbounded log growth would actually be a problem.

Alternative logging drivers for production

docker run --log-driver=syslog --log-opt syslog-address=udp://loghost:514 myapp
docker run --log-driver=json-file --log-driver=awslogs --log-opt awslogs-group=myapp myapp

Docker supports several alternative logging drivers (syslog, journald, fluentd, awslogs, gelf, and others) that forward log output directly to an external logging system rather than (or in addition to) writing local JSON files. This is the standard approach for genuine production log management, especially once running many containers across many hosts. Centralized, searchable, durable logging becomes essential rather than optional at that point, mirroring the DaemonSet-based log-shipping pattern covered in the Kubernetes stack.

A key caveat about switching drivers

Once you configure a non-default logging driver (like syslog or awslogs), docker logs generally stops working for that container, since it specifically reads from the local json-file format. Logs are instead only accessible through whatever external system the chosen driver forwards them to. This is an important tradeoff to know before switching drivers in an environment where people are used to reaching for docker logs directly.

ContextReasonable choice
Local development, quick debuggingDefault json-file, docker logs as-is
Multiple containers, one host, unbounded growth is a real riskjson-file + max-size/max-file rotation
Multiple hosts, need searchable/durable logsA centralized driver (syslog, fluentd, awslogs)

Related Resources