Sign in
← Docker
12 min · easy · Lesson 2 of 8

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.

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:

  1. Docker looked for the alpine:3.20.3 image locally. If it wasn't there, it pulled it from Docker Hub.
  2. 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.
  3. It ran echo hello inside. Inside the container this is the process with PID 1.
  4. echo finished, and the container moved to the exited state. It isn't removed, you can still see it in docker 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

FlagWhat it doesExample
-ddetached: the container runs in the background, the terminal is freeservices: nginx, postgres
-it-i keeps stdin open, -t allocates a pseudo-terminal (TTY)docker run -it --rm alpine:3.20.3 sh
-p HOST:CONTpublishes a container port on the host-p 127.0.0.1:8080:80
--name NAMEthe container's namewithout it Docker makes one up, like silly_curran
--rmremove the container right after it exitsone-off runs
-e KEY=VALenvironment variable-e APP_ENV=dev
--memory, --cpusmemory 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:

CommandWhat it does
docker runcreates a new container from an image and starts it
docker psshows running containers
docker ps -ashows 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> shstarts 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
running exited removed docker run docker stop / kill docker start docker rm
The main path a container takes. A plain docker rm won't remove a running container, you need docker rm -f.

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 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

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.

1. Run nginx in the background, map host port 8080 to container port 80, name the container web.
2. Watch the `web` container's logs in follow mode (a live tail).
3. Get inside the running `web` container and start `sh` in interactive mode.

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson docker-basics-02-first-container