Docker basics: images, containers, ports, logs and exec
Run your first real containers: pull an image, start a web server, publish its port, read its logs, open a shell inside, and stop, start and remove it, knowing what each step changes.
By the end of this lesson you'll be able to
0 of 5 completeImages and containers
An image is a read-only template: a filesystem and a default command. A container is one run of an image, with its own process, network and writable layer (lesson 2). One image, many containers:
$ docker pull nginx:1.30-alpine $ docker images REPOSITORY TAG IMAGE ID SIZE nginx 1.30-alpine 970364228b1e 102MB
docker pull nginx:1.30-alpinedockerbuild and run containerspulldownload an imagenginx:1.30-alpinethe image
docker imagesimageslist images
The tag (1.30-alpine) picks a version and variant. nginx releases stable versions with an even
minor number (1.30) and mainline with odd (1.31); -alpine is the small Alpine Linux base. Pin a tag in
anything you'll run twice: nginx alone means nginx:latest, which changes under you.
docker run
$ docker run -d --name web -p 8080:80 nginx:1.30-alpine 3893cb7f5c00f1dfad90d75368565eaee2507636450773077c73bb9f8e20cfb9 $ docker ps CONTAINER ID IMAGE STATUS PORTS NAMES 3893cb7f5c00 nginx:1.30-alpine Up 13 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp web
docker run -d --name web -p 8080:80 nginx:1.30-alpinedockerbuild and run containersrunstart a new container-drun in the background--name webname the container-p 8080:80publish a port, host:containernginx:1.30-alpinethe image to start
docker pspslist running containers
| Flag | Does |
|---|---|
-d | run in the background and print the container ID |
--name web | a name to use instead of the ID |
-p 8080:80 | publish container port 80 on host port 8080 |
-it | interactive with a terminal, for shells: docker run -it alpine:3.22 sh |
--rm | delete the container when it exits: for one-off commands |
-e KEY=value | set an environment variable inside |
Open http://localhost:8080 and nginx answers from inside the container:
Publishing ports
Each container has its own network namespace, so port 80 inside isn't port 80 on your machine. -p connects
them: host port first, container port second.
$ curl -sI localhost:8080 | head -2 HTTP/1.1 200 OK Server: nginx/1.30.6 $ docker run -d --name web2 -p 8081:80 nginx:1.30-alpine # a second nginx: also port 80 inside
curl -sI localhost:8080 | head -2curlmake an HTTP request-ssilent: no progress bar-Iheaders only|pipe: send output to the next commandheadshow the first lines
docker run -d --name web2 -p 8081:80 nginx:1.30-alpinedockerbuild and run containersrunstart a new container-drun in the background--name web2name the container-p 8081:80publish a port, host:containernginx:1.30-alpinethe image to start
Both containers listen on port 80 in their own namespace; on the host they're 8080 and 8081. Two containers
can't publish the same host port: the second fails with failed to bind host port 0.0.0.0:8080/tcp: address already in use.
0.0.0.0:8080 in docker ps means "every address of the host", so other machines can reach it. Bind to the
loopback only when that's what you want: -p 127.0.0.1:8080:80. Lesson 10 explains container networking
fully.
Logs and exec
$ docker logs web | tail -2 172.17.0.1 - - [16/Oct/2026:05:17:05 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.18.0" "-" 172.17.0.1 - - [16/Oct/2026:05:17:05 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.18.0" "-" $ docker logs -f --since 5m web # follow new lines, from the last 5 minutes $ docker exec web nginx -v # run a command inside nginx version: nginx/1.30.6 $ docker exec -it web sh # a shell inside; exit to leave (the container keeps running) / # ls /usr/share/nginx/html 50x.html index.html
docker logs web | tail -2dockerbuild and run containerslogsshow a container’s outputwebthe container|pipe: send output to the next commandtailshow the last lines
docker logs -f --since 5m web-fkeep following new output--since 5monly output newer than this
docker exec web nginx -vexecrun a command in a running containernginx -vthe command to run inside it
docker exec -it web sh-itinteractive, with a terminalshthe command to run inside it
docker logs shows what the main process writes to stdout and stderr. The requests come from
172.17.0.1: the default bridge network's gateway, which is how your curl reached the container.
Stop, start, remove
$ docker stop web # SIGTERM, then SIGKILL after 10 s $ docker ps -a --filter name=web --format 'table {{.Names}}\t{{.Status}}' NAMES STATUS web Exited (0) 3 seconds ago $ docker start web # the same container, the same files $ docker rm -f web web2 # stop if needed, then delete $ docker rmi nginx:1.30-alpine # delete the image (only if no container uses it)
docker stop webdockerbuild and run containersstopstop a container (SIGTERM, then SIGKILL)webthe container
docker ps -a --filter name=web --format 'table {{.Names}}\t{{.Status}}'pslist running containers-ainclude stopped ones too--filter name=webshow only matching items--format 'table {{.Names}}\t{{.Status}}'print only these fields (Go template)
docker start webstartstart a stopped container
docker rm -f web web2rmdelete a container-fstop it first if runningweb2the container
docker rmi nginx:1.30-alpinermidelete an imagenginx:1.30-alpinethe image
| Command | The process | The container and its files | The image |
|---|---|---|---|
docker stop | ended | kept | kept |
docker start | started again | same ones | kept |
docker rm | (must be stopped, or -f) | deleted | kept |
docker rmi | deleted |
Serve your own page from nginx
Run nginx in the background on host port 8080, replace its welcome page with your own using docker exec, prove it with curl, read the request in the logs, then stop, start and remove the container, checking what survives each step.
curl localhost:8080 showing your page, then a clean docker ps -aReveal one safe solution
$ docker run -d --name web -p 8080:80 nginx:1.30-alpine $ docker exec web sh -c 'echo "<h1>Hello from my container</h1>" > /usr/share/nginx/html/index.html' $ curl -s localhost:8080 <h1>Hello from my container</h1> $ docker logs web | tail -1 # your curl, logged $ docker stop web && docker start web && curl -s localhost:8080 <h1>Hello from my container</h1> $ docker rm -f web $ docker run -d --name web -p 8080:80 nginx:1.30-alpine && sleep 1 && curl -s localhost:8080 | grep -o '<title>.*</title>' <title>Welcome to nginx!</title> $ docker rm -f web
docker run -d --name web -p 8080:80 nginx:1.30-alpinedockerbuild and run containersrunstart a new container-drun in the background--name webname the container-p 8080:80publish a port, host:containernginx:1.30-alpinethe image to start
docker exec web sh -c 'echo "<h1>Hello from my container</h1>" > /usr/share/nginx/html/index.html'execrun a command in a running containerwebthe containersh -c 'echo "<h1>Hello from my container</h1>" > /usr/share/nginx/html/index.html'the command to run inside it
curl -s localhost:8080curlmake an HTTP request-ssilent: no progress bar
docker logs web | tail -1logsshow a container’s output|pipe: send output to the next commandtailshow the last lines
docker stop web && docker start web && curl -s localhost:8080stopstop a container (SIGTERM, then SIGKILL)&&run the next command only if this one workedstartstart a stopped container
docker rm -f webrmdelete a container-fstop it first if running
docker run -d --name web -p 8080:80 nginx:1.30-alpine && sleep 1 && curl -s localhost:8080 | grep -o '<title>.*</title>'grepfind lines that match a pattern-oprint only the matching part
Your page survives stop and start (same container, same writable layer), but not rm: the new container
starts from the untouched image, with nginx's default page. Lesson 6 shows how to keep data across containers.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
What is the difference between an image and a container?
Official reading for this lesson
Or get every quiz answer right and it completes itself.