Your auroralibros/aurora-api:1.1.0 image is published and it is spotless: unprivileged user, OCI metadata, healthcheck and a clean 0.3-second shutdown. And it still returns ECONNREFUSED on /books, exactly as the previous lesson warned you. This entire module exists to fix that, and it starts where it has to start: really understanding the command you have been using since lesson 01-06 without ever looking at it closely. docker run is not one command, it is two: it creates a container and it starts it. And it accepts more than a hundred options, of which about fifteen get used in day-to-day work. In this lesson you are going to see what run does under the hood, you are going to learn those fifteen options grouped into families, you are going to override an image's CMD and ENTRYPOINT from the command line, and you are finally going to bring up aurora-db and aurora-cache as real containers. By the end you will have three live containers... and the API still will not work. That failure, with its error message changed, is the clue that kicks off the rest of the module.
Contents
- What
docker runreally does:create+start - Anatomy of the command and the order of the arguments
- Identity family:
--nameand--hostname - Execution family:
-d,-it,--rm,-w,-u,--entrypoint - Network and ports family: the bare minimum to work
- Configuration family:
-eand--env-file - Data family:
-vin its minimal form - Overriding
CMDandENTRYPOINTfrom the command line - Foreground, background and how to detach
docker attachversusdocker exec- Hands-on: the Aurora Libros database and cache
- What
docker run really does: create + start
docker run really does: create + startdocker run is a shortcut. Underneath it performs two distinct operations that you can invoke separately:
Docker prints the container's full ID (64 hexadecimal characters) and hands control back to you. Notice what has happened and what has not:
The container exists, it has its writable layer reserved and all its configuration stored (ports, variables, command), but the state is Created and the PORTS column is empty: no process is running and port 8080 is not listening on your machine. Check it:
Now start it:
docker start prints the name of the container it started. And now, yes:
docker ps --filter name=lifecycle-test --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080This separation is not an academic detail: it explains the division of responsibilities you are going to rely on throughout the module.
| Phase | What it does | What can be changed afterwards |
|---|---|---|
create |
Reserves the writable layer, stores the configuration (name, ports, variables, mounts, command, limits) | Almost nothing: ports, variables and mounts are frozen forever |
start |
Creates the namespaces, applies the cgroups and launches the PID 1 process | Can be repeated as many times as you like (stop/start) |
The practical consequence, worth burning into your memory right now: you cannot add a port or an environment variable to a container that already exists. If you got it wrong, you delete it and create it again. That is exactly why in module 4 you will end up writing that configuration into a file instead of on the command line.
Clean up the demonstration:
The complete flow, now that you know the pieces from lesson 01-03:
flowchart TD
A["docker run -d -p 8080:80 nginx:alpine"] --> B{"Is the image<br/>local?"}
B -- No --> C["implicit docker pull<br/>from the registry"]
B -- Yes --> D["CREATE PHASE"]
C --> D
D --> D1["Writable layer (copy-on-write)"]
D --> D2["Configuration: name, ports,<br/>variables, command"]
D1 --> E["START PHASE"]
D2 --> E
E --> E1["Namespaces: pid, net, mnt, uts, ipc"]
E --> E2["Cgroups: memory and CPU"]
E --> E3["Network rules and port publishing"]
E1 --> F["runc launches the PID 1 process"]
E2 --> F
E3 --> F
F --> G["Container in running state"]
- Anatomy of the command and the order of the arguments
There is a single golden rule, and it causes half of all beginner errors:
Everything before the image is a Docker option. Everything after the image is the command that runs inside the container.
Watch it fail:
docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: exec: "-e": executable file not found in $PATHDocker did not read -e as one of its own options: because it came after alpine:3.20, it tried to run a program called -e inside the container. The correct version:
Here --rm and -e MESSAGE=hello are Docker options (they go before the image) and sh -c 'echo $MESSAGE' is the command inside the container (it goes after). We need sh -c because expanding $MESSAGE is a shell's job, and without it Docker would pass the literal string.
An overview of the families you will meet in the sections that follow:
| Family | Options | What for |
|---|---|---|
| Identity | --name, --hostname, --label |
Being able to refer to the container and organize it |
| Execution | -d, -it, --rm, -w, -u, --entrypoint |
How and with what identity the process runs |
| Network and ports | -p, -P, --expose, --network |
Who can talk to it (in depth in 03-05) |
| Configuration | -e, --env-file |
What values the application reads |
| Data | -v, --mount, --tmpfs |
What outlives the container (in depth in 03-06) |
| Resources and resilience | -m, --cpus, --restart |
How much it may consume and what happens if it fails (in depth in 03-07) |
- Identity family:
--name and --hostname
--name and --hostname--name
Without --name, Docker invents a two-word name (nostalgic_hopper, elegant_bardeen) and forces you to copy IDs around. With a name, every subsequent command becomes readable and scriptable:
The rules for names:
| Rule | Detail |
|---|---|
| Valid characters | [a-zA-Z0-9][a-zA-Z0-9_.-]* |
| Unique across the whole machine | Stopped containers included |
| Doubles as a DNS name | On a user-defined network, another container resolves it by that name (lesson 03-05) |
That last point is the key to the whole module, and it is why the Aurora Libros names (aurora-db, aurora-cache, aurora-api, aurora-web) are not decorative: very soon they will be real hostnames.
The most common mistake with --name:
docker: Error response from daemon: Conflict. The container name "/aurora-cache" is already in use by container "a1b2c3d4e5f6".
You have to remove (or rename) that container to be able to reuse that name.Careful: the conflict happens even if the previous container is stopped. An Exited container still occupies its name. You resolve it with docker rm aurora-cache (or docker rm -f if it is running).
--hostname
It changes the name the container sees for itself inside its own UTS namespace:
Without --hostname, the hostname is the container's short ID:
--name and --hostname are different things: --name is how you call it from outside (and how other containers call it over DNS); --hostname is what the container calls itself. It gets little use, but it shows up in application logs and in database clusters.
- Execution family:
-d, -it, --rm, -w, -u, --entrypoint
-d, -it, --rm, -w, -u, --entrypoint| Option | What it does | When you use it |
|---|---|---|
-d, --detach |
Starts in the background and prints the ID | Services: databases, APIs, web servers |
-i, --interactive |
Keeps standard input open | When you are going to type something to the process |
-t, --tty |
Allocates a pseudo-terminal (prompt, colors, Ctrl+C) |
Interactive shells |
-it |
The combination of the previous two | Getting into a container with sh or bash |
--rm |
Deletes the container when it finishes | One-shot commands and tests |
-w, --workdir |
Working directory, overrides the image's WORKDIR |
Running something in a different path without rebuilding |
-u, --user |
User/UID the process runs as | Adjusting permissions or getting into an unprivileged image as root |
--entrypoint |
Replaces the image's ENTRYPOINT |
Debugging an image whose fixed executable gets in the way |
-d versus the foreground
It returns the ID and leaves your terminal free. Without -d, the terminal stays busy showing the process's output and Ctrl+C stops it.
-it and why you need both letters
Try dropping one of the two letters to understand what each one contributes:
| Command | Result |
|---|---|
docker run --rm alpine:3.20 sh |
The shell starts, finds no input and exits immediately. You are back at the host prompt |
docker run --rm -i alpine:3.20 sh |
It works: you can type commands, but with no prompt, no colors and no history |
docker run --rm -t alpine:3.20 sh |
You see the / # prompt, but what you type never arrives: the container has no stdin. It hangs |
docker run --rm -it alpine:3.20 sh |
A complete interactive session |
A practical rule: -it for people, -d for services, nothing for one-shot commands. And never -it in an automated script or in CI: with no real terminal, -t triggers the input device is not a TTY.
--rm
The container runs, prints and disappears: it does not linger in docker ps -a and it takes up no writable layer. It is the option that stops you accumulating dozens of dead containers.
Two important warnings:
--rmis incompatible with debugging: if the container fails, it is deleted along with its logs and itsinspect. When something goes wrong, drop the--rmso you can investigate (lesson 03-04).--rmalso deletes the associated anonymous volumes. With named volumes nothing happens, but it is a detail we come back to in lesson 03-06.
-w and -u
docker run --rm -w /tmp alpine:3.20 pwd
docker run --rm -u 1000:1000 alpine:3.20 id
docker run --rm -u root auroralibros/aurora-api:1.1.0 id-w /tmpoverrides theWORKDIR /appyou set in the Dockerfile.-u 1000:1000forces a specific UID:GID even if that user does not exist inside the image (which is whyidshows the numbers without names).-u rootcancels theUSER nodein your image. It is extremely handy for debugging (installing a tool inside an unprivileged container), and at the same time a reminder thatUSERis defense in depth, not an impassable barrier: whoever can launch containers can choose the user.
--entrypoint
You already used it in lesson 02-04, and here it gets its proper place:
/app $ ls
Dockerfile node_modules package-lock.json package.json server.js
/app $ node --version
v22.13.0Without --entrypoint, your image has a fixed ENTRYPOINT ["node"] and anything you wrote after the image would be an argument to node. With --entrypoint sh you get in and look around. It is the service door of every well-built image.
- Network and ports family: the bare minimum to work
Only the essentials here; the full explanation arrives in lesson 03-05.
-p HOST:CONTAINER means "anything arriving at port 8080 on my machine, forward it to port 80 of this container". The order is never reversed: host first, container second.
| Syntax | Meaning |
|---|---|
-p 8080:80 |
Port 8080 on all host interfaces → port 80 of the container |
-p 127.0.0.1:8080:80 |
Only from your own machine; not visible on the local network |
-p 80 |
Port 80 of the container to a random free port on the host |
-p 8080:80/udp |
Publishes UDP instead of TCP |
-P |
Publishes all ports declared with EXPOSE, each on a random port |
--expose 9000 |
Declares the port in the metadata, without publishing it on the host |
Check -P with your own image, which declares EXPOSE 3000:
Docker picked 32768 from the ephemeral range. docker port is the command that answers "which host port did this end up on?".
Two ideas worth fixing right away, even though they are developed in 03-05:
EXPOSEin the Dockerfile and--exposeinrunopen nothing: they are documentation consumed by-Pand other tools.- Publishing a port is what lets you, from the host, reach the container. For two containers to talk to each other, nothing needs to be published. That distinction is the one that is about to solve the API's mystery.
Clean up:
- Configuration family:
-e and --env-file
-e and --env-fileYour server.js does not have a single hardcoded value: it reads PORT, DB_HOST, DB_USER, DB_PASSWORD, DB_NAME and REDIS_HOST from the environment. That decision from lesson 01-07 is what lets the same image serve your laptop, testing and production.
-e in its three forms
docker run --rm -e GREETING="Hello Aurora" alpine:3.20 printenv GREETING
export LOCAL_TOKEN=abc123
docker run --rm -e LOCAL_TOKEN alpine:3.20 printenv LOCAL_TOKEN
docker run --rm auroralibros/aurora-api:1.1.0 --version| Form | Effect |
|---|---|
-e KEY=value |
Defines the variable with that literal value |
-e KEY |
Copies whatever value that variable has in the host's shell |
No -e |
The application uses the image's ENV or its default value in the code |
The second form is very practical for keeping secrets off the command line, where they would end up in the shell history.
Variables defined with -e override those from the Dockerfile's ENV. Check it with your image, which ships ENV PORT=3000:
And look at the whole precedence ladder again:
| Precedence | Source | Example |
|---|---|---|
| 1 (wins) | -e / --env-file on docker run |
-e PORT=4000 |
| 2 | The Dockerfile's ENV |
ENV PORT=3000 |
| 3 | Default value in the code | process.env.PORT || 3000 |
--env-file
Once you pass three variables, the command line becomes unreadable. Create ~/aurora-libros/aurora.env:
# Local Aurora Libros configuration — NOT baked into the image
PORT=3000
DB_HOST=aurora-db
DB_PORT=5432
DB_USER=aurora
DB_PASSWORD=aurora_secret
DB_NAME=aurora_books
REDIS_HOST=aurora-cache
REDIS_PORT=6379And use it:
docker run --rm --env-file ~/aurora-libros/aurora.env \
--entrypoint printenv auroralibros/aurora-api:1.1.0 DB_HOST DB_NAMEThe format rules are strict and different from a shell script's. This is a spot where a lot of time gets lost:
| Rule | Correct | Wrong, and why |
|---|---|---|
One variable per line, KEY=value |
DB_USER=aurora |
export DB_USER=aurora → the key would be export DB_USER |
| No quotes unless they are part of the value | DB_PASSWORD=aurora_secret |
DB_PASSWORD="aurora_secret" → the password would include the quotes |
| No variable expansion | DATA_PATH=/app/data |
DATA_PATH=$HOME/data → the literal value would be $HOME/data |
Comments with # at the start of the line |
# comment |
DB_PORT=5432 # the port → the value would be 5432 # the port |
No spaces around the = |
PORT=3000 |
PORT = 3000 → the key would be PORT |
And a security rule you already know from module 2, now with its concrete consequence:
echo "aurora.env" >> ~/aurora-libros/.gitignore
echo "aurora.env" >> ~/aurora-libros/api/.dockerignoreThe environment file contains a password: it goes neither into the Git repository nor into the build context. The image stays free of secrets —as it should— and the configuration travels outside it, injected at startup. You can combine both options: --env-file for the bulk and -e for whatever you want to override on the spot, because -e beats --env-file regardless of the order you write them in.
- Data family:
-v in its minimal form
-v in its minimal formYou used it in lesson 01-06 to serve your index.html with Nginx:
docker run -d --name aurora-web-demo -p 8080:80 \
-v ~/aurora-libros/web/index.html:/usr/share/nginx/html/index.html:ro \
nginx:alpineThe minimal syntax is -v SOURCE:DESTINATION[:options], with the host path always absolute and :ro to mount read-only. That is enough until lesson 03-06, where you will see the three types of mount, managed volumes and why the book catalog you are about to create will vanish if you do nothing about it. For now, hold on to the idea from lesson 01-05: everything a container writes outside a mount lives in its writable layer and dies with it.
- Overriding
CMD and ENTRYPOINT from the command line
CMD and ENTRYPOINT from the command lineWe pick up the table from lesson 02-04, now from the execution side. Your image declares:
The effective command is the concatenation ENTRYPOINT + CMD → node server.js. And from docker run you can touch each half separately:
| Command | ENTRYPOINT |
CMD |
What runs | Result |
|---|---|---|---|---|
docker run IMG |
node |
server.js |
node server.js |
Starts the API |
docker run IMG --version |
node |
--version |
node --version |
Prints v22.13.0 |
docker run IMG -e "console.log(2+2)" |
node |
-e console.log(2+2) |
node -e "console.log(2+2)" |
Prints 4 |
docker run --entrypoint sh IMG |
sh |
(discarded) | sh |
Interactive shell |
docker run --entrypoint sh IMG -c "ls /app" |
sh |
-c ls /app |
sh -c "ls /app" |
Lists the directory |
docker run --entrypoint "" IMG ls -la /app |
(none) | ls -la /app |
ls -la /app |
Runs ls directly |
Check it:
docker run --rm auroralibros/aurora-api:1.1.0 -e "console.log('Aurora ' + (2+2))"
docker run --rm --entrypoint sh auroralibros/aurora-api:1.1.0 -c "ls /app | head -3"
docker run --rm --entrypoint "" auroralibros/aurora-api:1.1.0 ls -la /app/server.jsAurora 4
Dockerfile
node_modules
package-lock.json
-rw-r--r-- 1 node node 4187 Aug 4 19:12 /app/server.jsTwo subtleties that catch people out:
- Overriding the
ENTRYPOINTdiscards the image'sCMD. In row 4 of the table,CMD ["server.js"]disappears: if you did not wantshto receiveserver.jsas an argument, you have nothing to do, it is already gone. --entrypoint ""(empty string) is the way to leave the image with no fixed executable, so that whatever you write after the image is the complete command. It is the trick that saves the day with images whose entrypoint is a complicated script.
- Foreground, background and how to detach
When you start without -d, your terminal stays hooked to the input and output of PID 1:
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/08/04 19:41:07 [notice] 1#1: start worker processesAnd there it stays. If you press Ctrl+C, you send SIGINT to the process and the container stops. That is rarely what you want with a service.
The alternative, if you started with -it, is to detach without killing it using the sequence Ctrl+P followed by Ctrl+Q:
Still alive. Three conditions for the sequence to work, and their absence explains 90% of the "it doesn't work for me" cases:
- The container must have been started with
-t(it needs the pseudo-terminal). - It must be hooked to stdin, that is, with
-i. - The sequence is pressed inside the session, not in another terminal.
If you need Ctrl+P for something else (in bash it recalls the previous command), change it:
Now the combination is Ctrl+E followed by e. You can make it permanent in ~/.docker/config.json:
A summary of the three ways to start:
| Form | Command | Terminal | Ctrl+C |
|---|---|---|---|
| Foreground | docker run IMG |
Busy showing the output | Stops the container |
| Interactive foreground | docker run -it IMG sh |
Busy, with a session | Goes to the process inside |
| Background | docker run -d IMG |
Free immediately | Not applicable |
Clean up:
docker attach versus docker exec
docker attach versus docker execA container in the background can be "looked at again" in two radically different ways:
docker attach connects you to the input and output of the PID 1 process that already exists. It launches nothing new. And that is where its danger lies:
You just stopped Redis. Ctrl+C during an attach goes straight to PID 1. To leave without killing it you have to use Ctrl+P Ctrl+Q, or connect in safe mode from the start:
With --sig-proxy=false, Ctrl+C gives you the prompt back without sending the signal to the container.
docker exec, in contrast, launches a new process inside the container:
That redis-cli has nothing to do with PID 1: it is a second process that shares the container's namespaces and that can finish without affecting the service.
docker attach |
docker exec |
|
|---|---|---|
| What it does | Connects to the existing PID 1 | Creates a new process |
| Can it be used several times at once? | Yes, but everyone sees the same thing | Yes, independently |
Ctrl+C |
Kills the service (unless --sig-proxy=false) |
Ends only your process |
| Leaving without damage | Ctrl+P Ctrl+Q |
exit |
| Typical use | Watching the live output of an interactive process | Opening a shell, running diagnostics |
| Recommendation | Avoid it except in specific cases | The default choice |
The practical rule: to watch output use docker logs, and to get in use docker exec. Both are studied in depth in lesson 03-04. docker attach is reserved for processes that genuinely wait for your keyboard input.
- Hands-on: the Aurora Libros database and cache
The moment has arrived. Until now the API started on its own and failed because there was neither PostgreSQL nor Redis. Let's put them in place.
The database
docker run -d \
--name aurora-db \
-e POSTGRES_USER=aurora \
-e POSTGRES_PASSWORD=aurora_secret \
-e POSTGRES_DB=aurora_books \
-p 127.0.0.1:5432:5432 \
postgres:16-alpineLet's go over each line, because each one has its reason:
| Fragment | Why |
|---|---|
-d |
It is a service: we do not want the terminal tied up |
--name aurora-db |
Two lessons from now this name will be the hostname the API uses |
-e POSTGRES_USER/PASSWORD/DB |
These are the three variables the official PostgreSQL image uses to initialize itself the first time. They match what server.js expects |
-p 127.0.0.1:5432:5432 |
We publish locally only so we can connect with a client from the host. If you wrote -p 5432:5432, your database would be reachable from the entire local network |
postgres:16-alpine |
The same image you pulled in module 2 |
Check that it is alive:
docker ps --filter name=aurora-db --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
docker exec aurora-db pg_isready -U auroraNAMES IMAGE STATUS
aurora-db postgres:16-alpine Up 12 seconds
/var/run/postgresql:5432 - accepting connectionspg_isready is PostgreSQL's official tool for asking "are you accepting connections?". We run it inside the container with docker exec, with no need to have PostgreSQL installed on the host. That detail —using the tools that already ship in the image— is one of Docker's most underrated conveniences.
And check that the database exists, even if it is still empty:
Correct: the aurora_books database is created, but there are no tables. db/init.sql still needs to run, and that will happen automatically and elegantly in lesson 03-06, when you mount that file inside the container.
The cache
Redis needs no variables: it starts with its default configuration.
docker exec aurora-cache redis-cli PING
docker exec aurora-cache redis-cli SET test "Aurora Libros"
docker exec aurora-cache redis-cli GET testRedis works. Two live containers.
And now the API... which still does not work
docker run -d \
--name aurora-api \
-p 3000:3000 \
--env-file ~/aurora-libros/aurora.env \
auroralibros/aurora-api:1.1.0Docker accepted the command without complaining. But:
The container did not even stay running. It started and died with exit code 1. Let's see why:
[cache] error: getaddrinfo ENOTFOUND aurora-cache
[aurora-api] failed to start: getaddrinfo ENOTFOUND aurora-cacheLook carefully at the message, because it has changed since module 2 and that change is a first-rate clue:
| Configuration | Error | What it means |
|---|---|---|
DB_HOST=localhost (module 2) |
ECONNREFUSED 127.0.0.1:5432 |
The name did resolve (to itself), but nobody is listening on that port inside the container |
DB_HOST=aurora-db (now) |
getaddrinfo ENOTFOUND aurora-db |
The name could not even be resolved: as far as this container is concerned, aurora-db does not exist |
And here is the paradox that gives the rest of the module its purpose: aurora-db and aurora-cache are running right now, on the same machine, publishing their ports. You can verify it from the host:
NAMES STATUS PORTS
aurora-cache Up 4 minutes 127.0.0.1:6379->6379/tcp
aurora-db Up 6 minutes 127.0.0.1:5432->5432/tcpThey are alive, they are published, and even so aurora-api cannot find them. Why?
The short answer: because they do not share a network. Each container has its own network stack, and on Docker's default network there is no name resolution between containers. The long answer, with the complete solution and the curl that returns the eight books, is lesson 03-05. For now, leave the question open and delete nothing: these two containers will keep you company throughout the module.
We only delete the API (it is stopped and it is useless). aurora-db and aurora-cache stay.
Common Mistakes and Tips
- Putting options after the image.
docker run alpine -e VAR=1 shtries to run a program called-e. Options go before the image, always. - Expecting to add a port or a variable to an existing container. You cannot: they are frozen in the
createphase. The solution isdocker rmand create it again. When that starts to wear you down, you will be ready for module 4. - Using
--rmwhile debugging. If the container fails and gets deleted, you are left with no logs and noinspect. During an investigation, drop the--rm. -p 5432:5432on a database. It publishes PostgreSQL on all interfaces, including the café's Wi-Fi. Use-p 127.0.0.1:5432:5432, or simply do not publish it: containers do not need published ports to talk to each other.- Reversing the order in
-p.-p 80:8080with Nginx does not work and the error is confusing, because Docker happily publishes host port 80 towards an 8080 where nobody is listening. Host first, container second. -tin scripts and CI.the input device is not a TTY. In automation use-ior nothing, never-t.- Quotes in an
--env-file.DB_PASSWORD="aurora_secret"stores the password with the quotes included and causes a bafflingpassword authentication failed. No quotes. - Tip: always use
--name. An unnamed container is a container you will be hunting for by ID ten minutes from now in a twenty-linedocker ps -a. - Tip: write long
docker runcommands across several lines with a trailing\, and keep them in a notes file. You are going to repeat them many times, and in lesson 03-07 you will see how much they have grown.
Exercises
Exercise 1: demonstrate the separation between create and start
Without using docker run at any point, create an nginx:alpine container called lifecycle-exercise published on port 8090 and with the variable ENVIRONMENT=tests. Before starting it, answer with commands: what state is it in? Does port 8090 respond? Does it show up in docker ps? Then start it, check the three things again, and try to add a second port (8091) without deleting it. Explain what happens and why.
Exercise 2: master overriding ENTRYPOINT and CMD
Using only the auroralibros/aurora-api:1.1.0 image and without rebuilding it, produce these five outputs, writing the exact command in each case:
- The installed Node version.
- The contents of
/app/package.json. - An interactive shell inside the container.
- The result of
console.log(process.env.DB_HOST)withDB_HOSTset toaurora-db. - The file listing of
/appby runninglsdirectly, with nonodeand noshinvolved.
Exercise 3: file-based configuration for two environments
Create two environment files, aurora-dev.env and aurora-test.env, that differ in PORT (3000 and 3100), DB_NAME (aurora_books and aurora_books_test) and REDIS_HOST. Start two API containers with the same command except for the --env-file and the --name, verify with printenv that each one has its own configuration, and then start a third that uses aurora-dev.env but with PORT=3200 overridden from the command line. Answer: how many different images did you need? And which line of your .gitignore stops this ending up in the repository?
Solutions
Solution to exercise 1
State before starting:
docker ps -a --filter name=lifecycle-exercise --format "table {{.Names}}\t{{.State}}\t{{.Ports}}"
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8090
docker ps --filter name=lifecycle-exercise -qThe three answers: state created, the port does not respond (code 000, connection refused) and it does not appear in docker ps (the third command's output is empty), because docker ps without -a only lists running containers. The configuration is stored, but there is no process and no network rule.
docker start lifecycle-exercise
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8090
docker exec lifecycle-exercise printenv ENVIRONMENTNow it works: it appears in docker ps, the port returns 200 and the variable you defined in the create phase is present.
The attempt to add a port:
There is no way to do it. docker update only modifies resources (memory, CPU, restart policy — lesson 03-07), never ports, variables or mounts: those are frozen when the container is created because they involve network namespaces and forwarding rules that are built at startup. The only way out is to recreate it:
docker rm -f lifecycle-exercise
docker run -d --name lifecycle-exercise -p 8090:80 -p 8091:80 -e ENVIRONMENT=tests nginx:alpine
docker rm -f lifecycle-exerciseThis rigidity is precisely the argument in favor of declaring configuration in a versioned file instead of on the command line, which is what Docker Compose does in module 4.
Solution to exercise 2
# 1. Node version: the CMD "server.js" is replaced by "--version",
# and the ENTRYPOINT ["node"] stays in place → node --version
docker run --rm auroralibros/aurora-api:1.1.0 --version
# 2. Contents of package.json: we need "cat", which is not node.
# We cancel the entrypoint entirely.
docker run --rm --entrypoint cat auroralibros/aurora-api:1.1.0 /app/package.json
# 3. Interactive shell: entrypoint sh plus the two interactivity letters
docker run --rm -it --entrypoint sh auroralibros/aurora-api:1.1.0
# 4. Evaluating code with an environment variable: Docker's -e (before the
# image) and node's -e (after). The same dash, two meanings.
docker run --rm -e DB_HOST=aurora-db auroralibros/aurora-api:1.1.0 \
-e "console.log(process.env.DB_HOST)"
# 5. Running ls directly, with no node and no sh in between
docker run --rm --entrypoint "" auroralibros/aurora-api:1.1.0 ls -1 /appv22.13.0
{
"name": "aurora-api",
...
}
/app $
aurora-db
Dockerfile
node_modules
package-lock.json
package.json
server.jsCase 4 is the most instructive of the exercise: there are two -e flags in the same command and they mean different things. The first is before the image, so it is Docker's --env option; the second is after, so it is an argument Docker hands over untouched to ENTRYPOINT ["node"], and there -e is Node's option for evaluating code. The rule from section 2 explains it without ambiguity.
Case 5 shows the difference between --entrypoint ls and --entrypoint "". With --entrypoint ls you would still have to write the arguments after the image, but with --entrypoint "" the line reads naturally: the image stops imposing an executable and everything that follows is the complete command.
Solution to exercise 3
cat > ~/aurora-libros/aurora-dev.env <<'EOF'
PORT=3000
DB_HOST=aurora-db
DB_USER=aurora
DB_PASSWORD=aurora_secret
DB_NAME=aurora_books
REDIS_HOST=aurora-cache
EOF
cat > ~/aurora-libros/aurora-test.env <<'EOF'
PORT=3100
DB_HOST=aurora-db
DB_USER=aurora
DB_PASSWORD=aurora_secret
DB_NAME=aurora_books_test
REDIS_HOST=aurora-cache-test
EOFVerification with printenv, without starting the API (which would fail for reasons you already know):
docker run --rm --env-file ~/aurora-libros/aurora-dev.env \
--entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME REDIS_HOST
docker run --rm --env-file ~/aurora-libros/aurora-test.env \
--entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAME REDIS_HOSTThe third one, with a one-off override:
docker run --rm --env-file ~/aurora-libros/aurora-dev.env -e PORT=3200 \
--entrypoint printenv auroralibros/aurora-api:1.1.0 PORT DB_NAMEPORT is 3200 (the -e won) and DB_NAME keeps the file's value. -e always takes precedence over --env-file, regardless of the order you write them in on the command line: try swapping them around and you will get the same result.
The two final answers:
- A single image.
auroralibros/aurora-api:1.1.0served three different configurations without being rebuilt even once. This is the build once, deploy everywhere principle you saw in lesson 02-06: what changes between environments is the injected configuration, never the image. - The
.gitignoreline is*.env(oraurora-*.env). These files containDB_PASSWORD=aurora_secret; they do not go into the repository, and not into the.dockerignoreeither, so that they never end up in the build context nor, by accident, inside an image layer.
Conclusion
docker run has stopped being a magic formula. You know it is two operations, create and start, and you have proved it by running them separately: the create phase reserves the writable layer and freezes the configuration —which is why you cannot add a port or a variable to an existing container— and the start phase sets up namespaces, cgroups and network rules and launches PID 1. You know the golden rule about argument order, which separates Docker's options from the command inside, and you have the five families of options sorted out: identity, execution, network, configuration and data.
You handle -d for services, -it for people and --rm for throwaway commands, knowing that --rm becomes your enemy the moment something fails. You know how to override an image's CMD half and ENTRYPOINT half separately, including the complete cancellation with --entrypoint "". You know how to detach from a session with Ctrl+P Ctrl+Q without killing the process, and why docker attach is a sharp tool that can take a service down with one distracted Ctrl+C, as opposed to docker exec, which launches an independent process. And you know how to keep configuration outside the image with --env-file, with its strict format rules and its password that never enters Git or the build context.
Above all, the platform has started to move: aurora-db and aurora-cache are running right now on your machine, with PostgreSQL 16 accepting connections and Redis answering PONG. And aurora-api no longer gives ECONNREFUSED: it gives getaddrinfo ENOTFOUND aurora-db, a different error that says something very specific —the name does not even resolve— and that points straight at the solution. On top of that, the container did not end up unhealthy: it died in two seconds with exit code 1.
And that raises the question the course continues with: why do some containers keep running indefinitely while others die instantly? In the next lesson, Container Lifecycle, you are going to see the seven states a container passes through and the transitions between them, the rule that a container lives exactly as long as its PID 1 process lives, the difference between an orderly stop with SIGTERM and a SIGKILL ten seconds later, and the table of exit codes —that 1, and also 125, 137 and 143— that turns a cryptic number into a diagnosis. And you will teach server.js to shut down cleanly, closing the PostgreSQL pool and the Redis client before it goes.
Docker: From Beginner to Advanced
Module 1: Introduction to Docker
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
