Somewhere in my first month of using Docker, I realized I was typing docker run for everything, even in situations where I actually wanted to just start a container that already existed. That habit worked, but it hid an important truth: containers have a full lifecycle with distinct states, and each state has its own commands. Once I learned to move deliberately between “create,” “start,” “stop,” “pause,” and “remove,” debugging got a lot easier because I finally understood what state a misbehaving container was actually in.
The Container Lifecycle at a Glance
A Docker container moves through these states:
Created → Running → Paused → Stopped → Removed
- Created: the container exists on disk with its writable layer set up, but its main process hasn’t started.
- Running: the main process is executing.
- Paused: all processes inside are frozen (via cgroups freezer) but still present in memory.
- Stopped/Exited: the main process has ended; the container’s filesystem still exists.
- Removed: the container and its writable layer are deleted.
Creating a Container Without Starting It
docker create --name my-nginx -p 8080:80 nginx:latest
Expected output:
a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890
That long string is the container ID. At this point, nothing is running yet — I can confirm with:
docker ps -a
CONTAINER ID IMAGE COMMAND STATUS NAMES
a1b2c3d4e5f6 nginx:latest "/docker-entrypoint.…" Created my-nginx
Starting a Container
docker start my-nginx
my-nginx
Checking status again:
docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
a1b2c3d4e5f6 nginx:latest Up 3 seconds 0.0.0.0:8080->80/tcp my-nginx
docker run: Create and Start in One Step
In practice, I almost always use docker run, which combines create and start:
docker run -d --name web1 -p 8081:80 nginx:latest
b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3
The -d flag runs it detached (in the background); I’ll cover this in more depth in a dedicated guide on detached mode.
Viewing Logs
docker logs web1
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/07/29 10:15:22 [notice] 1#1: nginx/1.27.0
2026/07/29 10:15:22 [notice] 1#1: start worker processes
Follow logs live with -f:
docker logs -f web1
Stopping a Container
docker stop web1
web1
Internally, docker stop sends SIGTERM to the container’s main process (PID 1 inside the container), waits a grace period (10 seconds by default), and if the process hasn’t exited, sends SIGKILL. I can adjust the grace period:
docker stop -t 30 web1
Killing a Container Immediately
If I don’t care about a graceful shutdown — say, during quick local testing — docker kill sends SIGKILL right away:
docker kill web1
Restarting a Container
docker restart web1
This is functionally a stop followed by a start, useful when an app needs to reload configuration or recover from a hung state.
Pausing and Unpausing
docker pause web1
docker unpause web1
Pausing uses the Linux cgroups freezer subsystem to suspend all processes in the container without terminating them — useful for temporarily reclaiming CPU without losing in-memory state.
Removing a Container
Once a container is stopped, its filesystem and metadata still exist on disk until removed:
docker rm web1
web1
Trying to remove a running container fails safely:
docker rm web2
Error response from daemon: cannot remove container "/web2": container is running: stop the container before removing or force remove
Force-remove (stops and removes in one step) when I’m sure:
docker rm -f web2
Combining Stop and Remove for Cleanup
A pattern I use constantly during development:
docker rm -f $(docker ps -aq)
This stops (implicitly, via -f) and removes every container on the host — I only ever run this on a dev machine, never on anything with production containers.
Removing Containers Automatically After They Exit
For throwaway test containers, --rm removes the container the moment it stops, so I don’t have to clean up manually:
docker run --rm alpine echo "this container will clean itself up"
this container will clean itself up
Checking afterward:
docker ps -a
No trace of the container remains.
Internal Working: What Happens at Each Stage
When I run docker create, the Docker daemon (dockerd), through containerd and runc, prepares a container’s root filesystem by stacking the image’s read-only layers with a new writable layer (via overlay2 on most Linux hosts), sets up its network namespace, and writes container metadata to /var/lib/docker/containers/<id>/config.v2.json. Nothing is executed yet.
docker start hands the prepared bundle to runc, which uses Linux namespaces (PID, network, mount, UTS, IPC) and cgroups to actually launch the process in isolation. From that point, the container’s PID 1 is a real Linux process on the host, just heavily namespaced.
docker stop and docker kill operate by sending POSIX signals directly to that PID 1 process. docker rm deletes the writable layer and metadata but never touches the underlying image layers, which is why removing containers never deletes images.
Networking and Storage Notes
Each container gets its own network namespace by default, connected to a bridge network (docker0 unless otherwise configured) via a virtual ethernet pair. Stopping a container tears down that network namespace; removing it releases the container’s IP back to Docker’s internal address pool. Data written to the container’s writable layer is lost on docker rm unless it lives in a bind mount or named volume — something worth checking before cleanup:
docker inspect -f '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}' web1
Security Considerations
- Removing a container doesn’t remove secrets that may have been logged to
docker logsoutput — clear log retention policies matter. docker execinto a running container inherits that container’s namespace isolation, but not more; don’t assumedocker stopalone is sufficient isolation between sensitive workloads.- Be cautious with
docker rm -f $(docker ps -aq)on shared hosts — it has no concept of “mine” versus “someone else’s” containers.
Troubleshooting
Container immediately shows “Exited (0)” after docker run This typically means the main process finished right away — common with containers whose default command is something like bash with no interactive terminal attached. Check with:
docker inspect -f '{{ .State.ExitCode }}' web1
“No such container” errors Usually a typo in the container name/ID, or the container was already removed. docker ps -a shows everything, running or not.
Container won’t stop, even with docker stop The main process may be ignoring SIGTERM. After the grace period, Docker sends SIGKILL, which cannot be ignored, so this should eventually succeed. If it truly hangs, check docker events for anomalies.
Monitoring
docker stats web1
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O
b2c3d4e5f6a7 web1 0.05% 3.2MiB / 1.944GiB 0.16% 1.2kB / 0B
Best Practices
- Use
--rmfor throwaway/test containers. - Give containers meaningful
--namevalues instead of relying on random names. - Prefer
docker stopoverdocker killin production to allow graceful shutdown. - Regularly prune stopped containers with
docker container pruneto reclaim disk space. - Inspect mounts before removing containers holding important data.
Summary
The container lifecycle — create, start, stop, pause, remove — maps directly onto real Linux process and namespace mechanics, not just Docker-specific abstractions. Understanding which command touches which layer (metadata, writable filesystem, running process) makes debugging far more precise than treating docker run as a magic on/off switch.
References
- Docker CLI reference: https://docs.docker.com/reference/cli/docker/container/
- Docker container lifecycle documentation: https://docs.docker.com/engine/containers/
- runc: https://github.com/opencontainers/runc
- containerd: https://containerd.io/docs/