Right now you have three or four containers on your machine and you can still keep them all in your head. A month from now you will have thirty: the four Aurora Libros services, half a dozen forgotten experiments, containers from other projects and a pile of weeks-old Exited ones taking up disk and names. Without management tools, that docker ps -a turns into a wall of text you cannot read, and the mass docker rm you run at eleven at night takes down something you needed.
This is the day-to-day operations lesson. You are going to squeeze docker ps for everything it has —column by column, with its filters and its templates—, you are going to build a control panel for the four Aurora Libros services, you are going to learn to compose commands with -q to operate on dozens of containers without wrecking anything, to copy files between the host and a container with docker cp —which is how you will finally fill the books table with the eight titles—, to see with docker diff what a container has changed with respect to its image, and to understand why docker commit is a temptation you have to resist.
Contents
docker ps: the seven columns, one by one- Listing options:
-a,-q,-s,-n,-l - Filtering with
--filter - Formatting with
--formatand Go templates - Labels: organizing the fleet with
--label - Composing commands with
-q(and doing it sensibly) - Deleting containers:
rm,-f,-vandcontainer prune - Renaming and changing things live:
renameandupdate docker cp: moving files in both directionsdocker diff: what has changed with respect to the imagedocker commitand why you should not use it- The Aurora Libros control panel
docker ps: the seven columns, one by one
docker ps: the seven columns, one by oneCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f8a1c9e7b2d postgres:16-alpine "docker-entrypoint.s…" 35 minutes ago Up 35 minutes 127.0.0.1:5432->5432/tcp aurora-db
9d4b7e2f1a6c redis:7-alpine "docker-entrypoint.s…" 32 minutes ago Up 4 minutes (healthy) 127.0.0.1:6379->6379/tcp aurora-cache| Column | What it is exactly | Details worth knowing |
|---|---|---|
| CONTAINER ID | The first 12 characters of the 64-character ID | Typing the first few that are unique is enough: docker stop 3f8 works |
| IMAGE | The image reference exactly as it was written | If you started from a digest, you will see the digest; if the tag moved afterwards, this text no longer tells the truth |
| COMMAND | The effective command: ENTRYPOINT + CMD |
It appears truncated with …. You see it whole with --no-trunc |
| CREATED | When the container was created | It is not when it was started: a container created a month ago and started a minute ago says "5 weeks ago" |
| STATUS | State and time in it | Up 35 minutes, Exited (0) 2 hours ago, Up 4 minutes (healthy), Created, Paused |
| PORTS | Active publications | 127.0.0.1:5432->5432/tcp is host→container. If it only says 5432/tcp, it is exposed but not published |
| NAMES | The assigned or invented name | Unique on the machine |
Two observations about the output above, which double as a review:
aurora-cachesaysUp 4 minutesand notUp 32 minutesbecause you restarted it in the previous lesson: theSTATUScounter resets to zero on every start, whileCREATEDdoes not move.- The
(healthy)onaurora-cachedoes not appear by magic: the official Redis 7 image ships its ownHEALTHCHECK.aurora-dbshows nothing because the PostgreSQL image does not define one. Health is studied in depth in lesson 03-04.
To see the complete command:
- Listing options:
-a, -q, -s, -n, -l
-a, -q, -s, -n, -l| Option | What it does | Typical use |
|---|---|---|
-a, --all |
Shows all of them, Exited and Created included |
Finding the container that died |
-q, --quiet |
Only the IDs, one per line | Composing with other commands |
-s, --size |
Adds the SIZE column |
Knowing how much it really takes up |
-n N |
The last N created, running or not | docker ps -n 3 |
-l, --latest |
The last one created | docker logs $(docker ps -lq) |
--no-trunc |
Does not truncate IDs or commands | Copying a full ID |
-s: the real size of a container
NAMES IMAGE SIZE
aurora-cache redis:7-alpine 0B (virtual 41.2MB)
aurora-db postgres:16-alpine 127kB (virtual 278MB)This column has two numbers, and the difference between them is lesson 01-05 turned into a command:
- The first (
0B,127kB) is the writable layer: what that container has written on top of the image. It is the only thing that belongs exclusively to it and the only thing lost when you delete it. - The
virtualfigure is the total size the container sees: its writable layer plus all the image's layers, which are shared and read-only.
Ten redis:7-alpine containers do not take up 412 MB, but 41.2 MB plus ten tiny writable layers. And aurora-db with its 127 kB of writes stands out: you have just created an entire database, so where is the data? In an anonymous volume that the official image declares, and which does not count as a writable layer. Hold on to that question: it is the heart of lesson 03-06.
- Filtering with
--filter
--filter--filter (or -f) accepts key=value pairs and can be repeated. The available filters:
| Filter | Example | What it selects |
|---|---|---|
status |
--filter status=exited |
By state: created, running, paused, restarting, exited, dead |
name |
--filter name=aurora |
A name that contains that string (it is a substring, not an equality) |
ancestor |
--filter ancestor=redis:7-alpine |
Containers created from that image |
label |
--filter label=project=aurora-libros |
By label, with or without a value |
exited |
--filter exited=1 |
Those that exited with that code |
health |
--filter health=unhealthy |
By health state: starting, healthy, unhealthy, none |
before / since |
--filter since=aurora-db |
Created before/after that container |
id |
--filter id=3f8a1c9e7b2d |
By ID |
volume |
--filter volume=aurora-data |
Those that mount that volume (lesson 03-06) |
network |
--filter network=aurora-net |
Those connected to that network (lesson 03-05) |
publish / expose |
--filter publish=5432 |
Those that publish or expose that port |
Recipes ready to copy:
# Everything belonging to Aurora Libros, alive or dead
docker ps -a --filter name=aurora
# Only what is running from a specific image
docker ps --filter ancestor=postgres:16-alpine
# Containers that failed: they exited with a code other than 0
docker ps -a --filter status=exited --filter exited=1
# Containers that are sick right now
docker ps --filter health=unhealthy
# Everything created after aurora-db (useful for scoping a work session)
docker ps -a --filter since=aurora-dbA real example:
Two warnings about how filters behave:
- Several different
--filterflags combine with logical AND.--filter status=running --filter name=aurorademands both conditions. - Several values of the same filter combine with logical OR.
--filter status=exited --filter status=createdreturns those in either state. nameis a substring, not an equality.--filter name=aurora-dbalso findsaurora-db-copy. If you need an exact match, use an anchored expression:--filter name=^aurora-db$.
- Formatting with
--format and Go templates
--format and Go templates--format uses Go language templates, the same ones you already used with docker image ls in lesson 02-05.
| Placeholder | Content |
|---|---|
{{.ID}} |
Short ID |
{{.Names}} |
Name |
{{.Image}} |
Image |
{{.Command}} |
Effective command |
{{.CreatedAt}} |
Full creation date |
{{.RunningFor}} |
Elapsed time as text |
{{.Status}} |
State with its time |
{{.State}} |
Just the state (running, exited…) |
{{.Ports}} |
Published ports |
{{.Size}} |
Size (requires -s) |
{{.Labels}} |
All the labels |
{{.Label "key"}} |
The value of one label |
{{.Mounts}} |
Mounted volumes |
{{.Networks}} |
Connected networks |
The word table at the beginning turns on the header and column alignment:
Without table, the output is free text, perfect for scripting:
And there are two prebuilt formats that come in very handy:
docker ps --format json | head -1
docker ps --format "{{json .}}" | jq -r '.Names + " | " + .Status'{"Command":"\"docker-entrypoint.s…\"","CreatedAt":"2026-08-04 19:28:41 +0200 CEST","ID":"3f8a1c9e7b2d",...}
aurora-db | Up 41 minutes
aurora-cache | Up 10 minutesIf you like a format, make it permanent in ~/.docker/config.json:
From that moment on, a plain docker ps uses your format. It is one of the tweaks that day-to-day use appreciates most.
- Labels: organizing the fleet with
--label
--labelWhen containers from three projects share the same machine, filtering by name stops being reliable. Labels are arbitrary metadata you assign when creating the container:
docker run -d --name label-test \
--label project=aurora-libros \
--label component=demo \
--label environment=local \
alpine:3.20 sleep 300And then you filter by them with surgical precision:
docker ps --filter label=project=aurora-libros --format "{{.Names}}"
docker ps --filter label=component --format "{{.Names}}"
docker rm -f label-test--filter label=key=value demands the exact value; --filter label=key only demands that the label exists.
Let's properly label the Aurora Libros fleet. Since labels, just like ports and variables, are frozen when the container is created and cannot be added afterwards (lesson 03-01), we have to recreate them. That is fine: aurora-db has no data to lose yet.
docker rm -f aurora-db aurora-cache
docker run -d --name aurora-db \
--label project=aurora-libros \
--label component=database \
--label environment=local \
-e POSTGRES_USER=aurora \
-e POSTGRES_PASSWORD=aurora_secret \
-e POSTGRES_DB=aurora_books \
-p 127.0.0.1:5432:5432 \
postgres:16-alpine
docker run -d --name aurora-cache \
--label project=aurora-libros \
--label component=cache \
--label environment=local \
-p 127.0.0.1:6379:6379 \
redis:7-alpinedocker ps --filter label=project=aurora-libros \
--format 'table {{.Names}}\t{{.Label "component"}}\t{{.Status}}'A labeling convention that works well and that we will use throughout the course:
| Label | Values in Aurora Libros | What it is for |
|---|---|---|
project |
aurora-libros |
Separating it from other projects on the machine |
component |
database, cache, api, web |
Identifying each container's role |
environment |
local, tests, production |
Distinguishing instances of the same service |
With these three labels, operations like "stop everything from Aurora Libros in local without touching anything else" become trivial, and it is exactly what Docker Compose will do for you automatically in module 4: its com.docker.compose.project labels are this very mechanism.
- Composing commands with
-q (and doing it sensibly)
-q (and doing it sensibly)docker ps -q prints only IDs, and that turns it into the input for any other command:
Recipes used daily:
# Stop every container from a specific image
docker stop $(docker ps -q --filter ancestor=postgres:16-alpine)
# Delete every stopped container
docker rm $(docker ps -aq --filter status=exited)
# Stop the entire Aurora Libros fleet
docker stop $(docker ps -q --filter label=project=aurora-libros)
# Restart only the sick ones
docker restart $(docker ps -q --filter health=unhealthy)And now the three precautions, because these commands are sharp:
First: if the filter finds nothing, the command fails.
Annoying but harmless. You avoid it by checking first:
IDS=$(docker ps -q --filter label=project=aurora-libros)
[ -n "$IDS" ] && docker stop $IDS || echo "Nothing to stop"Second: docker rm -f $(docker ps -aq) is the most dangerous command in this lesson. It deletes absolutely every container on the machine, including those from other projects, and without asking. If you need to clean up, do it scoped:
Third: look before you shoot. The golden rule is to run the listing-only version first:
# 1. What exactly am I about to touch?
docker ps -a --filter status=exited --format "table {{.Names}}\t{{.Status}}"
# 2. If the list is what I expected, then go ahead
docker rm $(docker ps -aq --filter status=exited)That extra half-second has saved a lot of development databases.
- Deleting containers:
rm, -f, -v and container prune
rm, -f, -v and container prunedocker rm only works on stopped containers. With a running one:
Error response from daemon: cannot remove container "aurora-cache":
container is running: stop the container before removing or force remove| Option | Effect |
|---|---|
| (none) | Only deletes stopped containers |
-f, --force |
Sends SIGKILL and deletes. No grace period |
-v, --volumes |
Also deletes the associated anonymous volumes (never the named ones) |
-l, --link |
Removes a legacy link, not the container. Practically obsolete |
About -f, an important nuance that connects with the previous lesson: docker rm -f is not docker stop + docker rm. It sends SIGKILL directly, with none of the ten seconds of grace. On a database, that is unplugging the server. The correct sequence when data is involved is:
What exactly happens when you delete a container:
| Destroyed | Survives |
|---|---|
| The writable layer and everything that was in it | The image it came from |
| The container's logs | The named volumes (lesson 03-06) |
| Its configuration and its name | The host files mounted as bind mounts |
The anonymous volumes, only if you use -v |
The anonymous volumes, if you do not use -v (they are orphaned) |
docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
7d3f9a1b2c48e6015a2c9f7b3e1d8a4c6f0b2d9e7a5c3f1b8d6a4e2c9f7b5d3a
Total reclaimed space: 41.9kBIt deletes all stopped containers. It is worth repeating the warning from lesson 02-05: a stopped container may be something you were debugging or a development database you switched off yesterday. Scope it whenever you can:
# Only those stopped more than 24 hours ago
docker container prune --filter "until=24h"
# Only the stopped ones from a specific project
docker container prune --filter "label=project=aurora-libros"
# Without interactive confirmation (for scripts; use it with double care)
docker container prune -f --filter "until=168h"
- Renaming and changing things live:
rename and update
rename and updateYou already saw docker rename in the previous lesson. Its companion is docker update, which can change things on a running container, though a very specific list of them:
docker update --restart=unless-stopped aurora-db
docker update --memory 512m --memory-swap 512m aurora-dbCan be changed with update |
Can never be changed |
|---|---|
Memory (--memory, --memory-swap, --memory-reservation) |
Published ports |
CPU (--cpus, --cpu-shares, --cpuset-cpus) |
Environment variables |
Number of processes (--pids-limit) |
Mounts and volumes |
Restart policy (--restart) |
Image, CMD or ENTRYPOINT |
Disk I/O weight (--blkio-weight) |
Labels and networks |
The detail of all those resource options and of the restart policies is the content of lesson 03-07; what matters here is remembering the boundary: update touches what lives in the cgroups; everything that lives in the namespaces (network, mounts, environment) is frozen when the container is created.
docker cp: moving files in both directions
docker cp: moving files in both directionsOne of the two sides carries the container: prefix and the other is a host path. It works in both directions and also with stopped containers.
From the host to the container: loading the catalog
The time has come to fill the database. You have had ~/aurora-libros/db/init.sql since lesson 01-07, with the books table and the eight titles:
And now run it inside the container:
Check the result:
docker exec aurora-db psql -U aurora -d aurora_books \
-c "SELECT id, title, author, price FROM books ORDER BY id LIMIT 4;"
docker exec aurora-db psql -U aurora -d aurora_books -t \
-c "SELECT COUNT(*) FROM books;" id | title | author | price
----+---------------------------------------+------------------------+--------
1 | El jardín de senderos que se bifurcan | Jorge Luis Borges | 14.50
2 | Rayuela | Julio Cortázar | 19.90
3 | Cien años de soledad | Gabriel García Márquez | 17.95
4 | La sombra del viento | Carlos Ruiz Zafón | 21.00
(4 rows)
8The eight Aurora Libros books are in a real database, in a real container. It is the first time in the whole course that they exist outside a .sql file. Nobody can read them from the API yet —that comes in lesson 03-05— but they are there.
A note on method: loading the schema by hand with docker cp works but is not the right way, because you have to repeat it every time you recreate the container and nobody ever remembers to. In lesson 03-06 you will solve it properly, mounting init.sql into /docker-entrypoint-initdb.d/ so that PostgreSQL runs it by itself when it initializes.
From the container to the host: extracting a file
The reverse direction is just as easy and turns out to be essential when investigating an incident:
docker cp aurora-db:/var/lib/postgresql/data/pg_hba.conf ~/aurora-libros/pg_hba.conf.copy
head -3 ~/aurora-libros/pg_hba.conf.copySuccessfully copied 5.12kB to /home/junior/aurora-libros/pg_hba.conf.copy
# PostgreSQL Client Authentication Configuration File
# ===================================================And a very common case: rescuing the log of an application that —against all good practice— writes to a file instead of to standard output.
Details of how docker cp behaves that save you surprises:
| Situation | Result |
|---|---|
docker cp file container:/path/ (ends with /) |
Copies into the directory |
docker cp file container:/path (no /) |
If /path is a directory, it copies inside; if not, it creates or overwrites that file |
docker cp folder/. container:/destination |
Copies the folder's contents |
docker cp folder container:/destination |
Copies the whole folder inside the destination |
With -a/--archive |
Preserves owner and group (UID/GID) |
Without -a |
The files end up owned by root inside the container |
That last point bites often: if you copy a file into a container running with USER node, the file will belong to root and the application may not be able to read it. And one clear limitation: docker cp cannot copy from one container to another directly; you have to go through the host.
docker diff: what has changed with respect to the image
docker diff: what has changed with respect to the imagedocker diff compares the container's current file system with that of the image it came from:
C /tmp
A /tmp/init.sql
C /run
A /run/postgresql
A /run/postgresql/.s.PGSQL.5432
C /var/lib/postgresqlThree letters and their meaning:
| Letter | Means | Example |
|---|---|---|
| A | Added: a new file or directory | /tmp/init.sql, the one you just copied |
| C | Changed: modified (or a directory whose contents changed) | /tmp, because it now contains something new |
| D | Deleted: removed with respect to the image | A configuration file that was replaced |
It is the visible manifestation of the copy-on-write from lesson 01-05: what you see here is exactly the contents of the writable layer, which is why docker diff and the SIZE column of docker ps -s are talking about the same thing.
And now the important observation:
Not one line. You have just created a table and eight records, and PostgreSQL's data directory does not appear in the diff. The reason is that /var/lib/postgresql/data is not in the writable layer: the official image declares a VOLUME there, so Docker created an anonymous volume and mounted it at that path. And docker diff never looks inside mounts.
This is exactly what lesson 02-04 was anticipating when it talked about why VOLUME in a Dockerfile has side effects, and it is the doorway into lesson 03-06: there is data that lives neither in the image nor in the writable layer, but in a third place. A place that right now, for aurora-db, is anonymous and fragile.
A very practical use case for docker diff: finding out what an application you do not trust writes.
docker run -d --name diff-demo nginx:alpine
sleep 2 && curl -s localhost > /dev/null 2>&1
docker diff diff-demo
docker rm -f diff-demoC /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /run
A /run/nginx.pid
C /var/cache/nginx
A /var/cache/nginx/client_tempIn ten lines you know that Nginx modifies its configuration at startup (its docker-entrypoint.sh does it), creates a PID file and several cache directories. That is invaluable information when you are about to make the file system read-only for security, something you will see in lesson 05-03.
docker commit and why you should not use it
docker commit and why you should not use itdocker commit turns a container's current state into a new image:
docker commit --message "Catalog loaded by hand" aurora-db aurora-db:with-data
docker image ls aurora-db --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"sha256:8f2e9c1a7b34d6058e2f9a1c7b3d5e8a0c2f4b6d8e0a2c4f6b8d0e2a4c6f8b0d2
REPOSITORY:TAG SIZE
aurora-db:with-data 278MBIt works. And that is precisely why it is dangerous. These are the reasons images are not built this way, picking up the whole of module 2:
| Problem | Consequence |
|---|---|
| It is not reproducible | There is no file describing how that state was reached. If tomorrow you need the same image with Node 22.14, there is no way to regenerate it |
| It is not auditable | docker image history will show one enormous layer with whatever comment you wrote, without saying what was installed, what was removed or where it came from |
| It is not versioned | A Dockerfile lives in Git, gets reviewed in a pull request and has a history. A commit lives in the head of whoever made it |
| It drags in junk | Temporary files, shell history, logs, credentials typed during the debugging session... everything gets baked into the image |
| It is heavier than it should be | It is a monolithic layer that breaks the caching and layer sharing you took such care with in lesson 02-02 |
| It breaks the chain of trust | Nobody can answer "what is inside this?" without opening it up and looking |
In the trade's vocabulary, an image like that is called a hand-crafted image or a snowflake, and it is the containerized equivalent of that server nobody dared reboot. It does have one legitimate use:
# Forensics: freeze a broken container BEFORE deleting it, to investigate at leisure
docker commit aurora-api aurora-api:forensic-2026-08-04
docker rm aurora-api
docker run --rm -it --entrypoint sh aurora-api:forensic-2026-08-04Preserving the crime scene before cleaning it up. For that, yes, and for nothing else.
- The Aurora Libros control panel
We close by putting everything from the lesson into a command you will use daily:
docker ps -a --filter label=project=aurora-libros \
--format 'table {{.Label "component"}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'COMPONENT NAMES STATUS PORTS
cache aurora-cache Up 22 minutes 127.0.0.1:6379->6379/tcp
database aurora-db Up 22 minutes 127.0.0.1:5432->5432/tcpTurn it into a permanent alias in your ~/.bashrc or ~/.zshrc:
alias aurora='docker ps -a --filter label=project=aurora-libros --format "table {{.Label \"component\"}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias aurora-stop='docker stop $(docker ps -q --filter label=project=aurora-libros)'
alias aurora-start='docker start $(docker ps -aq --filter label=project=aurora-libros)'And a version with health and size, for when something goes wrong:
docker ps -as --filter label=project=aurora-libros \
--format 'table {{.Names}}\t{{.Status}}\t{{.Size}}'NAMES STATUS SIZE
aurora-cache Up 23 minutes 0B (virtual 41.2MB)
aurora-db Up 23 minutes 3.14kB (virtual 278MB)Notice that aurora-db's writable layer has grown from 127 kB to 3.14 kB… hold on, it has gone down. What is happening is that the container is new: you recreated it when you added the labels, and those 3.14 kB are almost entirely the init.sql you copied with docker cp. The data of the eight books is not there, but in the anonymous volume we were talking about. One more reminder that there is a piece of the puzzle you have not placed yet.
Common Mistakes and Tips
- Running
docker rm -f $(docker ps -aq)"to clean up". It takes everything on the machine with it, including containers from other projects and development databases with weeks of data. Always filter bylabelor byname. - Trusting
--filter name=as if it were an equality. It is a substring:name=aurora-dbalso findsaurora-db-backup. Anchor it with^...$when it matters. - Reading the
CREATEDcolumn as "started ago". It is the creation date. For uptime, look atSTATUSor{{.RunningFor}}. - Reading the
virtualsize as space used. Ten containers from the same image share its layers: the real disk usage is the sum of the writable layers plus one copy of the image. - Using
docker rm -fon a database. It is a SIGKILL with no grace. Firstdocker stop --time 30, thendocker rm. - Forgetting
-vwhen deleting and accumulating orphaned anonymous volumes. Every PostgreSQL container deleted without-vleaves its volume behind. You find them withdocker volume ls -f dangling=true(lesson 03-06). - Copying files with
docker cpinto an unprivileged container and not understanding thepermission denied. The file arrives as root. Use-a, or fix it afterwards withdocker exec -u root ... chown. - Building images with
docker commit. It works today and leaves you with no explanation tomorrow. The Dockerfile is the source of truth. - Tip: save your favorite formats in
~/.docker/config.jsonwithpsFormat. A readabledocker psby default is a daily gift. - Tip: before any mass operation, run the listing-only version. Look at the list, and only then swap
psforrm.
Exercises
Exercise 1: build your own control panel
Create five test containers with alpine:3.20 and sleep 300, labeled like this: two with project=aurora-libros and environment=local, two with project=aurora-libros and environment=tests, and one with project=other-client. Then write a single docker ps command that shows only the Aurora Libros ones in the tests environment, in a table with the columns name, environment and uptime. Finally, stop and delete in a single command exclusively the other-client ones, leaving the rest untouched, and prove with another command that the four Aurora Libros ones are still alive.
Exercise 2: investigate what a container writes (and where it does not write it)
Start a redis:7-alpine container called diff-redis. Before touching anything, run docker diff and note the result. Then write 100 keys with redis-cli, force a save to disk with redis-cli SAVE, and run docker diff again. Answer:
- Does the Redis data file appear in the
docker diff? And in theSIZEcolumn ofdocker ps -s? - Find out with
docker inspect --format '{{json .Mounts}}'where that file really is. - Copy it to the host with
docker cpand check its size withls -lh. - Delete the container with
docker rm -f(without-v) and explain exactly what has happened to those 100 keys. Are they lost? Where are they?
Exercise 3: rescue and safe cleanup
Simulate the end of a chaotic working day:
- Create six containers that end immediately: three with code 0 (
alpine:3.20 true) and three with code 1 (alpine:3.20 sh -c 'exit 1'), all with the labelproject=cleanup-tests. - Write a command that lists only the ones that failed, with their name and their state.
- From one of the failed ones, extract the file
/etc/os-releaseto the host withdocker cpbefore deleting it. - Delete in a single command only the ones that failed, leaving the three successful ones.
- Finally, clean up the rest with
docker container prunescoped to the label, and verify that neitheraurora-dbnoraurora-cachehas been affected.
Solutions
Solution to exercise 1
for n in 1 2; do
docker run -d --name demo-local-$n \
--label project=aurora-libros --label environment=local \
alpine:3.20 sleep 300
done
for n in 1 2; do
docker run -d --name demo-tests-$n \
--label project=aurora-libros --label environment=tests \
alpine:3.20 sleep 300
done
docker run -d --name demo-other --label project=other-client alpine:3.20 sleep 300The panel command, with two filters combined with logical AND:
docker ps --filter label=project=aurora-libros --filter label=environment=tests \
--format 'table {{.Names}}\t{{.Label "environment"}}\t{{.RunningFor}}'The environment=local ones do not appear: they meet the first filter but not the second, and both must be met.
Surgical deletion of other-client:
# First LOOK
docker ps -a --filter label=project=other-client --format "{{.Names}}"
# Then ACT
docker rm -f $(docker ps -aq --filter label=project=other-client)
# And CHECK
docker ps --filter label=project=aurora-libros --format "{{.Names}}" | wc -lThe four Aurora Libros ones are still standing. The sequence look → act → check is what turns a dangerous command into a routine operation.
Solution to exercise 2
Practically nothing: a freshly started Redis only writes its PID file. Now, load and save:
docker exec diff-redis sh -c 'for i in $(seq 1 100); do redis-cli SET book:$i "title-$i" > /dev/null; done'
docker exec diff-redis redis-cli DBSIZE
docker exec diff-redis redis-cli SAVE
docker exec diff-redis ls -lh /data
docker diff diff-redis
docker ps -s --filter name=diff-redis --format "{{.Names}}: {{.Size}}"(integer) 100
OK
-rw-r--r-- 1 redis redis 3.4K Aug 4 20:41 dump.rdb
C /run
A /run/redis_6379.pid
diff-redis: 0B (virtual 41.2MB)1. Here is the exercise's surprise: dump.rdb exists (you have just listed it with ls -lh, 3.4 kB) but it does not appear in the docker diff, and the writable layer still measures 0 B. The two tools agree with each other and both are telling the truth: that file is not in the container's writable layer.
2. The explanation is in the mounts:
[
{
"Type": "volume",
"Name": "b41f7c9e2a8d05f31c6e8b4a2d97f0c5e3a1b8d6f4c2e0a9b7d5f3c1e8a6b4d2",
"Source": "/var/lib/docker/volumes/b41f7c9e2a8d.../_data",
"Destination": "/data",
"Driver": "local",
"RW": true
}
]The official Redis image declares VOLUME /data, so Docker created an anonymous volume —that name that looks like a hash is exactly that: a volume with no name— and mounted it at /data. Everything Redis writes there goes to the volume, not to the writable layer, and that is why docker diff does not see it.
3.
Successfully copied 5.63kB to /tmp/dump-aurora.rdb
-rw-r--r-- 1 junior junior 3.4K Aug 4 20:42 /tmp/dump-aurora.rdbdocker cp does read through mounts: as far as it is concerned, /data/dump.rdb is simply a path in the container's file system. The 3.4 kB match the ls from inside; the 5.63 kB Docker reports is the size of the tar stream used to make the copy, with its headers and its block padding.
4.
The correct answer is more interesting than a plain "they are lost":
- The keys that were in memory died with the process, as in the previous lesson's exercise.
- But the
dump.rdbwith the 100 keys still exists, in a volume that is now orphaned: with no container using it, with a name nobody remembers and taking up disk indefinitely. Since you useddocker rm -fwithout-v, the volume was not deleted. - If tomorrow you start another
redis:7-alpine, Docker will create a new, empty anonymous volume: it will not reuse the previous one. From a practical point of view, the data is lost even though the bytes are still on your disk.
That is exactly what is happening today to aurora-db with the eight books you just loaded, and it is the problem lesson 03-06 solves by giving the volume a name.
Solution to exercise 3
for n in 1 2 3; do
docker run --name ok-$n --label project=cleanup-tests alpine:3.20 true
docker run --name fail-$n --label project=cleanup-tests alpine:3.20 sh -c 'exit 1'
done2. Listing only the ones that failed, combining status and exited:
docker ps -a --filter label=project=cleanup-tests --filter exited=1 \
--format "table {{.Names}}\t{{.Status}}"NAMES STATUS
fail-3 Exited (1) 6 seconds ago
fail-2 Exited (1) 7 seconds ago
fail-1 Exited (1) 8 seconds agoThe exited=1 filter is what does the fine work: it distinguishes "it ended" from "it ended badly", something status=exited alone cannot do.
3. Rescuing a file from a stopped container:
An important detail: docker cp works with the container stopped. There is no need to start it, and that makes it the ideal tool for performing autopsies on containers that no longer come up.
4. Selective deletion of the failed ones:
docker rm $(docker ps -aq --filter label=project=cleanup-tests --filter exited=1)
docker ps -a --filter label=project=cleanup-tests --format "{{.Names}} {{.Status}}"2f8c1a9e4b73
7d1e5b2c8f04
9a3f7c1e5d28
ok-3 Exited (0) 1 minute ago
ok-2 Exited (0) 1 minute ago
ok-1 Exited (0) 1 minute agoThe three successful ones are still there. No -f was needed because they were already stopped.
5. Final scoped cleanup and verification:
docker container prune -f --filter "label=project=cleanup-tests"
docker ps -a --filter label=project=aurora-libros \
--format 'table {{.Names}}\t{{.Status}}'Deleted Containers:
1c7e9a3f5b28...
4b8d2f6a1c93...
6e0a4c8f2b17...
Total reclaimed space: 0B
NAMES STATUS
aurora-cache Up 41 minutes
aurora-db Up 41 minutesaurora-db and aurora-cache untouched, because the --filter label= scoped the prune to the right label. Compare mentally with what would have happened with a plain docker container prune -f: the three ok-* would have gone the same way, but so would any stopped container from any other project on your machine. Labels are not bureaucracy: they are the mechanism that makes cleanup selective instead of indiscriminate.
Conclusion
You no longer look at docker ps: you read it. You know what each of its seven columns says and, above all, what it does not say —that CREATED is not the start time, that IMAGE may have gone stale if the tag moved, and that the virtual number from -s is not space used but space seen—. You know how to filter by state, name, image, label, exit code and health, combining filters with AND and with OR, and you know how to shape the output with Go templates until it becomes an Aurora Libros control panel that fits in an alias.
You have learned to compose commands with -q and, more importantly, to do it with a safety net: look → act → check, always filtering by label so that a mass deletion never takes something that was not its business. You know the difference between docker rm and docker rm -f —which is not stop + rm, but a SIGKILL with no grace—, you know what is destroyed and what survives when a container is deleted, and you use docker container prune scoped with until and label instead of bare. You have labeled the fleet with project, component and environment, which is exactly the mechanism Docker Compose will automate for you in module 4.
With docker cp you move files in both directions, even with the container stopped, and that has got you something memorable: the eight Aurora Libros books are loaded into PostgreSQL, with their table, their index and their INSERT 0 8. With docker diff you know how to see the writable layer turned into a list of files —A, C and D— and you have discovered the missing piece: PostgreSQL's data directory does not appear in the diff, because it is not in the writable layer but in an anonymous volume that the image declares and nobody controls. And you know why docker commit is no good for building images, even though it is perfect for freezing a crime scene before cleaning it up.
You know how to watch the fleet when everything is fine. The other half is missing. In the next lesson, Inspecting and Debugging Containers, you will face the containers that do not work and do not say why: you will read docker logs in depth with its time filters and its golden rule about stdout, you will get in with docker exec even into minimal images that do not have so much as ping —using ephemeral containers that share their namespaces—, you will squeeze the JSON out of docker inspect with templates and jq, and you will watch live with docker top, docker stats and docker events. And you will apply all of it to three real Aurora Libros breakdowns, among them the one that has been waiting for two lessons: why aurora-api cannot find aurora-db even though both are running on the same machine.
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
