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.
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 itThe 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 exitedThe 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 attachedThe 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.
docker run -d -p 8080:80 nginx:1.27
# â âââ container port
# âââ host portHost 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.
docker run -d \
-e NODE_ENV=production \
-e DATABASE_URL="postgres://db:5432/app" \
--env-file .env \
myapp:1.0Environment 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.
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.
docker run -d --memory 512m --cpus 1.5 myapp:1.0Without 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-streamdocker 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 # processesdocker 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.0That is the standard way to explore an image whose normal command exits immediately.
docker stop web # SIGTERM, wait 10s, then SIGKILL
docker stop -t 30 web # allow 30 seconds
docker kill web # SIGKILL immediatelydocker 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.
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, carefullyThe 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 bashUse it for anything one-off. Tests, scripts, exploration. Nothing accumulates.
| 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 |
docker run creates a new container each time; docker start reuses one.-p host:container, and EXPOSE publishes nothing.127.0.0.1 when you do not want a port on all interfaces: Docker writes its own iptables rules and can bypass a host firewall.unless-stopped is the sensible restart policy for services.docker stop every time means SIGTERM is not reaching your process.--rm for anything one-off so containers do not accumulate.<MCQExercise exercise={{ id: 'docker-cont-exits', title: 'A container that exits instantly', description: 'docker run -d myapp returns an ID but docker ps shows nothing. docker ps -a shows "Exited (0)". What does this mean?', options: [ { id: 'a', text: 'The main process completed successfully and exited. A container only lives as long as PID 1, so it needs a foreground process' }, { id: 'b', text: 'The -d flag failed and the container never actually started' }, { id: 'c', text: 'The image is corrupt and needs rebuilding' }, { id: 'd', text: 'Docker ran out of memory and terminated the container' }, ], correctId: 'a', explanation: 'Exit code 0 means the main process finished normally. Containers are not daemons. They run exactly as long as PID 1, so a command that completes ends the container. The usual causes are a service that daemonises itself into the background, or an image whose default command simply runs and exits. Fix it by keeping the process in the foreground, such as nginx -g "daemon off;".', wrongExplanations: { b: 'The container did start. It has an exit status, which only exists once a process has run and finished.', c: 'A corrupt image fails at pull or create with an explicit error, not with a clean exit code of 0.', d: 'An out-of-memory kill gives exit code 137, not 0, and would appear as OOMKilled in docker inspect.', }, }} />
<MCQExercise exercise={{ id: 'docker-cont-portorder', title: 'A port mapping that does nothing', description: '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?', options: [ { id: 'a', text: 'The mapping is backwards, -p is host:container, so this forwards host 3000 to container 8080 where nothing is listening' }, { id: 'b', text: 'The Dockerfile is missing an EXPOSE 3000 instruction' }, { id: 'c', text: 'Detached mode disables port publishing' }, { id: 'd', text: 'Ports below 1024 require elevated privileges' }, ], correctId: 'a', explanation: 'The syntax is -p hostPort:containerPort, so 3000:8080 sends traffic arriving on host port 3000 to port 8080 inside the container. The application is on 3000 inside, so the correct mapping is -p 3000:3000, or any host port you prefer on the left. Reversing the two is one of the most common Docker mistakes.', wrongExplanations: { b: 'EXPOSE is documentation only and publishes nothing. Port mapping works with or without it.', c: 'Detached mode only controls whether your terminal is attached. Publishing is unaffected.', d: 'Both 3000 and 8080 are above 1024, and the privilege rule applies to the host side only.', }, }} />
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.
-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.
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.
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.
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.
Manage local images with docker tag, docker rmi, docker push and docker save/load. Includes cleanup with docker image prune and the export vs save difference.
Learn how Docker containers work, from docker run and the container lifecycle to port publishing, restart policies and why a container exits immediately.
Learn the docker run command with real examples: detached and interactive mode, naming, port mapping, resource limits, restart policies and more.