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 and returns your terminal immediately, instead of attaching to the container's stdout/stderr (the default "foreground" mode). Almost always what you want for a long-running server; foreground mode is more useful for quick, interactive, or debugging runs.-p host_port:container_port— publishes a port, mapping a host port to a container port. Without this, the container's port is only reachable from other containers on the same Docker network, not from the host or the outside world.-e KEY=value— sets an environment variable inside the container, just like a DockerfileENVinstruction, but specified at run time instead of baked into the image. The standard way to inject runtime configuration.-vor--mount— attaches persistent storage, like a named volume or bind mount, so data survives beyond this one container's lifetime, or so the container can access files from the host.--name— gives the container a memorable name, likemy-api, instead of Docker's random default likehappy_euler. Makes later commands likedocker logs my-apiordocker stop my-apieasier to work with.--rm— automatically removes the container, and its writable layer, the moment it exits. Ideal for short-lived, one-off tasks — a quick script, a database migration, a debug shell — where you don't want leftover stopped containers clutteringdocker ps -a.--network— attaches the container to a specific, named Docker network instead of the default bridge. Needed for containers that rely on DNS-based service discovery of each other.--restart— sets the container's restart policy: 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, keeps stdin open) and -t (allocates a pseudo-TTY). Together they give you an interactive shell inside a freshly started container. --rm cleans it up automatically the moment you exit the shell, leaving nothing behind.
Why fluency with these specific flags matters
These flags answer the handful of questions every container needs answered: how does it run (foreground or background), how is it reachable (ports and 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 or Kubernetes equivalents, are almost always some combination of exactly these concerns.
Related Resources
The state diagram
docker create ──▶ Created
│
docker start
▼
Running ──── 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 and its configuration is recorded, but its process hasn't started yet. docker create alone doesn't run anything. docker start — or docker run, which combines create and start in one step — 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, using no CPU time, but stay in memory, fully intact, ready to resume instantly via docker unpause. This is different from stopping — a paused container's process state is preserved mid-execution, not terminated. Rarely used in typical production workflows, but useful for 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 — cleanly (exit code 0, whether from docker stop or the process finishing naturally) or due to a crash (non-zero exit code). Critically, the container still exists in this state. Its filesystem, including its writable layer's contents, configuration, and logs, is still present and inspectable via docker logs or 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, is now unrecoverable.
The critical distinction: stop vs. rm
A common point of confusion: docker stop does not delete anything. The container, its configuration, and its writable layer's data all persist — just not running. docker rm is the actual deletion step. Assuming docker stop alone cleans everything up leads to stopped containers accumulating indefinitely, each still consuming disk space for their writable layers. docker container prune is the standard way to clean up accumulated stopped containers in bulk once you're confident none are needed anymore.
Why this matters practically
Know precisely which commands are destructive (rm) versus merely pausing execution (stop or pause) 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) 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 — a clean exit, a crash, or the daemon restarting — it just stays stopped, needing a manual docker start to run again. Good for one-off tasks and anything you 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 — a clean, intentional exit (status 0) is left alone. The optional :3 caps this at 3 restart attempts. After that, Docker gives up and leaves the container Exited, avoiding an infinite restart loop for a container that's persistently failing. This is similar in spirit to Kubernetes's CrashLoopBackOff handling, though Docker's own backoff/retry logic 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. always also restarts the container automatically when the Docker daemon itself restarts, for example after a host reboot or a dockerd upgrade. This 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: if you explicitly run docker stop on a container using unless-stopped, Docker remembers that and won't bring it back, even across a later daemon restart. A container using always would come back even then — often surprising and undesired, since you stopped it on purpose and don't want it silently reappearing after the next host reboot. That makes unless-stopped the generally safer default for most long-running services.
Why restart policies matter operationally
Without an appropriate restart policy, a container that crashes — from an unhandled exception or an OOM kill — just stays down until a human notices and manually restarts it. For anything expected to run continuously, that's 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 without looping forever on a genuinely broken deployment. unless-stopped is the standard choice for infrastructure and support services — a reverse proxy, a database container in a simpler, non-orchestrated setup — that should always run 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. Docker's own --restart flag mainly matters 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 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, with exit or Ctrl+D, just ends that one exec'd process. The container's real main process, and the container itself, 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, or ENTRYPOINT. You now see exactly what that one process is outputting, and if it accepts stdin, 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 meant to just "detach and stop looking" can instead be sent as a signal to that process, potentially killing it, and 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 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 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 case where you genuinely want to interact with, or observe live output from, the container's actual main process — 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 why best practice, borrowed from the twelve-factor app methodology, is for containerized applications to log to stdout/stderr rather than write to internal log files — only stdout/stderr gets captured automatically by Docker's logging mechanism.
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 out of the box. A long-running, chatty container can fill up the host's disk with an ever-growing log file if you don't configure this explicitly. It's also tied to that one host and container — there's no built-in way to search or aggregate logs across many containers or hosts. This becomes a real operational gap once 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 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 instead of, or alongside, writing local JSON files. This is the standard approach for real production log management, especially once you're running many containers across many hosts, where centralized, searchable, durable logging becomes essential. It mirrors the DaemonSet-based log-shipping pattern used in Kubernetes.
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 the local json-file format. Logs are only accessible through whatever external system the chosen driver forwards them to. Know this tradeoff before switching drivers in an environment where people are used to reaching for docker logs directly.
| Context | Reasonable choice |
|---|---|
| Local development, quick debugging | Default json-file, docker logs as-is |
| Multiple containers, one host, unbounded growth is a real risk | json-file + max-size/max-file rotation |
| Multiple hosts, need searchable/durable logs | A centralized driver (syslog, fluentd, awslogs) |