The moment for serious practice has arrived. In this lesson you are not going to read theory: you are going to type, make mistakes and see results in your browser. You will start with the simplest possible container —one that prints something and dies— to understand the shortest lifecycle there is. Then you will bring up a real web server in the background, publish its port and visit it from the browser, learning along the way what -p 8080:80 means exactly, which is probably the option you will type most often in your career. You will walk through that container's full cycle: seeing it, reading its logs, stopping it, starting it and deleting it. Then you will go inside a container with an interactive shell to poke at its isolated filesystem with your own hands. And you will finish by serving the first Aurora Libros web page from a container, mounting a file from your machine inside it.

Contents

  1. Before you start: checks
  2. Your first container: in the foreground
  3. What happens when the process ends
  4. A service container in the background
  5. Port mapping explained
  6. The full cycle: logs, stop, start and rm
  7. An interactive container: going inside
  8. Serving the Aurora Libros page with -v
  9. Final cleanup

  1. Before you start: checks

Make sure everything is in order:

docker version
docker ps

The first must show the Client and Server blocks. The second, a table (probably empty) with no errors. If anything fails, go back to lesson 01-02.

Check as well that the port we are going to use is free:

ss -tln | grep 8080 || echo "Port 8080 free"

ss -tln lists the listening TCP ports (-t TCP, -l listening, -n without resolving names). If there are no matches, grep fails and the || runs the echo. If something were listening on 8080, use 8081 in every command in this lesson.

  1. Your first container: in the foreground

Let's start with the bare minimum:

docker run alpine:3.20 echo "Hello from Aurora Libros"
Hello from Aurora Libros

It looks trivial, but a lot has happened. Breaking down the line:

Part Meaning
docker run Creates a new container and starts it
alpine:3.20 The image to create it from
echo "Hello from Aurora Libros" The command to run inside the container, replacing the image's default command

And the internal sequence, which you already know from lesson 01-03: the client called the daemon, the daemon checked that it had alpine:3.20 locally, created a container by stacking a writable layer on the image's layers, asked containerd to start the echo process, that process wrote to its standard output, and that output was streamed through the socket to your terminal.

Since you did not use -d, the container runs in the foreground: your terminal stays connected to its output and does not get control back until the process ends.

Let's try something a little longer:

docker run alpine:3.20 sh -c "echo 'Catalog:'; echo '- Rayuela'; echo '- El jardín de senderos que se bifurcan'"
Catalog:
- Rayuela
- El jardín de senderos que se bifurcan

Here the command is sh -c "...": a shell is launched inside the container and handed a string with several orders. It is the usual pattern when you need to chain commands, because docker run only accepts an executable with its arguments, not a shell line with ; or &&.

And something that takes a while, so you can see the foreground for real:

docker run alpine:3.20 sh -c "for i in 1 2 3 4 5; do echo \"Processing order \$i\"; sleep 1; done"
Processing order 1
Processing order 2
Processing order 3
Processing order 4
Processing order 5

The lines come out one at a time, with a one-second pause. Your terminal is blocked in the meantime: that is the foreground. If you press Ctrl+C while it runs, you send an interrupt signal to the process and the container ends early.

  1. What happens when the process ends

This is the most important rule of the whole lesson:

A container lives exactly as long as its main process lives. When that process ends, the container moves to the Exited state. There is nothing else to "power down".

See for yourself:

docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Empty: none of the three previous containers is still alive.

docker ps -a --format "table {{.Names}}\t{{.Command}}\t{{.Status}}"
NAMES               COMMAND                  STATUS
happy_bardeen       "sh -c 'for i in 1 2…"   Exited (0) 10 seconds ago
festive_curie       "sh -c 'echo Catalog…"   Exited (0) 1 minute ago
vibrant_torvalds    "echo 'Hello from Au…"   Exited (0) 2 minutes ago

All three are there, stopped, with exit code 0 (they finished cleanly). Each takes up a little disk.

This behavior explains a classic beginner's stumble:

docker run alpine:3.20
docker ps

The second command shows nothing, and the typical reaction is "the container won't start". It did start: the alpine image's default command is /bin/sh, a shell that, having neither an interactive terminal nor any input, finds nothing to do and exits immediately. The container did its job and died in milliseconds.

For a container to stay alive it needs a process that does not end: a web server waiting for requests, a database, or a shell with an interactive terminal attached. That is what we will see now.

  1. A service container in the background

Let's bring up an Nginx web server, which is exactly that: a process that stays listening indefinitely.

docker run -d --name aurora-web-demo -p 8080:80 nginx:alpine
Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
f18232174bc9: Already exists
2c8d4f7e1a09: Pull complete
...
Status: Downloaded newer image for nginx:alpine
7c3e9a1f5b2d84e0a6c7d9f3b1e5a7c9d2f4b6e8a0c2d4f6b8e0a2c4d6f8b0e2

Let's analyze it option by option, because each one solves a specific problem:

Option Long name What it does
-d --detach Runs the container in the background and hands your terminal back immediately, printing its ID
--name aurora-web-demo Gives it a readable name instead of a random one. You will be able to refer to it by that name in every command
-p 8080:80 --publish Publishes the container's port 80 on your machine's port 8080
nginx:alpine The image. We give no command, so the image's own is used: start Nginx

That long hexadecimal string on the last line is the container's full ID. Since you used --name, you will not need it.

Now, then:

docker ps
CONTAINER ID   IMAGE          COMMAND                  STATUS         PORTS                                     NAMES
7c3e9a1f5b2d   nginx:alpine   "/docker-entrypoint.…"   Up 8 seconds   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   aurora-web-demo

Up 8 seconds: it is alive, and it will stay alive because Nginx never ends on its own. Note that docker ps without -a already shows it, unlike the previous containers.

  1. Port mapping explained

The PORTS column deserves a section of its own because it is where most people get stuck.

A container has its own network stack: its own interface, its own internal IP address and its own ports. Nginx listens on port 80 of that internal network, which is not directly reachable from your machine. The -p option builds a bridge.

flowchart LR
    NAV["Your browser<br/>http://localhost:8080"]
    subgraph HOST["Your machine (host)"]
        P8080["Port 8080<br/>(reserved by Docker)"]
        subgraph NET["Docker bridge network · 172.17.0.0/16"]
            subgraph CNT["Container aurora-web-demo · 172.17.0.2"]
                P80["Port 80<br/>Nginx listening"]
            end
        end
    end
    NAV --> P8080
    P8080 -->|"-p 8080:80<br/>forwarding"| P80

The syntax always reads from the outside in:

-p HOST_PORT:CONTAINER_PORT
  • The first is your machine's: the one you type in the browser. You choose it and it must be free.
  • The second is the container's: the one the application listens on. You do not choose it, the application determines it (Nginx uses 80, PostgreSQL 5432, Redis 6379, and the Aurora Libros API will use 3000).

Useful variants:

Syntax Effect
-p 8080:80 Port 8080 on all the host's interfaces → 80 in the container
-p 127.0.0.1:8080:80 Reachable only from your own machine, not from the local network. More secure
-p 80:80 The host's port 80 (on Linux it may require privileges)
-p 8080:80 -p 8443:443 Several ports published at once
-P Publishes every port declared by the image on random high ports

Without -p, the container works perfectly well but is unreachable from outside. That is the right thing for internal services: in module 4, aurora-db and aurora-cache will not publish ports, because they should only be reachable from aurora-api, not from the internet.

Checking with curl and with the browser

curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
...
</html>

Headers only, to see the HTTP status:

curl -I http://localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.27.4
Content-Type: text/html
Content-Length: 615

-I asks for headers only (HEAD method). The 200 OK confirms the server responds.

Now open http://localhost:8080 in your browser: you will see Nginx's welcome page. You have just served a website from a container without having installed Nginx on your system. If you run nginx -v in your terminal, the command most likely does not exist: everything is inside the container.

  1. The full cycle: logs, stop, start and rm

Viewing the logs

docker logs aurora-web-demo
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/08/04 10:15:03 [notice] 1#1: nginx/1.27.4
2026/08/04 10:15:03 [notice] 1#1: start worker processes
172.17.0.1 - - [04/Aug/2026:10:16:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0"
172.17.0.1 - - [04/Aug/2026:10:16:45 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0 ..."

There are your two visits: the one from curl and the one from the browser, identified by their user agent. The IP 172.17.0.1 is your machine's as seen from Docker's internal network.

Very useful options:

docker logs -f aurora-web-demo        # follow live (Ctrl+C to exit)
docker logs --tail 20 aurora-web-demo # only the last 20 lines
docker logs -t aurora-web-demo        # with Docker's timestamp
docker logs --since 5m aurora-web-demo # only the last 5 minutes

Try docker logs -f aurora-web-demo, reload the browser and you will see the line appear in real time. Ctrl+C stops the follow without affecting the container (a very common doubt: no, you are not stopping it).

Stopping the container

docker stop aurora-web-demo
aurora-web-demo

It returns the name of what it stopped. Verify:

docker ps
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
NAMES              STATUS                     PORTS
aurora-web-demo    Exited (0) 5 seconds ago

Two things: docker ps no longer shows it, and in docker ps -a the PORTS column is empty (the port 8080 mapping has been released). Confirm it:

curl http://localhost:8080
curl: (7) Failed to connect to localhost port 8080 after 0 ms: Connection refused

Nobody is listening. The container still exists, but it is not running.

Starting it again

docker start aurora-web-demo
curl -I http://localhost:8080
aurora-web-demo
HTTP/1.1 200 OK

It works again, with the same configuration. And here there is an important conceptual detail: you did not have to repeat -p 8080:80. The configuration was fixed when the container was created and is part of it. docker start merely resumes it.

Corollary: you cannot change the port mapping of an existing container. If you need a different port, you have to delete it and create a new one. It is a manifestation of the disposable-container philosophy.

docker logs aurora-web-demo

You will see the logs from the first startup and those from the second, one after the other: logs are preserved for as long as the container exists.

Deleting it

docker stop aurora-web-demo
docker rm aurora-web-demo
aurora-web-demo
aurora-web-demo

If you try docker rm without stopping it first:

Error response from daemon: cannot remove container "aurora-web-demo": container is running:
stop the container before removing or force remove

You can force it with docker rm -f aurora-web-demo, which stops and deletes in one step. It is convenient in development, but it kills the process without giving it time to shut down cleanly.

Verify:

docker ps -a
docker images

The container has disappeared from the list; the nginx:alpine image is still there. Once again: deleting a container does not delete its image.

  1. An interactive container: going inside

Up to now you have been running one-off commands. Now you are going to open a shell inside a container and move around in it.

docker run -it --rm alpine:3.20 sh

Three new options:

Option Long name What it does
-i --interactive Keeps standard input open, so you can type
-t --tty Allocates a virtual terminal: prompt, colors, formatting
--rm Deletes the container automatically when you exit

-i and -t almost always go together (-it): with -i and no -t you could type but with no prompt or formatting; with -t and no -i you would see the prompt but could not type.

Your prompt changes:

/ #

You are inside the container. Explore:

hostname
4a7f2b9e1c03

The hostname is the container's short ID, not your machine's.

ls /
bin    dev    etc    home   lib    media  mnt    opt    proc   root
run    sbin   srv    sys    tmp    usr    var

A complete Linux filesystem... that is not yours. Check it:

cat /etc/os-release
NAME="Alpine Linux"
PRETTY_NAME="Alpine Linux v3.20"

Alpine, even if your machine is Ubuntu, Windows or macOS.

ls /home
ls /root

Empty: your documents are not here. Filesystem isolation is real.

ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps aux

This is revealing: there are only two processes, and the shell has PID 1. On your machine there are hundreds of processes and PID 1 is systemd. The container has its own process namespace and does not see the host's (the exact mechanism, in lesson 05-07).

And yet:

uname -r
6.8.0-52-generic

The kernel is your host's. Alpine user space, Ubuntu kernel. That sentence sums up what a container is.

Installing something inside

Alpine uses apk as its package manager:

curl --version
sh: curl: not found

Not installed. Let's install it:

apk add --no-cache curl
fetch https://dl-cdn.alpinelinux.org/alpine/v3.20/main/x86_64/APKINDEX.tar.gz
(1/8) Installing ca-certificates (20240705-r0)
...
(8/8) Installing curl (8.12.1-r0)
Executing busybox-1.36.1-r29.trigger
OK: 13 MiB in 23 packages
  • apk add installs packages (the equivalent of apt install).
  • --no-cache avoids storing the package index on disk; it is the standard option in containers because it reduces size.
curl --version
curl 8.12.1 (x86_64-alpine-linux-musl) libcurl/8.12.1 ...

It works. And something important: you have installed curl in the container, not on your machine. Your system has not been touched.

Create a file as well, for the next check:

echo "Aurora Libros S.L." > /test.txt
cat /test.txt

Now leave:

exit

You are back at your normal prompt.

The magic of --rm

docker ps -a --filter "ancestor=alpine:3.20"

It does not show up. Thanks to --rm, the container was deleted automatically when its process ended. With it went its writable layer, the curl you installed and the /test.txt file.

Confirm it by opening another one:

docker run -it --rm alpine:3.20 sh
curl --version
cat /test.txt
exit
sh: curl: not found
cat: can't open '/test.txt': No such file or directory

This is lesson 01-05 in the flesh: every container is born clean from the immutable image. Changes live in its writable layer and die with it.

--rm is an excellent option for tests and one-off commands: it saves you from piling up stopped containers. Do not use it on services you want to be able to resume.

  1. Serving the Aurora Libros page with -v

Nginx serving its default page is fine for testing, but we want our content. Let's create the first Aurora Libros page.

Creating the page

mkdir -p ~/aurora-libros/web
cd ~/aurora-libros/web

Create index.html with this content:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Aurora Libros · Online bookshop</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 42rem; margin: 3rem auto; padding: 0 1rem; color: #222; }
    h1 { color: #6b3fa0; }
    li { margin-bottom: .6rem; }
    footer { margin-top: 2rem; font-size: .85rem; color: #666; }
  </style>
</head>
<body>
  <h1>Aurora Libros</h1>
  <p>Your online bookshop. Featured catalog:</p>
  <ul>
    <li><strong>El jardín de senderos que se bifurcan</strong> — Jorge Luis Borges — €14.50</li>
    <li><strong>Rayuela</strong> — Julio Cortázar — €19.90</li>
    <li><strong>Cien años de soledad</strong> — Gabriel García Márquez — €17.95</li>
  </ul>
  <footer>Aurora Libros S.L. · Served from a Docker container</footer>
</body>
</html>

Mounting it in the container

docker run -d --name aurora-web-demo -p 8080:80 \
  -v ~/aurora-libros/web/index.html:/usr/share/nginx/html/index.html:ro \
  nginx:alpine

The new option is -v (--volume), and its syntax reads just like -p, from the outside in:

-v PATH_ON_YOUR_MACHINE:PATH_INSIDE_THE_CONTAINER[:options]
Part Meaning
~/aurora-libros/web/index.html The file on your machine. It must be an absolute path (~ is expanded by the shell)
/usr/share/nginx/html/index.html Where it will appear inside the container. That path is where the Nginx image looks for its files
:ro read-only: the container can read it but not modify it. Good practice when it should not write

Check it:

curl http://localhost:8080
<!DOCTYPE html>
<html lang="en">
...
  <h1>Aurora Libros</h1>
...

And in the browser, at http://localhost:8080, you will see the Aurora Libros page with its catalog. The file is still on your disk; the container simply sees it mounted in its filesystem.

The advantage: live editing

Edit index.html in your editor and add a book:

    <li><strong>La sombra del viento</strong> — Carlos Ruiz Zafón — €21.00</li>

Save and reload the browser. The change shows up instantly, without rebuilding anything or restarting the container. The mount is a live window between your disk and the container.

This pattern —mounting the source code inside the container to develop without rebuilding— is the basis of the local development workflow you will see in lesson 04-07.

Important note. This use of -v is purely introductory: it is a bind mount, which mounts a path from your machine. There is another kind, Docker-managed volumes, which is what aurora-db will need so as not to lose the catalog. The differences, when to use each and the permission and performance implications are the full content of lesson 03-06.

  1. Final cleanup

Let's leave the system as we found it:

docker stop aurora-web-demo
docker rm aurora-web-demo
docker container prune -f
  • The first two stop and delete the Nginx container.
  • container prune -f removes any other stopped container you may have left along the way.

Check:

docker ps -a
docker images
docker system df

No containers; the images (alpine:3.20, nginx:alpine, hello-world) untouched, ready to be reused without downloading them again. Your ~/aurora-libros/web/index.html file is still in place too: you will need it in the coming lessons.

Common Mistakes and Tips

  • Bind for 0.0.0.0:8080 failed: port is already allocated. The host port is already taken, almost always by another of your containers. Find it with docker ps and stop it, or publish on a different port (-p 8081:80).
  • Reversing the order of -p. -p 80:8080 with Nginx does not work: you would be forwarding the host's port 80 to the container's 8080, where nobody is listening. Remember: host:container, from the outside in.
  • docker run -d with a command that finishes. docker run -d alpine echo hello starts and dies instantly. -d keeps nothing alive: it only detaches your terminal. To stay alive you need a process that does not end.
  • Relative paths in -v. -v ./web:/usr/share/nginx/html may fail depending on the version and the shell. Always use absolute paths: $(pwd)/web or ~/....
  • Mounting a directory over one that already has content. The mount hides whatever was at that path inside the image. If you mount an empty directory over /usr/share/nginx/html, Nginx will return 403 or 404 because it no longer sees its index.html.
  • Confusing Ctrl+C with stopping the container. In docker logs -f or docker attach, Ctrl+C only cuts your connection. In a foreground container, it does interrupt the process and end it.
  • Tip: use --rm for anything disposable. Tests, one-off commands, exploratory shells. It will save you cleanups.
  • Tip: always name with --name. aurora-web-demo is infinitely more manageable than stupefied_swanson, and it makes name filters work.
  • Tip: if a container dies right after starting, docker logs first. Logs are preserved even when it is stopped, and the explanation is almost always there.

Exercises

Exercise 1: two storefronts at once

Bring up two Nginx containers simultaneously from the same image: one called aurora-web-es on port 8080 and another aurora-web-en on 8081, each serving a different index.html (one in Spanish and one in English). Check with curl that each port returns its own page. Then answer: how many copies of the nginx:alpine image are on your disk? How much does each container's writable layer take up (docker ps -as)? When you are done, delete both in a single command.

Exercise 2: the log detective

Run this container, which is deliberately misconfigured:

docker run -d --name aurora-fail -p 8080:80 \
  -v /tmp/unused-folder:/usr/share/nginx/html:ro \
  nginx:alpine

(Create the empty /tmp/unused-folder beforehand with mkdir -p /tmp/unused-folder.)

Visit http://localhost:8080 and observe the error. Then: (a) is the container running?, (b) which HTTP code does it return and why?, (c) use docker logs to find Nginx's exact error message, and (d) explain in terms of mounts what has happened and how you would fix it.

Exercise 3: comparative interactive exploration

Open an interactive alpine:3.20 container and an nginx:alpine one (in two terminals, both with --rm). In each, find out: (a) the user space's operating system, (b) the kernel version, (c) PID 1 and how many processes there are, and (d) whether the /usr/share/nginx/html directory exists and what it contains. Explain the differences and the similarities between the two containers, and what each one tells you about the nature of containers.

Solutions

Solution to exercise 1

mkdir -p ~/aurora-libros/web-es ~/aurora-libros/web-en
echo '<h1>Aurora Libros</h1><p>Bienvenido a nuestra librería.</p>' > ~/aurora-libros/web-es/index.html
echo '<h1>Aurora Books</h1><p>Welcome to our bookstore.</p>' > ~/aurora-libros/web-en/index.html

docker run -d --name aurora-web-es -p 8080:80 \
  -v ~/aurora-libros/web-es:/usr/share/nginx/html:ro nginx:alpine

docker run -d --name aurora-web-en -p 8081:80 \
  -v ~/aurora-libros/web-en:/usr/share/nginx/html:ro nginx:alpine

curl -s http://localhost:8080
curl -s http://localhost:8081
<h1>Aurora Libros</h1><p>Bienvenido a nuestra librería.</p>
<h1>Aurora Books</h1><p>Welcome to our bookstore.</p>

Here directories are mounted instead of individual files, which is the usual approach with Nginx. The two containers use different ports on the host (8080 and 8081) but both use 80 inside: there is no conflict, because each container has its own network stack and its own port 80.

docker images nginx
docker ps -as --format "table {{.Names}}\t{{.Size}}"
REPOSITORY   TAG      IMAGE ID       SIZE
nginx        alpine   3f8a4339aadd   52.5MB
NAMES            SIZE
aurora-web-en    1.09kB (virtual 52.5MB)
aurora-web-es    1.09kB (virtual 52.5MB)

Answers: there is only one copy of the image on disk, shared by both containers. Each writable layer takes up about 1 kB. The "virtual 52.5MB" includes the image's shared layers, which are not counted twice. Bringing up the second container cost practically zero disk: it is the density from lesson 01-01, measured.

docker rm -f aurora-web-es aurora-web-en

rm -f stops and deletes in one step, and accepts several names.

Solution to exercise 2

(a) Yes, the container is running:

docker ps --filter "name=aurora-fail"
CONTAINER ID   IMAGE          STATUS         PORTS                  NAMES
b2c8f4a1e7d9   nginx:alpine   Up 20 seconds  0.0.0.0:8080->80/tcp   aurora-fail

Nginx started correctly. The problem is not the startup, but the content it serves.

(b) It returns 403 Forbidden:

curl -I http://localhost:8080
HTTP/1.1 403 Forbidden
Server: nginx/1.27.4

It is 403 and not 404 because the root directory exists (you mounted it) but is empty: there is no index.html to serve and Nginx does not allow directory listing by default.

(c) The logs:

docker logs aurora-fail
2026/08/04 11:02:41 [error] 30#30: *1 directory index of "/usr/share/nginx/html/" is forbidden,
client: 172.17.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost:8080"

The message is explicit: directory index ... is forbidden.

(d) What happened: the mount hides the original content at that path in the image. The nginx:alpine image ships its own index.html in /usr/share/nginx/html, but by mounting an empty directory from your machine on top, that content stops being visible (it is not deleted: it is still in the image's layer, simply covered up). Nginx finds an empty directory and answers 403.

Possible fixes: put an index.html in /tmp/unused-folder, mount a directory that does have content, or mount nothing at all if you wanted the default page.

echo '<h1>Aurora Libros</h1>' > /tmp/unused-folder/index.html
curl -s http://localhost:8080
docker rm -f aurora-fail

Note that you do not need to restart the container: the mount is live, so as soon as the file exists on your disk, Nginx serves it.

Solution to exercise 3

# Terminal 1
docker run -it --rm alpine:3.20 sh

# Terminal 2
docker run -it --rm nginx:alpine sh

(The Nginx image also ships a shell, so you can go into it the same way.)

In each one:

cat /etc/os-release | head -2
uname -r
ps aux
ls /usr/share/nginx/html

Results and how to read them:

Check alpine:3.20 nginx:alpine What it means
(a) User space OS Alpine Linux v3.20 Alpine Linux (the version of Nginx's base image) Both are built on Alpine: they share base layers, as you saw in 01-05
(b) Kernel Your host's (e.g. 6.8.0-52-generic) The same Confirms that both containers share the host's kernel. There is no guest OS
(c) PID 1 and process count sh as PID 1, 2 processes sh as PID 1, 2 processes Each container has its own process namespace, isolated. And note: Nginx is not running here, because by passing sh you replaced the image's default command
(d) /usr/share/nginx/html Does not exist (ls: ... No such file or directory) Exists, with index.html and 50x.html The filesystem comes from the image: they differ because the images ship different content

The important conclusions: containers share the kernel (b) but have isolated filesystems and process namespaces (c, d); two images can share their base and differ only in the upper layers (a); and passing a command to docker run replaces the image's default command, which is why going into the Nginx image with sh gives you Nginx's filesystem without Nginx running.

Conclusion

You have now created, managed and destroyed containers with your own hands. You take away the rule that governs everything else: a container lives as long as its main process lives; if that process ends, the container moves to Exited, and that is why docker run alpine "does nothing" while docker run nginx:alpine stays alive indefinitely.

You know how to launch containers in the foreground and in the background with -d, how to name them with --name, and above all how to publish ports with -p host:container, always reading the syntax from the outside in; you have confirmed that the mapping is fixed when the container is created and that docker start resumes it with the same configuration. You have walked the full cycle —logs, stop, start, rm—, you have gone inside a container with -it to see with your own eyes the filesystem and process isolation alongside the shared kernel, and you have confirmed with --rm that everything you write inside dies with the container. And you have served the first Aurora Libros page by mounting a file from your disk with -v, live editing included.

You have the three pieces of the puzzle —images, containers and commands— and a working web page. What you do not yet have is the real problem that justifies all of this. In the module's final lesson, The Course Project: The Aurora Libros Platform, you will get to know the company, its target architecture and its API's complete code, and you will try to start it without Docker so as to experience first-hand the ordeal that the rest of the course is going to solve.

Docker: From Beginner to Advanced

Module 1: Introduction to Docker

Module 2: Working with Docker Images

Module 3: Docker Containers

Module 4: Docker Compose

Module 5: Advanced Docker Concepts

Module 6: Docker in Production

Module 7: Docker Ecosystem and Tools

© Copyright 2026. All rights reserved