In lesson 03-06 you learned to use volumes, bind mounts and tmpfs. Here you will see what lies underneath: how the daemon stacks an image's layers with overlay2, how much writing to a container's layer really costs, what volume drivers are, and how to set up a backup strategy for Aurora Libros with tested restores.
Contents
- What a storage driver is and what it solves
overlay2from the inside: the four directories- Real inspection of an image and a container
- Comparison table of the storage drivers
- Checking the active driver and changing it
- The cost of copy-on-write, measured
- Why databases demand volumes
- The
localdriver and itsdriver_opts - Mounting NFS and
tmpfsas a volume - Third-party plugins and declaring them in Compose
externalvolumes shared between stacks- The Aurora Libros backup strategy
- A backup script with rotation and verified restore
- Disk space and
data-root - Performance and permissions (UID/GID)
- What a storage driver is and what it solves
An image is a stack of read-only layers (lesson 01-05). A container adds a writable layer on top. The technical problem is this: presenting all those layers as a single coherent filesystem, and doing it without copying gigabytes every time a container starts.
That is the job of the daemon's storage driver. It has three responsibilities:
- Stacking the image's layers into a single directory tree visible from inside.
- Applying copy-on-write: when a file from a lower layer is modified, copy it to the upper layer and work on the copy.
- Sharing common layers between containers, so that ten instances of
node:22-alpinetake up the space of one.
It is a piece of the daemon, not of the container: from inside all you see is an ordinary filesystem.
overlay2 from the inside: the four directories
overlay2 from the inside: the four directoriesoverlay2 uses OverlayFS, a union filesystem included in the Linux kernel. It works with four concepts:
| Directory | What it holds | Mode |
|---|---|---|
lowerdir |
The image's layers, stacked and separated by : |
Read-only |
upperdir |
The container's writable layer | Read and write |
merged |
The unified view: what the container sees as / |
Read and write |
workdir |
The kernel's internal working space for atomic operations | Internal use |
The resolution rules are simple, and they explain all the behavior you have already observed:
- Reading a file: it is looked up in
upperdir; if it is not there,lowerdiris walked from top to bottom. The first match wins. - Writing to a file that only exists in
lowerdir: it is copied whole toupperdir(copy-up) and written there. The original is untouched. - Deleting a file from
lowerdir: you cannot delete something that is read-only, so a special whiteout file is created inupperdirto hide it. The original still takes up space.
That third rule is, at last, the explanation for why deleting files in a later Dockerfile layer does not reduce the image's size. You will see it measured in lesson 05-04.
flowchart TB M["merged/ ← what the container sees"] U["upperdir/ writable layer<br/>new files, copy-ups and whiteouts"] L3["lowerdir 3 · COPY src/ (aurora-api)"] L2["lowerdir 2 · npm ci --omit=dev"] L1["lowerdir 1 · node:22-alpine"] U --> M L3 --> M L2 --> M L1 --> M
- Real inspection of an image and a container
All of this is visible on the host's disk. Each layer lives in /var/lib/docker/overlay2/<id>/:
docker image inspect auroralibros/aurora-api:1.2.0 --format '{{json .GraphDriver.Data}}' | tr ',' '\n'
mount | grep overlay | head -1 | tr ',' '\n' | grep -E 'lowerdir|upperdir|workdir' | cut -c1-70{"LowerDir":"/var/lib/docker/overlay2/4a8e77/diff:/var/lib/docker/overlay2/c93b2e/diff"
"MergedDir":"/var/lib/docker/overlay2/7d1f0a/merged"
"UpperDir":"/var/lib/docker/overlay2/7d1f0a/diff"
"WorkDir":"/var/lib/docker/overlay2/7d1f0a/work"}
lowerdir=/var/lib/docker/overlay2/l/QW3F:/var/lib/docker/overlay2/l/K7RT
upperdir=/var/lib/docker/overlay2/9be21c/diff
workdir=/var/lib/docker/overlay2/9be21c/workNotice the short links under overlay2/l/: they exist because the length of the kernel's mount arguments is limited, and an image with many layers would exceed the limit if full paths were used.
Now the demonstration that ties everything together. Write a file inside the container and watch it appear in the host's upperdir:
up=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo "layer test" > /tmp/aurora.txt'
sudo cat "$up/tmp/aurora.txt" && sudo find "$up" -type f | head -2The file is physically on the host, inside that container's diff and that container's alone. It is the same thing docker diff showed you in lesson 03-03, except now you know where it lives.
- Comparison table of the storage drivers
| Driver | Status in 2026 | Requirements | Performance | Notes |
|---|---|---|---|---|
overlay2 |
The default standard | Kernel ≥ 4.0, ext4 or xfs with ftype=1 |
Very good for reads; copy-up is expensive on large files | The right choice unless you have a strong reason |
btrfs |
Supported | Btrfs filesystem on /var/lib/docker |
Good; native and very cheap snapshots | Useful if you already run Btrfs; needs maintenance |
zfs |
Supported | ZFS installed | Good; compression and snapshots | High RAM usage (ARC); license outside the kernel |
devicemapper |
Removed | — | Poor in loop-lvm mode |
Historical; dropped in Docker 25. If you see it, migrate |
fuse-overlayfs |
Supported | Rootless mode on old kernels | Worse than overlay2: it goes through user space |
Kernels ≥ 5.13 no longer need it |
vfs |
Last resort | None | Very poor: copies every layer in full | No CoW; only for tests or nested environments |
Practical rule: if docker info says overlay2, do not touch anything; if it says vfs on a server, you have a configuration problem that will multiply your disk usage by ten.
- Checking the active driver and changing it
You change the driver in /etc/docker/daemon.json with {"storage-driver": "overlay2"} and restart the daemon.
Warning. Changing the storage driver makes the daemon stop seeing all existing images and containers: they are still on disk, under the old driver's directory, but they are inaccessible until you go back. Before touching it, push your images to a registry, save any unpublished ones with
docker save, and back up your volumes. Volumes, on the other hand, do not depend on the driver: they live in/var/lib/docker/volumes/and survive the change.
- The cost of copy-on-write, measured
Theory says copy-up copies the whole file the first time you touch it. Let's measure it with a 512 MB file baked into an image.
mkdir -p /tmp/cow && cd /tmp/cow
printf 'FROM alpine:3\nRUN dd if=/dev/zero of=/large.bin bs=1M count=512\n' > Dockerfile
docker build -q -t cow-test .
# One byte written to a file that lives in a lower layer
docker run --rm cow-test sh -c 'time dd if=/dev/zero of=/large.bin bs=1 count=1 conv=notrunc 2>/dev/null'
# The same byte, on a file that is already in the writable layer
docker run --rm cow-test sh -c 'cp /large.bin /c.bin; time dd if=/dev/zero of=/c.bin bs=1 count=1 conv=notrunc 2>/dev/null'
# And the space that byte cost
docker run -d --name cow cow-test sh -c 'dd if=/dev/zero of=/large.bin bs=1 count=1 conv=notrunc; sleep 30'
sleep 3 && docker ps -s --filter name=cow --format '{{.Names}}\t{{.Size}}' && docker rm -f cowreal 0m 1.94s <- first write: 512 MB copy-up
real 0m 0.00s <- the file was already in upperdir
cow 512MB (virtual 528MB)Almost two seconds to write one byte, and 512 MB of writable layer consumed.
That is the real cost of copy-on-write, and it explains three rules you had already been applying without knowing why:
| Rule | The real reason |
|---|---|
| Do not modify large files from the image | Every first write copies the whole file |
Use COPY --chown instead of a later RUN chown -R |
chown touches the metadata of every file and triggers a massive copy-up |
| Databases go in volumes | They write constantly to large files |
- Why databases demand volumes
PostgreSQL writes non-stop to data files of tens or hundreds of megabytes. If /var/lib/postgresql/data were in the writable layer, every page modification would trigger a copy-up of the entire file the first time, and performance would be catastrophic.
A volume bypasses OverlayFS completely: it is a bind mount to a host directory (/var/lib/docker/volumes/<name>/_data), mounted over the corresponding path. Writes go straight to the host's filesystem, with no layers and no copy-up.
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...)
/dev/sda2 on /var/lib/postgresql/data type ext4 (rw,relatime)There is the proof: while / is overlay, /var/lib/postgresql/data is the host's filesystem directly. That is why official database images declare that path as a VOLUME: if you forget to mount one, they at least avoid the penalty by creating an anonymous volume.
- The
local driver and its driver_opts
local driver and its driver_optsThe default volume driver is called local, and it accepts options almost nobody uses, even though they are surprisingly powerful: they are the same ones as Linux's mount command.
# A volume backed by a specific host directory (a named bind mount)
docker volume create --driver local \
--opt type=none --opt device=/srv/aurora/data --opt o=bind aurora-on-srv
# An in-memory volume, capped at 64 MB
docker volume create --driver local \
--opt type=tmpfs --opt device=tmpfs --opt o=size=64m,uid=1000 aurora-temp| Option | Meaning |
|---|---|
type |
Filesystem type: none (bind), tmpfs, nfs, cifs… |
device |
The source: a host path, tmpfs, or :/exported/path on NFS |
o |
Comma-separated mount options: bind, ro, size, uid, addr… |
The difference from a plain bind mount is one of management: a named volume shows up in docker volume ls, accepts labels, can be declared external, and does not depend on the project's relative path.
- Mounting NFS and
tmpfs as a volume
tmpfs as a volumeAn NFS volume lets several hosts share the same data, something impossible with a local volume:
# compose.yaml (fragment)
volumes:
aurora-covers:
driver: local
driver_opts:
type: nfs
o: "addr=10.0.0.20,rw,nfsvers=4,soft,timeo=30"
device: ":/exported/aurora/covers"Three warnings from the field. The mount happens when the container starts: if the NFS server does not answer, the container does not start. Use soft and a reasonable timeo so that a network failure produces an error rather than a process stuck forever in state D. And never put PostgreSQL's data on NFS: file locking over the network does not offer the guarantees a database needs, and corruption is only a matter of time. For static files —the catalog's cover images— it is perfect.
- Third-party plugins and declaring them in Compose
A volume driver is a daemon plugin that services mount requests. Docker ships local; everything else is installed:
docker plugin install --grant-all-permissions rclone/docker-volume-rclone:amd64
docker plugin ls # ID NAME ENABLED -> 9f2c1a rclone/docker-volume-rclone true| Plugin family | Examples | What for |
|---|---|---|
| Network storage | local with NFS/CIFS, netshare |
Sharing between hosts on the same network |
| Cloud object storage | rclone, s3fs |
S3, GCS or Azure Blob as if they were directories |
| Cloud block storage | EBS, Cinder plugins | Disks that follow the container between nodes |
| Snapshots | Plugins on top of ZFS or Btrfs | Instant copies and cloning |
In Compose they are declared like any other volume, with driver: rclone and its driver_opts (remote: "s3aurora:backups"). The important warning: a volume plugin is privileged software inside your daemon. Install only plugins from verified sources, and treat them with the same scrutiny you would apply to approving a production dependency.
external volumes shared between stacks
external volumes shared between stacksCompose prefixes the project name to the volumes it creates (aurora-libros_aurora-data). When a volume has to exist outside the project's lifecycle —or be shared with another stack— you declare it external:
Two very practical consequences. First: docker compose down -v does not delete an external volume, which makes it a cheap safety net for the data you cannot afford to lose. Second: another stack —a separate reporting project, for instance— can mount the same volume as :ro and read the data without touching the main platform.
- The Aurora Libros backup strategy
There are two ways to back up aurora-db's data, and they are not interchangeable:
| Aspect | Logical backup (pg_dump) |
File-level backup (tar of the volume) |
|---|---|---|
| Service stopped | No: taken hot | Yes, so that it is consistent |
| What it produces | SQL or PostgreSQL's own format | A tar archive with the data directory |
| Size | Small (compresses very well) | Large: includes indexes and free space |
| Restore | Requires a running server | Dumped back in place and started |
| Major version change | Yes: PostgreSQL 16 to 17 without trouble | No: binary format tied to the version |
| Restores a single table | Yes | No |
| Speed on large databases | Slow | Fast |
Logical backup, taken hot:
docker compose exec -T aurora-db \
pg_dump -U aurora -d aurora_books --format=custom --compress=9 \
> ~/backups/aurora-$(date +%Y%m%d-%H%M).dump # -> aurora-20260805-2310.dump, 18KThe -T is mandatory: without it, exec allocates a pseudo-terminal and corrupts the binary stream with line-ending translations. It is the classic mistake in this operation, and it gives no warning at all: the file looks fine and fails on restore.
File-level backup of the volume:
docker compose stop aurora-db # essential for consistency
docker run --rm \
-v aurora-libros_aurora-data:/data:ro \
-v "$HOME/backups":/output \
alpine:3 tar czf /output/volume-$(date +%Y%m%d).tar.gz -C /data .
docker compose start aurora-dbWhy the service has to be stopped: PostgreSQL keeps modified pages in memory and writes asynchronously. A tar over a live data directory captures some files at one instant and others at another, producing an incoherent set that can restore without errors and then corrupt itself days later. With the service stopped, the directory is still and the backup is valid.
- A backup script with rotation and verified restore
#!/usr/bin/env bash
# backup-aurora.sh — logical backup with rotation and restore verification
set -euo pipefail
DEST="${DEST:-/srv/backups/aurora}"
RETENTION_DAYS="${RETENTION_DAYS:-14}"
FILE="$DEST/aurora-$(date +%Y%m%d-%H%M).dump"
mkdir -p "$DEST"
echo "==> Logical backup of aurora_books"
docker compose exec -T aurora-db \
pg_dump -U aurora -d aurora_books --format=custom --compress=9 > "$FILE"
# A 0-byte backup is a silent failure: always check
[ -s "$FILE" ] || { echo "ERROR: empty backup"; exit 1; }
echo "==> Verifying the restore in a throwaway database"
docker run -d --rm --name verify-backup \
-e POSTGRES_USER=aurora -e POSTGRES_PASSWORD=verify -e POSTGRES_DB=verify \
--tmpfs /var/lib/postgresql/data postgres:16-alpine >/dev/null
for i in $(seq 1 30); do
docker exec verify-backup pg_isready -U aurora -d verify -q && break || sleep 1
done
docker exec -i verify-backup pg_restore -U aurora -d verify --no-owner < "$FILE"
TOTAL=$(docker exec verify-backup psql -U aurora -d verify -tAc 'SELECT count(*) FROM books')
docker rm -f verify-backup >/dev/null
[ "$TOTAL" -ge 9 ] || { echo "ERROR: the backup only has $TOTAL books"; exit 1; }
echo "==> Restore verified: $TOTAL books"
echo "==> Rotation: deleting backups older than $RETENTION_DAYS days"
find "$DEST" -name 'aurora-*.dump' -mtime "+$RETENTION_DAYS" -print -delete==> Logical backup of aurora_books
==> Verifying the restore in a throwaway database
==> Restore verified: 9 books
==> Rotation: deleting backups older than 14 daysWhat makes this script serious is not the backup, it is the verification: every run restores the file it has just created into an ephemeral PostgreSQL on tmpfs and counts the rows. If the backup was empty, truncated or corrupt, the script fails today, not on the day of the disaster.
Burn the rule into your memory: a backup that has never been restored is not a backup, it is a folder with hope inside.
- Disk space and
data-root
data-rootEverything Docker stores lives under /var/lib/docker:
| Subdirectory | Contents | How it is cleaned |
|---|---|---|
overlay2/ |
Image and container layers | docker image prune -a, container prune |
volumes/ |
Named and anonymous volumes | docker volume prune (carefully) |
containers/ |
Metadata and logs in JSON | Log rotation (lesson 05-06) |
buildkit/ |
Build cache | docker builder prune |
image/, network/ |
Indexes and metadata | They manage themselves |
4.1G /var/lib/docker/overlay2
1.3G /var/lib/docker/volumes
612M /var/lib/docker/buildkit
88M /var/lib/docker/containersIf containers/ is enormous, the culprit is unrotated logs (you will fix that in 05-06). If overlay2/ is, it is old images. To move the store to a bigger disk:
sudo systemctl stop docker
sudo rsync -aHAX /var/lib/docker/ /data/docker/
printf '{\n "data-root": "/data/docker"\n}\n' | sudo tee /etc/docker/daemon.json
sudo systemctl start docker
docker info --format 'Docker Root Dir: {{.DockerRootDir}}' # -> /data/dockerUse rsync -aHAX and not cp: hard links, extended attributes and permissions have to be preserved, or the layers will be useless. Check that everything works before deleting the old directory.
- Performance and permissions (UID/GID)
Choosing a mount by performance, from best to worst for write-intensive workloads:
| Mount | Write performance | Recommended use |
|---|---|---|
tmpfs |
Maximum (RAM) | Ephemeral data, tests, temporary files |
| Named volume | Native to the filesystem | Databases and all persistent data |
| Bind mount (Linux) | Native | Code in development, configuration |
| Bind mount (macOS/WSL 2 crossing the boundary) | Poor | Avoid for many small files |
| The container's writable layer | The worst: copy-up | Nothing that is written repeatedly |
Permissions are the other half of the problem. An empty volume inherits the owner of the image's mount point; a volume with data keeps its own, and aurora-api runs as the node user (UID 1000), so a volume created by root is unreadable to it.
docker volume create aurora-attachments
docker run --rm -v aurora-attachments:/data alpine:3 stat -c '%u:%g' /data # -> 0:0
docker run --rm -v aurora-attachments:/data alpine:3 chown -R 1000:1000 /data # just once
docker run --rm -u 1000 -v aurora-attachments:/data alpine:3 sh -c 'touch /data/ok && echo OK'With bind mounts the fix is different, because the owner is set by the host: either you align the container's UID with your user's (user: "${UID}:${GID}" in Compose), or you adjust permissions on the host. And remember from lesson 03-06 that the kernel only understands numbers: the name node means nothing outside the container.
Common Mistakes and Tips
Writing persistent data to the container's layer. Terrible performance because of copy-up, and total loss when the container is recreated.
Running tar on a volume while the database is up. The backup looks fine and is incoherent. Stop the service or use pg_dump.
Forgetting -T in docker compose exec pg_dump. The pseudo-terminal corrupts the binary dump without warning.
Never testing a restore. On the day of the incident you find out you have been storing empty files for months.
Running docker volume prune without looking. It deletes every volume not used by any container, and a volume belonging to a stopped stack fits that definition. Declare anything critical as external.
Changing the storage driver without a prior backup, or putting PostgreSQL data on NFS. In the first case images and containers vanish from the inventory; in the second, network file locking does not give the guarantees a database needs.
Tip: label your volumes when you create them (--label criticality=high) and filter by that label before any cleanup: docker volume ls --filter label=criticality=high. A minute of labeling avoids the deletion that has no undo.
Exercises
Exercise 1. Demonstrate copy-on-write in both directions: locate aurora-api's upperdir, create a file inside the container and find it on the host's disk; then measure the cost of modifying one byte of a 256 MB file that comes in the image, and compare it with modifying one byte of a file already present in the writable layer.
Exercise 2. Set up the full backup and restore cycle for aurora-db: take a logical backup, destroy the data by deleting the volume, restore, and check that the nine titles are back. Explain at exactly which point the platform would have been left unusable without the backup.
Exercise 3. Turn aurora-data into an external volume, prove that docker compose down -v no longer deletes it, and explain what protection this gives and what its downside is.
Solutions
Solution 1.
up=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo "hello layers" > /tmp/test.txt'
sudo cat "$up/tmp/test.txt"
docker diff aurora-libros-aurora-api-1 | grep testprintf 'FROM alpine:3\nRUN dd if=/dev/zero of=/g.bin bs=1M count=256\n' > /tmp/Dockerfile.cow
docker build -q -t cow256 -f /tmp/Dockerfile.cow /tmp
docker run --rm cow256 sh -c 'time dd if=/dev/zero of=/g.bin bs=1 count=1 conv=notrunc 2>/dev/null'
docker run --rm cow256 sh -c 'cp /g.bin /c.bin; time dd if=/dev/zero of=/c.bin bs=1 count=1 conv=notrunc 2>/dev/null'One second versus zero, to write exactly the same byte. The difference is the copy-up: in the first case the file lives in a read-only lowerdir and the kernel has to copy all 256 MB to the upperdir before allowing the write; in the second, cp had already brought it into the writable layer. Extrapolate: PostgreSQL writing non-stop to data files without a volume would pay that toll over and over, on top of multiplying the space it occupies. Here you can see, in one second of wall clock, why the decision made in lesson 03-06 was not a convention but a physical necessity.
Solution 2.
docker compose exec -T aurora-db pg_dump -U aurora -d aurora_books -Fc > /tmp/aurora.dump
docker compose exec aurora-db psql -U aurora -d aurora_books -tAc 'SELECT count(*) FROM books'
docker compose down -v # the disaster
docker volume ls --filter name=aurora-data -q | wc -l
docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_books -tAc 'SELECT count(*) FROM books'There is a nuance here that confuses a lot of people: after the down -v the database comes back with 9 books without anything having been restored, because the volume has been recreated empty and PostgreSQL has run db/init.sql again. That is exactly what makes the situation dangerous: it looks as if nothing was lost.
What has been lost is the data added after initialization, which is everything that matters in production:
docker compose exec aurora-db psql -U aurora -d aurora_books \
-c "INSERT INTO books (title, author, year, isbn) VALUES ('Trafalgar','B. Pérez Galdós',1873,'978-84-000001-0')"
docker compose exec -T aurora-db pg_dump -U aurora -d aurora_books -Fc > /tmp/aurora10.dump
docker compose down -v && docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_books -tAc 'SELECT count(*) FROM books'
docker compose exec -T aurora-db pg_restore -U aurora -d aurora_books --clean --if-exists --no-owner < /tmp/aurora10.dump
docker compose exec aurora-db psql -U aurora -d aurora_books -tAc 'SELECT count(*) FROM books'After the disaster 9 remain (the ones from init.sql); after the restore, the real 10. The exact point at which the platform would have been left unusable is the down -v: the -v deletes the volumes, and with them the only place where the user-added data existed. Without /tmp/aurora10.dump, that row is not recoverable from anywhere.
Solution 3.
docker compose down && docker volume create --label criticality=high aurora-data-ext
docker run --rm -v aurora-libros_aurora-data:/source:ro -v aurora-data-ext:/target \
alpine:3 sh -c 'cp -a /source/. /target/'docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_books -tAc 'SELECT count(*) FROM books'
docker compose down -v
docker volume ls --filter name=aurora-data-ext --format '{{.Name}}'
docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_books -tAc 'SELECT count(*) FROM books'The down -v has not deleted the volume and the data is still intact: Compose refuses to destroy what it did not create. The protection is real and costs two lines of YAML, so for the data volume of a production platform it is an almost automatic decision.
The downside has two faces. The first is operational: the volume no longer creates itself, and whoever deploys for the first time on a new server has to run docker volume create before the up, or Compose will fail with external volume not found. The second is conceptual: you lose the reproducibility of an environment built from scratch. In development that is annoying —the reset from lesson 04-07 stops working— so the sensible practice is to declare external only in compose.prod.yaml, leaving the local environment disposable.
Conclusion
You now know what happens beneath -v. A storage driver stacks an image's layers and applies copy-on-write, and overlay2 does it with four directories you have seen on disk: the image's read-only lowerdirs, the upperdir where every file the container writes appears, the merged view that the process sees as /, and the kernel's internal workdir. From those come the three resolution rules, including the whiteout rule that explains why deleting a file in a later layer frees no space. You know the full driver table —btrfs, zfs, the retired devicemapper, fuse-overlayfs for rootless and the painfully slow vfs— and you know that changing driver makes all your images invisible.
And you have measured copy-on-write: almost two seconds and 512 MB of layer to write a single byte to a file that came in the image. That number turns a recommendation into physics: a database writes constantly to large files, therefore it goes in a volume, which bypasses OverlayFS and writes straight to the host's filesystem. You know how to squeeze the local driver with its driver_opts —a named bind, a sized tmpfs, NFS for the cover images and never for PostgreSQL—, how to declare third-party plugins in Compose, and how to use external volumes so that down -v cannot take production data with it. You also take away a backup strategy that is a strategy and not an intention: the table separating the hot logical backup with pg_dump from the file-level backup with tar —and why the second requires stopping the service—, a script with rotation that restores and counts rows on every run, the rule that a backup which has never been restored is not a backup, and control over the space in /var/lib/docker through data-root.
Lesson 05-03 brings security, and with it the full hardening of Aurora Libros: the threat model and its four surfaces, why belonging to the docker group is equivalent to being root —with the escalation demonstrated—, rootless mode, minimal images pinned by digest, --read-only, kernel capabilities, seccomp, vulnerability scanning with Docker Scout and Trivy, SBOM and signing with Cosign, and a hardened compose.prod.yaml line by line with its actionable checklist.
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
