Docker solves building and running containers. Everything else — seeing what is going on without diving into docker ps, knowing why an image is heavier than it should be, catching vulnerabilities before publishing, running your own registry, testing against a real database — is solved by the ecosystem around it. This lesson is not a catalog: it is the equipment selected with judgment, plus the judgment for selecting it yourself.

Contents

  1. The three levels of extensibility
  2. Build your own docker aurora
  3. Daemon plugins and desktop extensions
  4. Management interfaces
  5. Image inspection and optimization
  6. Security
  7. Private registries
  8. Development and testing
  9. Building without a Dockerfile
  10. Cleanup and maintenance
  11. Networking and diagnostics
  12. Criteria for adopting a tool
  13. The recommended minimum kit

  1. The three levels of extensibility

Level Mechanism Scope Risk if it fails
CLI A docker-<name> binary on the PATH or in ~/.docker/cli-plugins/ Your terminal Low: the subcommand does not start
Daemon Volume, network or logging plugins (the plugin API) The whole host High: it can bring containers down
Desktop Docker Desktop extensions (a container + UI) Your machine Medium: third-party code with access to the daemon

The rule that follows: experiment freely at the CLI level, be conservative at the daemon level. A badly written logging plugin blocks the startup of every container on the host.

  1. Build your own docker aurora

The CLI plugin mechanism is astonishingly simple: any executable called docker-something in ~/.docker/cli-plugins/ becomes docker something. That is how docker compose, docker buildx and docker scout work. The only formal requirement is answering the hidden docker-cli-plugin-metadata subcommand.

#!/usr/bin/env bash
# ~/.docker/cli-plugins/docker-aurora — Aurora Libros team shortcuts
set -euo pipefail

if [ "${1:-}" = "docker-cli-plugin-metadata" ]; then
  cat <<'JSON'
{ "SchemaVersion": "0.1.0", "Vendor": "Aurora Libros S.L.",
  "Version": "1.0.0", "ShortDescription": "Aurora platform shortcuts" }
JSON
  exit 0
fi

shift                     # the first argument is always "aurora"
case "${1:-help}" in
  up)      docker compose -f compose.yaml -f compose.dev.yaml up -d --wait ;;
  down)    docker compose down ;;
  logs)    docker compose logs -f --tail=100 "${2:-api}" ;;
  psql)    docker compose exec -it aurora-db psql -U aurora -d aurora_books ;;
  redis)   docker compose exec -it aurora-cache redis-cli ;;
  health)  curl -fsS localhost:8080/health/ready | jq . ;;
  books)   curl -fsS localhost:8080/books | jq -r '.books[] | "\(.id)  \(.title)"' ;;
  reset)   docker compose down -v && docker compose up -d --wait ;;
  *) echo "Usage: docker aurora {up|down|logs|psql|redis|health|books|reset}" ;;
esac
chmod +x ~/.docker/cli-plugins/docker-aurora
docker aurora up
docker aurora books
# 1  El jardín de senderos que se bifurcan
# 2  Rayuela
# 3  Cien años de soledad
# ...
docker --help | grep aurora
#   aurora*  Aurora platform shortcuts (Aurora Libros S.L. 1.0.0)

Forty lines and the whole team stops memorizing long commands. Keep it in the repository with a make install-plugin and it becomes part of onboarding. The asterisk in the help output means it is a plugin, not a native command.

  1. Daemon plugins and desktop extensions

Daemon plugins extend Docker from underneath. You install them with docker plugin install and manage them separately:

docker plugin install grafana/loki-docker-driver:latest --alias loki
docker plugin ls
docker run --log-driver=loki --log-opt loki-url="http://loki:3100/loki/api/v1/push" ...
Type Examples When it is justified
Volume rexray, local-persist, NFS/CIFS Network storage from Compose
Network weave, calico Multi-host networks outside Swarm/K8s
Logging loki, fluentd, gelf Centralizing logs without a sidecar (05-06)

The serious warning: a logging plugin that hangs can stop containers from starting at all, because the daemon waits for it to accept the stream. Before putting it into production, test it with the destination deliberately down and check that mode: non-blocking is configured.

Docker Desktop extensions are containers with a user interface that plug into the dashboard. Convenient, and with the same caution as any third-party code with access to the daemon socket: whoever controls the socket controls the host (05-03).

  1. Management interfaces

Tool Type Strong at Weak at License
lazydocker Terminal TUI Viewing logs, restarting, stats without typing Local only MIT
ctop TUI, top-style Seeing the whole fleet's usage at a glance No advanced management MIT
Portainer Web, server Teams, permissions, several hosts, Swarm/K8s Heavy; CE cut down compared with BE Zlib (CE)
Dockge Web, server Managing Compose stacks by editing the YAML Compose only, young project MIT
Docker Desktop Local GUI Integrated, builds with timings Desktop only, licensing Proprietary
# lazydocker: zero permanent installation
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \
  -v ~/.config/lazydocker:/.config/jesseduffield/lazydocker \
  lazyteam/lazydocker

With the Aurora Libros stack up, lazydocker gives you all four containers on a single screen, with their live logs and their resource usage, and it lets you restart aurora-api with one keystroke. For local debugging it is faster than any GUI.

Portainer is only justified when there are several people operating several hosts and per-user permissions are needed. For one server and a small team on Compose, Dockge is lighter and does not hide the YAML: you still have your files and you edit them from the browser.

A caution common to all of them: they expose the Docker socket. Portainer or Dockge reachable from the Internet without strong authentication is the same as handing over the host.

  1. Image inspection and optimization

Tool What it does When to use it
dive Walks the image layer by layer and calculates the waste Before signing off on an image
slim (formerly docker-slim) Runs the app, watches what it uses and discards the rest Legacy images you cannot rewrite
container-diff Compares two images: packages, files, sizes Auditing what changed between 2.0.0 and 2.1.0
docker history Size per layer, with nothing to install A quick first look
dive ghcr.io/auroralibros/aurora-api:2.0.0
# Image Details: Total Image size: 104 MB
#                Potential wasted space: 1.8 MB
#                Image efficiency score: 98%

That 98 % is the result of the work from 05-04: the multi-stage build left the compilation dependencies out and there are no duplicated files across layers. In integration mode, dive returns a non-zero exit code if the project drops below the threshold, and that plugs straight into the pipeline:

CI=true dive --ci --lowestEfficiency 0.95 ghcr.io/auroralibros/aurora-api:2.0.0
# container-diff: what actually changed between two versions
container-diff diff --type=apt --type=file --type=size \
  daemon://aurora-api:2.0.0 daemon://aurora-api:2.1.0

About slim: it slims images down automatically by running the application and keeping only what it touched. It can take a 400 MB image down to 30 MB, and it can also break it silently the day a code path that was not exercised during the analysis gets executed. Use it with a complete test suite behind it, and always prefer fixing the Dockerfile if you have access to it.

  1. Security

Tool What it analyzes When License
hadolint The Dockerfile, before building Editor and CI GPL-3.0
Trivy Vulnerabilities, secrets, IaC, SBOM CI and registry Apache 2.0
Grype Vulnerabilities (paired with Syft) CI, second opinion Apache 2.0
Docker Scout Vulnerabilities + recommendations Desktop and CI Proprietary
Docker Bench Host and daemon configuration Periodic audit Apache 2.0
Falco Behavior at runtime Production, continuous Apache 2.0
Cosign Image signing and verification Publishing and admission Apache 2.0

hadolint is the cheapest to adopt and the one that prevents the most problems, because it acts before anything gets built. Against an early version of the API's Dockerfile:

docker run --rm -i hadolint/hadolint < Dockerfile
# Dockerfile:1 DL3006 warning: Always tag the version of an image explicitly
# Dockerfile:4 DL3009 info: Delete the apt-get lists after installing something
# Dockerfile:7 DL3016 warning: Pin versions in npm. npm install <package>@<version>
# Dockerfile:9 DL3020 error: Use COPY instead of ADD for files and folders
# Dockerfile:12 DL3002 warning: Last USER should not be root
# Dockerfile:14 SC2086 info: Double quote to prevent globbing and word splitting

Six warnings that are exactly the syllabus of modules 2 and 5: pin the base image, clean up package caches, pin versions, use COPY instead of ADD, do not finish as root, and quote variables in RUN. Against the current Dockerfile of aurora-api:2.0.0 not one of them appears, because the base is pinned by digest and the final USER is not root. Integrating it costs four lines:

# .github/workflows/ci.yml (fragment)
- name: Lint the Dockerfile
  uses: hadolint/hadolint-action@v3
  with: { dockerfile: aurora-api/Dockerfile, failure-threshold: warning }

Falco covers the gap nobody else covers: the others analyze artifacts sitting still; Falco watches live syscalls and alerts when a container does something it should not — opening a shell inside aurora-api, writing to /etc, connecting to an unknown IP. It is the last line of defense when the vulnerability was not in any database.

# A custom Falco rule for Aurora Libros
- rule: Shell in aurora-api
  desc: The production API must never open a shell
  condition: spawned_process and container.image.repository contains "aurora-api"
             and proc.name in (sh, bash, ash)
  output: "Unexpected shell in aurora-api (user=%user.name cmd=%proc.cmdline)"
  priority: CRITICAL

About the first three in the table: you do not have to pick just one. Trivy in the pipeline as the gate that fails the build, Scout on the desktop for the early check, and Grype as a second opinion when a result is surprising. Each scanner uses different sources and they do not always agree.

  1. Private registries

Option Strong at Cost When
registry:2 Trivial to run, official None Lab, local cache
Zot OCI only, lightweight, signing and search Low A modern private registry
Harbor Users, policies, scanning, replication, quotas High: it is a platform A company with governance
ghcr.io and similar Zero maintenance Subscription The default case

One use that almost always pays off and that almost nobody sets up: a Docker Hub mirror on the local network. It saves bandwidth, dodges pull rate limits and keeps the pipeline working when Hub is having a bad afternoon.

# compose.registry.yaml — read-only Docker Hub mirror
services:
  mirror:
    image: registry:2
    ports: ["5000:5000"]
    environment:
      REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io
    volumes: ["registry-data:/var/lib/registry"]
volumes: { registry-data: }
// /etc/docker/daemon.json on each host
{ "registry-mirrors": ["http://registry.internal:5000"] }

Harbor is only justified when there are several teams, retention policies, cross-region replication and a need for auditing. It is a platform with a database, Redis and several components: somebody has to maintain it.

  1. Development and testing

Tool What for Fits with
Testcontainers Bringing up real dependencies from the test itself Any language; CI
Tilt An inner loop with Kubernetes: save and see the change Teams with a cluster
Skaffold Continuous build+deploy towards K8s Similar, more opinionated
devcontainer A reproducible development environment in the editor VS Code and compatible editors
docker init Initial Dockerfile and Compose scaffolding New projects

Testcontainers is the one that changes your life the most and the one fewest people know about. In 06-02 you set up PostgreSQL and Redis services in the CI workflow; Testcontainers makes the test itself bring them up, so the tests run identically on your laptop and in the pipeline with no duplicated configuration:

// aurora-api/test/books.test.js — Node 22, the native node:test runner
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { GenericContainer, Wait } from 'testcontainers';
import { createApp } from '../src/app.js';

let pg, redis, app;

before(async () => {
  pg = await new PostgreSqlContainer('postgres:16-alpine')
    .withDatabase('aurora_books').withUsername('aurora').withPassword('secret')
    .withCopyFilesToContainer([{ source: './db/init.sql',
                                 target: '/docker-entrypoint-initdb.d/init.sql' }])
    .start();

  redis = await new GenericContainer('redis:7-alpine')
    .withExposedPorts(6379)
    .withWaitStrategy(Wait.forLogMessage('Ready to accept connections'))
    .start();

  app = createApp({
    dbUrl: pg.getConnectionUri(),
    redisUrl: `redis://${redis.getHost()}:${redis.getMappedPort(6379)}`
  });
});

after(async () => { await pg.stop(); await redis.stop(); });

test('GET /books returns the nine titles of the catalog', async () => {
  const res = await app.inject({ method: 'GET', url: '/books' });
  assert.equal(res.statusCode, 200);
  const { books } = res.json();
  assert.equal(books.length, 9);
  assert.ok(books.some(b => b.title === 'Rayuela'));
});

test('the second read of a book comes from the cache', async () => {
  await app.inject({ method: 'GET', url: '/books/3' });          // warm it up
  const res = await app.inject({ method: 'GET', url: '/books/3' });
  assert.equal(res.json().source, 'cache');                       // cache-aside
});
node --test test/          # brings up PostgreSQL and Redis, tests, and destroys them

The second test is what justifies the tool: checking that cache-aside returns source: cache requires a real Redis. With a test double you would have validated your own mock-up, not the real behavior. Testcontainers needs an accessible daemon; in CI you already have one, and it works just the same against Podman with the compatible socket (07-05).

Tilt and Skaffold solve a different problem: when you develop against a cluster, the build-push-deploy cycle takes minutes. Both cut it down to seconds by syncing files inside the Pod. They only pay off if your inner loop is Kubernetes; if you develop with docker compose watch (04-07), you do not need them.

  1. Building without a Dockerfile

Tool How it builds Strong at Limit
Buildpacks (pack) Detects the language and applies buildpacks No Dockerfile, automatic base patching Bigger images, less control
Jib A Maven/Gradle plugin, with no daemon Java: very fast, optimal layers JVM only
ko Compiles and packages Go binaries Go: seconds, minimal image Go only
Nixpacks Infers the environment with Nix Very automatic, used by PaaS providers Less well known, reproducible but opaque
pack build aurora-api:pack --builder paketobuildpacks/builder-jammy-base
docker images aurora-api
# aurora-api  pack    412MB      ← against the 104 MB of your multi-stage build
# aurora-api  2.0.0   104MB

There is the trade-off in a single line. Buildpacks saves you writing and maintaining the Dockerfile, and in exchange it produces an image four times bigger. When does it pay off?

Not writing a Dockerfile pays off Writing it pays off
Many small, similar services Few images, heavily tuned
Nobody on the team knows Docker well The knowledge is there (you already have it)
Automatic base patching is valued The base is pinned by digest
Size and startup time do not matter Size matters (edge, scaling)
An internal platform that standardizes Specific security requirements

For Aurora Libros, with a well-optimized, multi-architecture, signed image, it does not pay off. For an internal platform with forty microservices, it probably does.

  1. Cleanup and maintenance

The disk fills up on its own. Automate it before an alert wakes you up:

# /etc/cron.weekly/docker-maintenance — on every Aurora Libros host
#!/bin/sh
docker image prune -a --filter "until=168h" -f     # images older than 7 days
docker builder prune --filter "until=168h" -f      # BuildKit cache
docker container prune --filter "until=24h" -f     # stopped containers
# NEVER --volumes here: aurora-data lives in a volume
docker system df >> /var/log/docker-maintenance.log
Tool What it brings Watch out for
docker system prune Native, enough for almost everything --volumes destroys data
docker builder prune --keep-storage A fixed ceiling for the BuildKit cache —
docker-gc / docker-cleanup Finer policies, containers Projects with uneven maintenance
Registry retention policy Prevents thousands of stale tags Configure it in ghcr.io or Harbor

The until filter is the difference between a safe cleanup and a full pull the following morning. And the comment line is the most important one in the script.

  1. Networking and diagnostics

netshoot is an image with every networking tool that your production images, quite rightly, do not carry: dig, curl, tcpdump, ss, iperf3, nmap.

# Debugging Aurora Libros internal DNS and connectivity (05-01)
docker run --rm -it --network aurora-libros_backend nicolaka/netshoot \
  sh -c 'dig +short aurora-db; nc -zv aurora-cache 6379'

# Getting inside the API container's own network namespace
docker run --rm -it --network container:aurora-api nicolaka/netshoot \
  ss -tnp

The second form is the powerful one: --network container: shares the network namespace of aurora-api (05-07), so you see exactly its connections and its ports without installing anything inside it or modifying the image. It is the same thing docker debug does, with a public image and no subscription.

dockviz draws the image and container tree; useful for making sense of a legacy host with forty images of unknown origin.

  1. Criteria for adopting a tool

Criterion The concrete question Warning sign
Maintenance Commits and closed issues in the last 6 months? Last release two years ago
License Is it compatible with the commercial use you will give it? GPL in something you redistribute; "free for now"
Provenance Who publishes it? Is the image signed? An image from an anonymous Hub user
Criticality If it disappears tomorrow, does deployment stop? A one-author binary on the critical path
Understanding Does the team know what it does inside? "I copied it from a blog and it works"
Exit How much does it cost to remove it? Proprietary formats, no export
Surface Does it need the Docker socket or privileges? Yes, and exposed to the network on top of that
Overlap Does it do something you already do with what you have? Three scanners saying the same thing

Two golden rules. First: the closer to the pipeline, the more demanding you should be. A local TUI that gets abandoned is replaced in five minutes; a plugin that signs your images and stops receiving patches is a security problem and a deployment problem at the same time. Second: validate with your security lead any tool that enters the build chain or that needs the daemon socket, and check its license with whoever is responsible before integrating it into a commercial product. Adding a dependency to the pipeline is an architectural decision, not a personal preference.

  1. The recommended minimum kit

Profile Essential Recommended Skippable
Learning lazydocker, dive hadolint Portainer, Tilt
Application developer hadolint, dive, Testcontainers lazydocker, netshoot Buildpacks, Falco
DevOps / platform Trivy, Cosign, hadolint, netshoot Harbor or Zot, Falco, Portainer slim, Nixpacks
Operating in production Trivy, Falco, Docker Bench, ctop Portainer, dockviz dive, docker init
Team on Kubernetes Trivy, Cosign, Tilt or Skaffold k9s, Harbor Dockge, lazydocker

If you could only adopt three for the rest of your career: hadolint (it prevents mistakes before you build), Trivy (it stops you publishing something vulnerable) and Testcontainers (it makes your tests mean something). All three are open source, maintained, and they tie nobody down.

Common Mistakes and Tips

  • Exposing Portainer or Dockge to the Internet. They control the daemon socket: whoever gets in is root on the host. VPN or internal network, always, and with strong authentication.
  • Installing a logging plugin without testing what happens when the destination fails. If the destination does not answer and the mode is blocking, containers do not start. Test with Loki deliberately down.
  • Trusting slim alone. It slims down by observing one run; what was not executed disappears. Without a complete test suite behind it, it is a time bomb.
  • Piling up three scanners that say the same thing. One as the pipeline gate, another as an occasional second opinion. Any more only generates noise nobody reads.
  • Adding --volumes to the scheduled cleanup. It is the fastest way to lose aurora-data on a Sunday night.
  • Adopting a tool because it showed up at a conference. Apply the table from §12 first: maintenance, license, criticality and exit cost.
  • Tip: put hadolint in the pipeline today. It costs four lines and prevents half a dozen bad practices per Dockerfile.
  • Tip: keep the docker aurora plugin in the repository. Team shortcuts stop living in everybody's individual bash history.
  • Tip: learn netshoot with --network container:. It solves 90 % of network debugging without touching the image.

Exercises

Exercise 1 — Extend docker aurora. Start from the plugin in §2 and add three subcommands: scan, which runs Trivy against the local API image and fails if there are critical vulnerabilities; weigh, which shows the image size and runs dive --ci with a 95 % efficiency threshold; and net, which launches netshoot in the network namespace of aurora-api and checks that it resolves aurora-db and reaches aurora-cache. The plugin must keep working with nothing installed beyond Docker.

Exercise 2 — Linting and fixing a Dockerfile. Deliberately write a bad Dockerfile for aurora-api (untagged base, apt-get without cleanup, ADD instead of COPY, npm install with no pinned version, no USER), run hadolint over it and note every warning code. Fix them one by one until the lint comes out clean, explaining in a comment which real problem each fix prevents. Then add the check to the pipeline with a warning threshold.

Exercise 3 — Adoption assessment. A colleague proposes adding a tool to the pipeline that automatically optimizes images before publishing them. It is published by an individual developer on Docker Hub, it has 900 stars on GitHub, the last commit is eleven months old and the image is not signed. Apply the criteria table from §12 and write a reasoned answer: your recommendation, the three concrete risks you identify, and what conditions would have to be met for you to accept it.

Solutions

Solution 1.

# Add inside the case block of ~/.docker/cli-plugins/docker-aurora
  scan)
    IMG="${2:-ghcr.io/auroralibros/aurora-api:2.0.0}"
    docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
      -v "$HOME/.cache/trivy:/root/.cache/trivy" \
      aquasec/trivy:latest image --severity CRITICAL,HIGH \
      --exit-code 1 --ignore-unfixed "$IMG"
    ;;
  weigh)
    IMG="${2:-ghcr.io/auroralibros/aurora-api:2.0.0}"
    docker image inspect "$IMG" --format 'Size: {{.Size}} bytes ({{len .RootFS.Layers}} layers)'
    docker run --rm -e CI=true -v /var/run/docker.sock:/var/run/docker.sock \
      wagoodman/dive:latest --ci --lowestEfficiency 0.95 "$IMG"
    ;;
  net)
    docker run --rm --network container:aurora-api nicolaka/netshoot sh -c '
      echo "DNS aurora-db -> $(dig +short aurora-db)"
      nc -zv aurora-cache 6379 && echo "cache reachable"
      ss -tnp | head -20'
    ;;
docker aurora scan && docker aurora weigh && docker aurora net

Three decisions matter more than the rest. Mounting the socket is essential because both Trivy and dive inspect local images from the daemon, not remote ones; mounting ~/.cache/trivy avoids downloading the entire vulnerability database on every run, which is what makes the first run take a minute and the following ones three seconds. The --exit-code 1 turns the scan into a gate: without it, Trivy reports and returns 0, and the pipeline would pass with critical vulnerabilities. And --ignore-unfixed avoids the noise of flaws with no patch available yet, which you cannot act on. In net, --network container:aurora-api enters the real container's network namespace (05-07), so what you see is its DNS resolution and its connections, not those of some arbitrary network.

Solution 2. The bad Dockerfile and the codes it triggers:

FROM node                          # DL3006: no tag or digest
RUN apt-get update && apt-get install -y curl   # DL3009/DL3015: lists not cleaned
ADD . /app                         # DL3020: ADD for local files
WORKDIR /app
RUN npm install                    # DL3016: no pinned versions
CMD npm start                      # DL3025: CMD in shell form
Code Warning The real problem it prevents
DL3006 Tag the base image node means latest: the build is not reproducible (05-04)
DL3009 Delete the apt lists ~40 MB of permanent junk in the layer
DL3015 Use --no-install-recommends Packages nobody asked for, more attack surface
DL3020 COPY instead of ADD ADD unpacks archives and downloads URLs: unexpected behavior
DL3016 Pin npm versions A build today and one tomorrow install different things
DL3025 CMD in exec form In shell form the process never receives SIGTERM: goodbye graceful shutdown (06-01)
DL3002 The last USER should not be root Root in the container is root in the kernel (05-03)

The fixed version:

FROM node:22-alpine@sha256:1f8c...  AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev               # reproducible from the lockfile

FROM node:22-alpine@sha256:1f8c...
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --chown=node:node src ./src
USER node
CMD ["node", "src/index.js"]        # exec form: PID 1 receives the signals
docker run --rm -i hadolint/hadolint < Dockerfile   # no output = clean

The most underestimated warning is DL3025: with CMD npm start, PID 1 is a shell that does not forward SIGTERM, and all the graceful shutdown you built in 06-01 stops working without a single test noticing. It shows up as requests cut off during deployments, and it takes days to find if you do not know where to look.

Solution 3. Recommendation: do not adopt it in the pipeline in its current state.

Criterion (§12) Assessment
Maintenance Last commit 11 months ago: if a bug appears, nobody fixes it
Provenance An individual author, an unsigned image on Hub: you cannot verify what you are running
Criticality It would sit on the publishing path: if it fails, nothing gets deployed
License Not checked in the brief; it has to be verified before anything else
Understanding "Optimizes automatically" is a black box over the artifact that goes to production

The three concrete risks: (1) supply chain — an unsigned image from an anonymous author, running in CI with access to your images and probably to the socket, is exactly the vector that Cosign and the attestations from 06-02 were brought in to close; (2) silent breakage, because an automatic optimization can remove a file that is only used on an infrequent code path, and you would find out in production; (3) abandonment, since eleven months without commits in a tool that touches the final artifact means that the day it breaks with a new version of BuildKit, deployment stops and the fix is yours.

Conditions for accepting it: that it runs off the critical path, as a comparative report and not as a step that modifies the published image; that the image is signed and pinned by digest; that there is an integration test suite that validates the optimized image before publishing it; and that the security lead validates the license and the provenance. A reasonable counter-proposal: your colleague's goal — smaller images — is already covered by the multi-stage build and dive --ci with a threshold, which are part of the pipeline, do not depend on third parties, and already give 98 % efficiency on aurora-api:2.0.0. The conversation is not "no", it is "this problem is already solved".

Conclusion

You now have the equipment that surrounds Docker and, more importantly, the judgment to choose it. You understand the three levels of extensibility and their asymmetry of risk: at the CLI level experiment freely, at the daemon level be conservative, because a blocked logging plugin stops containers from starting. And you have built your own docker aurora in forty lines of bash, discovering that the mechanism behind docker compose and docker buildx is simply an executable on the PATH that answers docker-cli-plugin-metadata.

From the tour through the categories you take away a selection, not a catalog: lazydocker and ctop for seeing the fleet without typing, Portainer only when there are several people and several hosts, Dockge when what you manage is Compose stacks; dive confirming the 98 % efficiency you earned in 05-04 and acting as a CI gate with --ci --lowestEfficiency, container-diff for auditing what changed between versions and slim with its serious warning; hadolint pointing out, in six messages, the entire syllabus of modules 2 and 5 — including the DL3025 that breaks graceful shutdown without any test noticing — Trivy as the gate, Scout as the early check and Falco watching syscalls when the vulnerability was not in any database; the Docker Hub mirror with registry:2 that almost nobody sets up and that almost always pays off; Testcontainers bringing up PostgreSQL and Redis from the test itself to check that cache-aside really does return source: cache; Buildpacks with its measured trade-off — 412 MB against your 104 — and the table of when not writing a Dockerfile pays off; scheduled cleanup with until and without --volumes; and netshoot with --network container: entering the network namespace of aurora-api without touching the image.

And you take away the adoption criteria, which are worth more than any list of tools: active maintenance, a compatible license, verifiable provenance, criticality in the pipeline, real understanding by the team, exit cost and added attack surface. With the two golden rules: the closer to the pipeline, the more demanding you should be, and validate with your security lead — and with whoever handles licensing — any tool that enters the build chain or asks for the daemon socket. If you had to keep three for the rest of your career: hadolint, Trivy and Testcontainers.

In the next lesson we take a fundamental step back: Podman, containerd and the OCI standard. You are going to discover why the images you have been building all course long are not "Docker images", what exactly the Open Container Initiative standardizes, and how ghcr.io/auroralibros/aurora-api:2.0.0 runs identically on Podman, on containerd or on a Kubernetes node without changing a single byte.

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