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

  1. What a storage driver is and what it solves
  2. overlay2 from the inside: the four directories
  3. Real inspection of an image and a container
  4. Comparison table of the storage drivers
  5. Checking the active driver and changing it
  6. The cost of copy-on-write, measured
  7. Why databases demand volumes
  8. The local driver and its driver_opts
  9. Mounting NFS and tmpfs as a volume
  10. Third-party plugins and declaring them in Compose
  11. external volumes shared between stacks
  12. The Aurora Libros backup strategy
  13. A backup script with rotation and verified restore
  14. Disk space and data-root
  15. Performance and permissions (UID/GID)

  1. 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-alpine take 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.

  1. overlay2 from the inside: the four directories

overlay2 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:

  1. Reading a file: it is looked up in upperdir; if it is not there, lowerdir is walked from top to bottom. The first match wins.
  2. Writing to a file that only exists in lowerdir: it is copied whole to upperdir (copy-up) and written there. The original is untouched.
  3. Deleting a file from lowerdir: you cannot delete something that is read-only, so a special whiteout file is created in upperdir to 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

  1. 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/work

Notice 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 -2
layer test
/var/lib/docker/overlay2/9be21c/diff/tmp/aurora.txt

The 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.

  1. 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.

  1. Checking the active driver and changing it

docker info --format 'Driver: {{.Driver}} | Backing FS: {{index .DriverStatus 0 1}}'
Driver: overlay2 | Backing FS: extfs

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.

  1. 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 cow
real    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

  1. 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.

docker compose exec aurora-db sh -c 'mount | grep -E "postgresql| / "'
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.

  1. The local driver and its driver_opts

The 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.

  1. Mounting NFS and tmpfs as a volume

An 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"
docker compose exec aurora-api sh -c 'mount | grep covers'
10.0.0.20:/exported/aurora/covers on /app/covers type nfs4 (rw,relatime,vers=4.2)

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.

  1. 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.

  1. external volumes shared between stacks

Compose 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:

docker volume create --label project=aurora --label criticality=high aurora-data
volumes:
  aurora-data:
    external: true        # it already exists; Compose neither creates nor deletes it

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.

  1. 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, 18K

The -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-db

Why 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.

  1. 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 days

What 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.

  1. Disk space and data-root

Everything 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
sudo du -sh /var/lib/docker/* 2>/dev/null | sort -rh | head -4
4.1G    /var/lib/docker/overlay2
1.3G    /var/lib/docker/volumes
612M    /var/lib/docker/buildkit
88M     /var/lib/docker/containers

If 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/docker

Use 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.

  1. 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 test
hello layers
A /tmp/test.txt
printf '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'
real    0m 0.98s
real    0m 0.00s

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'
9
0
9

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'
9
10

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/'
# compose.yaml
volumes:
  aurora-data:
    name: aurora-data-ext
    external: true
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'
10
aurora-data-ext
10

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

Module 2: Working with Docker Images

Module 3: Docker Containers

Module 4: Docker Compose

Module 5: Advanced Docker Concepts

Module 6: Docker in Production

Module 7: Docker Ecosystem and Tools

© Copyright 2026. All rights reserved