docker run in depth
Detach, names, ports, and tags.
Of every Docker command, docker run is the one you will wear out. It accepts a frankly silly number of options, but five of them cover almost everything you do in a normal week: -d, --name, -p, -e, and the image name you park at the end. Two words first, because the rest of this rests on them. An image is a read-only template. It is a frozen meal sealed in its box: you cannot change what is inside, you heat it up. A container is one running copy of that image, the meal actually cooking in the oven. docker run takes an image and starts a container from it. One image, as many containers as you want.
$ docker run -d --name web -p 8080:80 nginx:1.27Unable to find image 'nginx:1.27' locally1.27: Pulling from library/nginxa2abf6c4d29d: Pull completee7f9e5e6f1a1: Pull completeDigest: sha256:2fd9a1c0b7e4d3f6a8b2c5e9d0f1a3b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2Status: Downloaded newer image for nginx:1.27c3f279d17e0a5f8b6a1d4e9c2b7a0f3e5d8c1b6a4f2e9d0c7b3a1f6e4d2c9b8a
Read that line right to left. nginx:1.27 is the image (nginx is a very widely used web server), and the bit after the colon, 1.27, is its tag. A tag is a version label stuck on an image, like the edition printed on the spine of a book. Leave the tag off and Docker fills in latest for you. That word sounds like a promise. It is not. It means whatever the publisher stamped as latest most recently, and that can move while you sleep. When you care which version runs, write a real number. Keep going left. --name web hands the container a label you chose, and if you skip it Docker invents one like gifted_bell. Then -p 8080:80 publishes a port. A port is a numbered door on a machine, and this wires door 8080 on your computer to door 80 inside the container. Last, -d runs the container detached, which means in the background.
That -d deserves a second look. Leave it off and your terminal is tied to the container like a dog on a leash. Logs scroll past live, and your prompt does not come back until the container stops, usually because you hit Ctrl+C. Handy when you are watching a short job run to the end. Maddening for a web server you want humming quietly for hours. So for anything long-lived, reach for -d. You already saw the payoff above: Docker printed the container's long ID and gave you your prompt straight back. Detached does not mean blind, though. docker logs web prints everything that container has written so far, any time you feel like looking.
Did it actually start?
Your prompt came back, which proves Docker accepted the command. It does not prove the server is answering. Two quick checks settle it. First, docker ps lists the containers running right now, and yours turns up under the name you picked with its port mapping spelled out. Second, knock on the door yourself with curl, a small tool that fires a web request straight from the terminal. A 200 response code is the server saying hello, all good here. No curl on your machine? Opening http://localhost:8080 in a browser tells you exactly the same thing.
$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESc3f279d17e0a nginx:1.27 "/docker-entrypoint.…" 6 seconds ago Up 4 seconds 0.0.0.0:8080->80/tcp web$ curl -I localhost:8080HTTP/1.1 200 OKServer: nginx/1.27.4Content-Type: text/htmlContent-Length: 615
-e: passing settings in at startup
Most images can be adjusted as they boot without rebuilding anything, and environment variables (named settings the operating system hands to a program when it starts) are the usual way in. They behave like sticky notes you slap on the program on its way out the door: it reads the ones it recognises and ignores the rest. The official Postgres database image is the famous case. It flatly refuses to start until you hand it a password this way. Each setting goes in as -e NAME=value, and you can stack up as many as the image understands. Want to confirm a value landed? docker exec runs a command inside a container that is already running, so you can ask the container to read its own sticky notes back to you.
$ docker run -d --name db \-e POSTGRES_PASSWORD=devsecret \-e POSTGRES_DB=payments \postgres:165e1c9a7b3f2d8c604a19e7d2b8f3c1a6d904e2f7b1c8a3d6e0f4b7a2c9d1e6f30$ docker exec db env | grep POSTGRES_DBPOSTGRES_DB=payments
--rm: clean up on the way out
Every container you start sticks around after it stops. It does not vanish. It sits there in a stopped state, still holding its name and a little disk space. Plain docker ps shows only what is running, so add -a (short for all) and the stopped ones appear too. Fire off a dozen throwaway commands in an afternoon and you will have a dozen leftovers by dinner. --rm settles that: the container deletes itself the instant it exits. Perfect for one-off jobs you never want to keep, like running a single command inside a small image to see what it prints.
$ docker run --rm --name ping alpine:3.20 echo "hello from a throwaway"hello from a throwaway$ docker ps -a --filter name=pingCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
When the port is already taken
Here is the wall almost everyone walks into in week one. You have a container serving on port 8080. You start a second one on that same 8080, and this time docker run refuses. One door fits one occupant. The error that comes back is long and a little alarming, so it helps to know who is talking: the Docker daemon, the background service that does the real work of running your containers every time you type a docker command. Skip to the very last line, which is the only part that matters: bind: address already in use.
$ docker run -d --name web2 -p 8080:80 nginx:1.27docker: Error response from daemon: driver failed programming externalconnectivity on endpoint web2 (a1b2c3d4e5f6): Error starting userland proxy:listen tcp4 0.0.0.0:8080: bind: address already in use.$ docker rm web2web2$ docker run -d --name web2 -p 8081:80 nginx:1.27b7d2e9f0c1a3d5e8f4a0b6c2d9e3f7a1c5b8d4e6f0a2c3b9d7e1f5a8c4b0d6e2
docker run ships with dozens of flags and you will reach for about six. -d frees your terminal. --name gives you a handle to type instead of a long ID. -p opens a door from the host into the container. -e passes settings in. --rm takes out the rubbish. The tag decides which version of the image you get. One layout rule holds them together: flags go before the image name, and anything you type after the image name is the command that runs inside the container. Get comfortable with that shape before you go hunting for the rare options.
Port publishing catches everyone exactly once. -p 8080:80 reads as "host port 8080 forwards to container port 80". The number on the right has to match the port the program inside genuinely listens on. nginx listens on 80 by default. Plenty of Node.js apps (JavaScript running on a server rather than in a browser) listen on 3000 instead. Get that right-hand number wrong and the symptom is misleading: docker ps swears the container is up, while curl sits there and hangs. Check the image's documentation for its listening port.
Detached containers still need watching. If the main process inside dies, a -d container drops off docker ps without a sound, because nothing was attached to tell you. Worth making a habit: after every detached run, check docker ps. If your container is not listed, run docker ps -a to find the stopped one, then docker logs to read its last words. Detaching does not make a container immortal. It only means your terminal is not wired to its output.
Names have to be unique on a single host machine. Restart a tutorial without clearing the old container and you will meet: Conflict. The container name is already in use. Treat that message as a favour. It is stopping you from quietly stacking up duplicates you have already forgotten about. Either remove the old container with docker rm -f oldname, or pick a different name for the new one.
Try this
Start nginx detached with a clear name and a published port, prove it is answering with curl, then crash into the port clash on purpose so the error looks familiar the day it happens for real.
docker run -d --name webdemo -p 8080:80 nginx:1.27-alpinecurl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/docker run -d --name webdemo2 -p 8080:80 nginx:1.27-alpine || truedocker rm -f webdemo
<container-id>200docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint webdemo2 (...): Bind for 0.0.0.0:8080 failed: port is already allocated
Takeaway
Day to day, docker run comes down to five moves: detach, name, publish, set env, pin a tag. The one that bites hardest is publishing. Docker will happily wire host port 8080 to whatever container port you name, including a port nothing is listening on, and then hand you a container that looks perfectly healthy in docker ps while curl hangs forever. Find out what the process inside listens on before you type -p.