docker run in depth

Names, detached runs, ports, environment, cleanup and restart policies.

Beginner11 min · lesson 5 of 14

docker ps says the container is Up, the PORTS column shows 127.0.0.1:8083->8080/tcp, and curl answers Recv failure: Connection reset by peer. The container is healthy. The command that started it published a port the program inside never listens on. Most docker run mistakes are of this kind: the flags were accepted, and they did something other than what you meant. This lesson goes through the flags you will use every day and shows what each one does, including when it goes wrong.

The shape of the command

docker run [OPTIONS] IMAGE [COMMAND] [ARG...]. Options for Docker go before the image name. Everything after the image name is the command to run inside the container, replacing the image's default command. Docker does not read past the image name, which this run makes obvious:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run --rm alpine:3.22 echo --name lab-x -p 9999:80
--name lab-x -p 9999:80

--name lab-x -p 9999:80 came after the image, so Docker passed it to echo as plain arguments: no name was set and no port was published. When a flag "does nothing", check which side of the image name it is on.

-d and --name

Without -d, docker run stays attached to the container: its output streams to your terminal and the command returns when the process exits. That suits a one-off command. For a service, -d (detach) starts the container in the background and prints its ID. --name gives the container a name you choose; without it Docker generates one such as eager_lovelace. A name is unique on the daemon, as "Images, containers and the core commands" showed, so scripts that run the same container twice need to remove the old one first.

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-web -p 127.0.0.1:8080:80 nginx:1.30-alpine
940f8123e83b626df7d7a74ee1904eb6285a4cfb55d663a1278ccb13e24a9713
$ docker ps --filter name=lab-web
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 940f8123e83b nginx:1.30-alpine "/docker-entrypoint.…" 1 second ago Up Less than a second 127.0.0.1:8080->80/tcp lab-web

-p publishes a container port

A container on the default network has its own IP address on a private bridge inside the host, and nothing outside the host can reach it. -p publishes a port: -p 127.0.0.1:8080:80 means "accept connections on the host's 127.0.0.1, port 8080, and forward them to port 80 in the container". The right-hand number must be the port the program inside actually listens on; nginx listens on 80.

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ curl -s -o /dev/null -w '%{http_code} %header{server}\n' http://127.0.0.1:8080/
200 nginx/1.30.5

curl printed the HTTP status and the Server header of the response: the request went to the host port and was answered by nginx 1.30.5 inside the container. The address in front of the ports matters. Leave it out and Docker publishes on every address of the host, IPv4 and IPv6:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-web-all -p 8081:80 nginx:1.30-alpine
4bf525505b25a9acb0221171fc3a4e2e6ad3298934a552baf28ba64e86465bd9
$ docker ps --filter name=lab-web-all --format '{{.Names}} {{.Ports}}'
lab-web-all 0.0.0.0:8081->80/tcp, [::]:8081->80/tcp

0.0.0.0:8081 and [::]:8081 mean any machine that can reach this host can reach the container. On a laptop or a shared server, publish on 127.0.0.1 unless other machines really need the service. Docker writes its own firewall rules for published ports, and they take effect before rules you may have added with tools such as ufw, so a host firewall does not necessarily protect a port published on all addresses. "Publishing ports and the packet path" in Docker in depth explains the rules and how to restrict them.

When the host port is taken

One host address and port can be published once. Try to publish 127.0.0.1:8080 again while lab-web holds it:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-web2 -p 127.0.0.1:8080:80 nginx:1.30-alpine
fa34d23b94088f4046470886e8b6e8a419b79adac77bb0053e7a4e7a02537d75 docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint lab-web2 (a0e6d3b2a8e5c9e8272e252cd7c1185ca72e8e0981af45e4f7f30cf0d8508336): Bind for 127.0.0.1:8080 failed: port is already allocated Run 'docker run --help' for more information
$ docker ps -a --filter name=lab-web2 --format '{{.Names}}: {{.Status}}'
lab-web2: Created
$ docker rm lab-web2
lab-web2
$ docker run -d --name lab-web2 -p 127.0.0.1:8082:80 nginx:1.30-alpine
58bb8111583a3ce0424fd80be37342d78ba92f63d017558fbd3a60da700481e3

Read the error from the end: Bind for 127.0.0.1:8080 failed: port is already allocated means another container already publishes that address and port. Notice the container ID printed above the error. Docker created lab-web2, failed while setting up its networking, and left it in the Created state, holding the name. Remove it, then run it again with a free host port (8082 here). Only the left-hand number changes; the container still listens on 80.

If the port is held by a program outside Docker, the message ends differently. Start a small Python web server on 127.0.0.1:8090 (Python 3 is installed in the lab VM); nohup and the redirects keep it running quietly in the background, and ss -ltn confirms that something now listens on the port:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ nohup python3 -m http.server 8090 --bind 127.0.0.1 </dev/null >/dev/null 2>&1 &
$ sleep 1; ss -ltn 'sport = :8090'
State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 5 127.0.0.1:8090 0.0.0.0:*

Now publish the same address and port from a container:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-web3 -p 127.0.0.1:8090:80 nginx:1.30-alpine
3b7a2db509166c822f8f1231732b5d556093b5a8f81435d4560586d3c2b5cf1e docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint lab-web3 (e04011e2dd1ea21637639a8a46e81fbb31846e12f71d5454f5f66025c5d49e17): failed to bind host port 127.0.0.1:8090/tcp: address already in use Run 'docker run --help' for more information
$ docker rm lab-web3
lab-web3

address already in use comes from the kernel refusing the bind, so look for the process with sudo ss -ltnp 'sport = :8090' (with sudo, -p can show processes of every user). port is already allocated comes from Docker's own bookkeeping, so look at docker ps. Stop the Python server again; the pattern http[.]server matches the server's command line but not the pkill command itself:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ pkill -f 'http[.]server 8090'

When nothing listens on the container port

This container publishes host port 8083 to container port 8080, where nginx is not listening:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-wrong -p 127.0.0.1:8083:8080 nginx:1.30-alpine
de60f35b3a1ef7f57565895f2f0674e0a877ef6398e2bf0166d534b9584ba98b
$ curl -sS http://127.0.0.1:8083/
curl: (56) Recv failure: Connection reset by peer

The connection to the host port succeeded, the forward into the container found nothing on 8080, and the client got a reset. The container is fine and docker ps shows it as Up. Find the real port in the image's documentation, or in the ports the image declares (the 80/tcp that docker ps shows for an unpublished nginx), and fix the right-hand number.

-e sets environment variables

Most images read their settings from environment variables, so the same image runs with different configuration per environment. Each -e NAME=value adds one:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run --rm -e APP_ENV=staging -e LOG_LEVEL=debug alpine:3.22 printenv APP_ENV LOG_LEVEL
staging debug

Some images refuse to start without certain variables. The official postgres image, for example, exits unless you set POSTGRES_PASSWORD (or POSTGRES_PASSWORD_FILE, which reads it from a file). Environment variables are visible to anyone who can run docker inspect on the container, so they are not a safe place for production secrets. "Environment variables and configuration" covers --env-file and its rules, and Advanced container security covers secrets.

--rm for throwaway containers

Every container you start stays on disk after it exits, unless you pass --rm, which removes it when the process ends:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run --rm --name lab-once alpine:3.22 echo done
done
$ docker ps -a --filter name=lab-once
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
$ docker run --rm --restart always alpine:3.22 true
docker: conflicting options: cannot specify both --restart and --rm Run 'docker run --help' for more information

Nothing named lab-once is left. Use --rm for one-off commands and experiments. It cannot be combined with a restart policy, as the last command shows: a container that is deleted on exit cannot also be restarted on exit.

--restart policies

A restart policy tells the daemon what to do when the container's process exits. no is the default. on-failure[:N] restarts after a non-zero exit, at most N times. always restarts whatever the exit code and also starts the container when the daemon starts, for example after a reboot. unless-stopped behaves like always, except that a container you stopped yourself stays stopped across daemon restarts. A container that always exits with status 1 shows the policy at work:

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker run -d --name lab-flaky --restart on-failure:3 alpine:3.22 sh -c 'echo started; exit 1'
d821afe3764f93d10048e19ed99304213d55cf2462c5667f2a5f3f611f6c85be
$ docker inspect -f '{{.RestartCount}} restarts, status {{.State.Status}}, exit {{.State.ExitCode}}' lab-flaky
3 restarts, status exited, exit 1
$ docker logs lab-flaky
started started started started

The daemon started it once and restarted it three times, then gave up, which is why the log has four started lines and the container is exited with status 1. Between attempts the daemon waits, starting at 100 ms and doubling each time, so a crashing container does not spin the CPU. A restart policy keeps a service running through crashes and reboots; it does not fix the crash. If the restart count keeps climbing, read docker logs. Compose files set the same policies with the restart: key.

ubuntu@secopslog-docker:~ · Docker 29.8.2
$ docker rm -f lab-web lab-web-all lab-web2 lab-wrong lab-flaky
lab-web lab-web-all lab-web2 lab-wrong lab-flaky
Quick check
01You run docker run -d nginx:1.30-alpine -p 8080:80 --name web. What happens?
Correct — Everything after the image name is the container's command and arguments. Docker never sees them as options; depending on the image they may also make the program fail.
Incorrect — Docker does not reorder anything. Options must come before the image name.
Incorrect — The image does not have to be last. Docker accepts arguments after it and passes them to the container.
Incorrect — Neither flag is read by Docker, the name included, because both come after the image.
02A service published with -p 8080:80 on a cloud VM is reachable from the internet although the VM's host firewall allows only SSH. Which change keeps it local to the VM?
Incorrect — That publishes host port 80 to container port 8080, where nginx is not listening, and still on every address.
Incorrect — Detaching has nothing to do with which addresses a port is published on.
Incorrect — Restart policies decide what happens when the process exits; they do not change published ports.
Correct — Without an address Docker publishes on all host addresses (0.0.0.0 and [::]), and its own firewall rules apply before many host firewall rules.
03docker run -d --name api -p 127.0.0.1:9000:9000 my-api:1 prints a container ID and then failed to bind host port 127.0.0.1:9000/tcp: address already in use. What is the best next step?
Incorrect — This wording comes from the kernel, which means a program outside Docker probably holds the port. A Docker clash says port is already allocated.
Incorrect — The port stays held as long as the other program runs, and the name api is now taken by the failed container.
Correct — The message points at a non-Docker listener, and the failed run left a Created container named api that must be removed.
Incorrect — The conflict is on the host side. Changing the container port leaves host port 9000 busy and points at a port the app does not use.

Try this

Work through “--restart policies” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

If you keep one thing from docker run in depth, keep “--restart policies”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.

Related