Docker Containers
Running and managing Docker containers: the lifecycle, port publishing, environment variables, restart policies, and why a container exits immediately.
A container is a running instance of an image with its own writable layer, process namespace and network interface. Images are templates; containers are what actually runs.
The lifecycle
docker run ──► Running ──► docker stop ──► Exited ──► docker rm ──► gone
│ ▲
└─ pause ────┘
docker run -d --name web nginx:1.27 # create + start
docker stop web # SIGTERM, then SIGKILL after 10s
docker start web # start it again. Same container
docker rm web # delete it
The distinction that matters: docker run creates a new container every time. docker start restarts an existing one. Running the same command three times gives three containers, which is why docker ps -a fills up unexpectedly.
docker ps # running only
docker ps -a # including exited
Why your container exits immediately
The most common Docker frustration, and it has one root cause.
A container lives exactly as long as its main process. When PID 1 exits, the container exits. There is no background mode that keeps it alive.
docker run ubuntu # exits instantly
docker run ubuntu bash # also exits, no TTY, bash has no input and quits
docker run -it ubuntu bash # stays running. Interactive terminal attached
The same applies to your own images. A web server that daemonises itself, forking into the background and returning, makes PID 1 exit, so Docker concludes the work is done:
# Wrong, nginx forks and the foreground process exits
CMD ["nginx"]
# Right. Stays in the foreground
CMD ["nginx", "-g", "daemon off;"]
Diagnosing it:
docker ps -a # check the STATUS and exit code
docker logs <container> # what did it print before dying?
STATUS
Exited (127) 2 seconds ago
Exit codes carry information: 0 means clean exit, 1 a general application error, 125 the Docker command itself failed, 126 the command is not executable, 127 command not found, 137 SIGKILL (often out of memory), 143 SIGTERM.
Exit 127 nearly always means a typo in the command or a binary missing from the image.
Publishing ports
docker run -d -p 8080:80 nginx:1.27
# │ └── container port
# └── host port
Host on the left, container on the right. Reversing them is a rite of passage.
docker run -d -p 127.0.0.1:8080:80 nginx # bind to localhost only
docker run -d -P nginx # publish all EXPOSEd ports randomly
docker port web # what is actually mapped?
EXPOSE in a Dockerfile publishes nothing. It is documentation. Only -p or -P at run time actually maps a port.
Binding to 127.0.0.1 is worth knowing: without it, -p 8080:80 binds all interfaces, and on a cloud VM that can bypass your firewall, because Docker writes its own iptables rules.
Environment and configuration
docker run -d \
-e NODE_ENV=production \
-e DATABASE_URL="postgres://db:5432/app" \
--env-file .env \
myapp:1.0
Environment variables are the standard way to configure a container, which is what lets one image serve every environment.
Avoid passing secrets with -e where you can. They appear in docker inspect and in the process list on the host. A mounted file or a secrets manager is better.
Restart policies
docker run -d --restart unless-stopped nginx:1.27
| Policy | Behaviour |
|---|---|
no | Default (never restart) |
on-failure[:n] | Restart on non-zero exit, optionally capped |
always | Always restart, including after daemon restart |
unless-stopped | Like always, but stays stopped if you stopped it deliberately |
unless-stopped is usually the right choice for a long-running service. always will restart a container you deliberately stopped when the daemon next starts, which is rarely what anyone wants.
Resource limits
docker run -d --memory 512m --cpus 1.5 myapp:1.0
Without limits, one container can consume the whole host. A memory limit means the container is killed when it exceeds it, exit code 137, rather than the host running out of memory and the kernel choosing a victim arbitrarily.
docker stats # live usage
docker stats --no-stream
Working with a running container
docker logs -f web # follow
docker logs --tail 100 web
docker logs --since 10m web
docker exec -it web sh # shell inside
docker exec web env # single command
docker cp web:/etc/nginx/nginx.conf ./ # copy a file out
docker inspect web # full configuration as JSON
docker top web # processes
docker exec attaches to an already running container. If the container has exited, exec fails. You need docker run with a different command, or --entrypoint to override what it starts:
docker run --rm -it --entrypoint sh myapp:1.0
That is the standard way to explore an image whose normal command exits immediately.
stop vs kill
docker stop web # SIGTERM, wait 10s, then SIGKILL
docker stop -t 30 web # allow 30 seconds
docker kill web # SIGKILL immediately
docker stop gives the application a chance to finish in-flight work. Use it. docker kill is for a process that is genuinely stuck.
If docker stop always takes exactly 10 seconds, your application is not receiving SIGTERM, almost always because the Dockerfile used shell-form CMD, making /bin/sh PID 1 instead of your process.
Cleaning up
Stopped containers accumulate and hold disk through their writable layers:
docker container prune # remove all stopped containers
docker rm -f web # force-remove a running one
docker ps -aq | xargs docker rm -f # remove everything, carefully
The best habit is preventing the buildup: --rm deletes the container the moment it exits.
docker run --rm alpine echo hello
docker run --rm -it ubuntu bash
Use it for anything one-off. Tests, scripts, exploration. Nothing accumulates.
Quick reference
| Task | Command |
|---|---|
| Run detached with a port | docker run -d -p 8080:80 --name <n> <image> |
| Run and auto-delete | docker run --rm -it <image> <cmd> |
| List running | docker ps |
| List all | docker ps -a |
| Logs | docker logs -f <n> |
| Shell inside | docker exec -it <n> sh |
| Explore an image that exits | docker run --rm -it --entrypoint sh <image> |
| Port mappings | docker port <n> |
| Live resource usage | docker stats |
| Stop gracefully | docker stop <n> |
| Remove stopped containers | docker container prune |
Key takeaways
- A container lives exactly as long as PID 1. Exit immediately usually means the process daemonised or was not found.
docker runcreates a new container each time;docker startreuses one.- Port mapping is
-p host:container, andEXPOSEpublishes nothing. - Bind to
127.0.0.1when you do not want a port on all interfaces: Docker writes its own iptables rules and can bypass a host firewall. unless-stoppedis the sensible restart policy for services.- Exit 137 is SIGKILL, usually out of memory; 127 is command not found.
- A 10-second
docker stopevery time meansSIGTERMis not reaching your process. - Use
--rmfor anything one-off so containers do not accumulate.
Check your understanding
A container that exits instantly
docker run -d myapp returns an ID but docker ps shows nothing. docker ps -a shows "Exited (0)". What does this mean?
A port mapping that does nothing
An app listens on port 3000 inside the container. The user runs docker run -d -p 3000:8080 myapp and cannot reach it on localhost:3000. Why?
Frequently Asked Questions
Why does my Docker container exit immediately?
Because its main process finished. A container runs exactly as long as PID 1, so anything that completes, or daemonises itself into the background, ends the container. Check docker ps -a for the exit code and docker logs <container> for output. Exit 0 means a clean finish, so the command simply ran and stopped; exit 127 means command not found, usually a typo or a binary missing from the image. For servers, keep the process in the foreground, such as nginx -g "daemon off;".
What is the difference between docker run and docker start?
docker run creates a new container from an image and starts it, so running the same command three times produces three containers. docker start restarts an existing, stopped container by name or ID, preserving its writable layer and configuration. If docker ps -a is filling up with stopped containers, that is run being used where start was intended, or a missing --rm flag on one-off commands.
How does Docker port mapping work?
-p hostPort:containerPort. Host on the left, container on the right. So -p 8080:80 makes a container listening on 80 reachable at localhost:8080. Reversing them is extremely common and produces a mapping that silently forwards to a port nothing is listening on. Note that EXPOSE in a Dockerfile publishes nothing; it only documents the port. Use -p 127.0.0.1:8080:80 to bind to localhost only, since a plain -p binds all interfaces and Docker's own iptables rules can bypass a host firewall.
How do I get a shell inside a Docker container?
docker exec -it <container> sh for a running container, using bash if the image has it. Many slim images do not. If the container has already exited, exec will not work; start a new one with an overridden command instead: docker run --rm -it --entrypoint sh <image>. That is the standard way to explore an image whose normal command exits immediately or crashes on start.
Which restart policy should I use?
unless-stopped for most long-running services. It restarts the container after a crash or a Docker daemon restart, but respects a deliberate docker stop, whereas always will bring back a container you intentionally stopped the next time the daemon starts, which is rarely wanted. Use on-failure for batch work that should retry on error but not after a successful run, and leave the default no for one-off commands.
How do I remove all stopped Docker containers?
docker container prune removes every stopped container in one command and reports how much space was reclaimed. To remove absolutely everything including running containers, docker ps -aq | xargs docker rm -f works but should be used carefully. The better habit is prevention: add --rm to any one-off docker run so the container deletes itself on exit, which stops the buildup entirely.