Your first container
docker run and its flags, ps/logs/exec, how to get inside a running container, and how an image differs from a container.
Almost all work with Docker starts with four commands: docker run,
docker ps, docker logs, and docker exec. In this lesson you'll start
nginx, look at its logs, go inside it, and work out the states a container
can be in. This is the foundation for every lesson in the module that
follows.
Before you start, check that Docker is installed and the daemon responds:
docker version
The output should have two blocks, Client and Server. If there's no
Server block and you see Cannot connect to the Docker daemon, the
daemon isn't running (on macOS and Windows, open Docker Desktop).
The error text in this lesson can differ depending on the Docker version, for example between 20.10 and 29, but the key phrase stays the same.
Image, container, process
These are three different things, and beginners mix them up often.
- Image — an immutable template made of read-only layers. It's named
with a tag, for example
nginx:1.27.3-alpine. Images are covered in detail in the next lesson. - Container — an instance of an image: a writable layer on top of the image's layers, plus settings (name, environment variables, ports, limits).
- Process — the program currently running inside the container. Inside the container it has PID 1, on the host it has a different PID.
You can start ten containers from one image. Each has its own writable layer and its own processes, and the image itself never changes.
docker run
The command template:
docker run [flags] <image> [command] [arguments]
A minimal example:
docker run alpine:3.20.3 echo hello
hello
What happened:
- Docker looked for the
alpine:3.20.3image locally. If it wasn't there, it pulled it from Docker Hub. - It created a container: an isolated environment with its own process list and its own filesystem made of the image's layers, with an empty writable layer on top.
- It ran
echo helloinside. Inside the container this is the process with PID 1. echofinished, and the container moved to theexitedstate. It isn't removed, you can still see it indocker ps -a.
You can check the PID like this:
docker run --rm alpine:3.20.3 sh -c 'echo pid=$$'
pid=1
The command at the end
If you don't give a command, Docker runs the one baked into the image.
The command at the end of docker run replaces it. Some images also have
a startup script (ENTRYPOINT), and then your command becomes its
argument. ENTRYPOINT and CMD are covered in detail in the Dockerfile
lesson.
Key flags
| Flag | What it does | Example |
|---|---|---|
-d | detached: the container runs in the background, the terminal is free | services: nginx, postgres |
-it | -i keeps stdin open, -t allocates a pseudo-terminal (TTY) | docker run -it --rm alpine:3.20.3 sh |
-p HOST:CONT | publishes a container port on the host | -p 127.0.0.1:8080:80 |
--name NAME | the container's name | without it Docker makes one up, like silly_curran |
--rm | remove the container right after it exits | one-off runs |
-e KEY=VAL | environment variable | -e APP_ENV=dev |
--memory, --cpus | memory and CPU limits | --memory 128m --cpus 0.5 |
The -v flag (volumes) and --restart (restart policy) are covered in
later lessons in the module.
About -p. Writing -p 8080:80 publishes the port on all of the host's
interfaces, so nginx is also visible to anyone else on the network. For
lab environments, write the address explicitly: -p 127.0.0.1:8080:80.
Then the port is reachable only from your own machine.
On a Linux server, keep in mind that Docker writes its own iptables rules, and a port blocked in ufw is still open to the outside world (Docker and ufw).
States and the lifecycle
The main commands:
| Command | What it does |
|---|---|
docker run | creates a new container from an image and starts it |
docker ps | shows running containers |
docker ps -a | shows all of them, including stopped ones |
docker logs [-f] <name> | the container's stdout and stderr, -f prints new lines as they appear |
docker exec -it <name> sh | starts another process inside a running container |
docker stop <name> | stops it: a stop signal, then SIGKILL after 10 seconds |
docker kill <name> | SIGKILL right away |
docker start <name> | starts an existing container again |
docker rm <name> | removes a stopped container |
docker run does two things: it creates the container (docker create
does the same) and starts it (docker start). In between, the container
sits in the created state: it already shows up in docker ps -a, but
the process hasn't started yet. The full list of states for the
docker ps --filter status=… filter: created, running, paused,
restarting, exited, removing, dead.
The exit code shows up in the STATUS column of docker ps -a:
Exited (0) means the process finished on its own without an error,
Exited (1) means the program exited with an error, Exited (137) means
the process was killed by SIGKILL (137 = 128 + 9). SIGKILL can come from
docker kill, or from the kernel when the container hits its --memory
limit. docker inspect helps tell the two apart, more on that below.
docker inspect
docker inspect <name> prints everything Docker knows about the
container as one big JSON blob: state, exit code, image, ports, limits,
and the environment variables the container was created with
(Config.Env). The -f flag pulls a single field out of that JSON. You
write the path to the field in double curly braces:
docker inspect -f '{{.State.ExitCode}}' <name>
This prints just a number, for example 0 for a container that exited
without an error.
If a container exited with code 137 because it hit the memory limit, the
.State.OOMKilled field is true. After docker kill it's false.
A typical session
docker run -d --name web -p 127.0.0.1:8080:80 --memory 128m --cpus 0.5 nginx:1.27.3-alpine
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES STATUS PORTS
web Up 2 seconds 127.0.0.1:8080->80/tcp
Check that nginx responds. curl on its own prints the HTML page, and the
-w flag shows the response code. The -sS flag hides the progress
indicator but keeps the error text if the connection fails:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080
200
If the container isn't listening on the port, instead of a code you'll
get 000 and the line
curl: (7) Failed to connect to 127.0.0.1 port 8080.
Next, logs and getting inside:
docker logs -f web # Ctrl+C stops the output, the container keeps running
docker exec -it web sh # a shell inside the container
ls /etc/nginx # this is already inside
exit # exit sh, the container keeps running
docker stop web
docker rm web
docker exec can also run a single command without opening a shell:
docker exec web ls /etc/nginx.
The nginx alpine image has sh but not bash. If
docker exec -it web bash answers with an executable file not found
error, use sh.
stop, kill, rm
docker stopsends the process a stop signal and waits 10 seconds. If the process hasn't exited, it finishes it off with SIGKILL. The signal comes from the image (theSTOPSIGNALinstruction), SIGTERM by default. Fornginx:1.27.3-alpineit's SIGQUIT: nginx finishes the requests it's currently handling and exits. The-tflag changes the wait time:docker stop -t 2 web.docker killsends SIGKILL right away. You need it when a process has hung and doesn't react to the stop signal. The service won't have time to close connections or flush data to disk.docker rmremoves a stopped container along with its writable layer. Anything the app wrote inside without a volume is gone. On a running container,docker rmfails withcontainer is running: stop the container before removing or force remove(on Docker 20.10:You cannot remove a running container).
docker rm -f removes a running container too, and the process gets
SIGKILL.
Some images create an unnamed volume when the container starts, for
example postgres for its data. docker rm doesn't remove that volume, it
stays on disk without an owner. To remove it along with the container, add
-v: docker rm -v <name>.
The difference between stop and kill shows up in practice:
docker run -d --name s1 alpine:3.20.3 sleep 300
time docker stop s1
docker rm s1
sleep doesn't catch SIGTERM, and the kernel only delivers signals to the
process with PID 1 in the container if it has registered a handler for
them. The signal is simply dropped, so docker stop waits the full 10
seconds and the container ends up Exited (137). The --init flag puts
a small init process on PID 1 that passes the signal along:
docker run --init -d --name s2 alpine:3.20.3 sleep 300
time docker stop s2
docker rm s2
The container stops within a second with code 143 (128 + 15, SIGTERM).
Getting more out of docker ps
docker ps -a # including stopped ones
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker ps --no-trunc # don't truncate the ID and command
docker ps -q # IDs only
-q is handy for bulk operations, but pick containers with a filter. The
command docker rm -f $(docker ps -aq) wipes out every container on the
machine, including other people's projects.
Filtering by name also needs care. --filter name=web searches for a
substring and will also match my-webapp and webhook-db. An exact name
needs regex anchors:
docker ps -a --filter 'name=^/?web$'
docker rm -f $(docker ps -aq --filter 'name=^/?web$')
If the filter finds nothing, the command fails with
'docker rm' requires at least 1 argument (on Docker 20.10 the command
name is in double quotes). Nothing breaks, there was just nothing to
remove.
Common mistakes
Name already in use. Running docker run --name web a second time
gives you:
docker: Error response from daemon: Conflict. The container name "/web" is already in use by container "3fc468aa...". You have to remove (or rename) that container to be able to reuse that name.
An old container with that name exists, even if it's exited. Find it in
docker ps -a, then either remove it (docker rm web) or start it again
(docker start web).
Port already in use.
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint web (424f5b1a...): Bind for 127.0.0.1:8080 failed: port is already allocated
This is how Docker 29 writes it; Docker 20.10 doesn't have the
failed to set up container networking: prefix. The key phrase
port is already allocated means another container is holding the port.
If the port is held by a regular program on the host, Docker Desktop
shows different text:
Ports are not available: ... bind: address already in use. To see who's
listening on the port, run lsof -nP -iTCP:8080 -sTCP:LISTEN (macOS,
Linux, WSL2).
With this error, docker run already managed to create the container on
the first step (see "States and the lifecycle" above), and the start only
failed on the second step. The container stays in docker ps -a with the
status Created, and running docker run --name … again will hit a name
conflict, not the port. From there you either run docker start <name>,
or docker rm <name> and start over.
Container went straight to exited. It's not in docker ps, and in
docker ps -a the status is Exited with a nonzero code. The program
itself almost always prints the reason, check docker logs <name>. If the
program complains about a setting, compare it with what the container was
actually created with: docker inspect <name>.
run instead of start, run instead of exec. docker run creates a new
container every time. To start an existing one again, you need
docker start <name>. To get inside a running one, you need
docker exec -it <name> sh; docker run -it nginx:1.27.3-alpine sh
spins up a separate new container. Files created in it won't show up in
the first container.
What you'll need for the task
docker run -d --name … -p 127.0.0.1:HOST:CONTAINER --memory … --cpus … image:tagdocker exec <name> <command>docker psanddocker ps -a, theSTATUScolumn and the exit codedocker logs <name>docker inspect <name>anddocker inspect -f '{{.State.ExitCode}}' <name>docker stop,docker kill,docker rm,docker startcurl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:PORT
In the next lesson we'll go deeper into images: layers, tags, digests, and why a tag without a specific version isn't reliable.
Self-check
Type the command or key line. Case and spaces don't matter.
Interview-style questions
Interview questions on this topic: 5
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson docker-basics-02-first-container
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress