Every build in this course has been done by BuildKit without you noticing. In this lesson you take control: cache mounts that spare you from reinstalling npm on every build, secrets that leave no trace in any layer, a cache shared between your machine and CI, images that work on amd64 and arm64 at the same time, and a single command to build the whole Aurora Libros platform.

Contents

  1. What BuildKit is and how it differs from the classic builder
  2. The graph of a multi-stage build
  3. Enabling it, verifying it and reading its output
  4. Buildx and builders
  5. Mounts in RUN: type=cache
  6. type=bind and type=tmpfs
  7. type=secret: credentials that leave no trace
  8. type=ssh: cloning private repositories
  9. Shared remote cache
  10. Multi-architecture: QEMU and manifest lists
  11. TARGETPLATFORM and cross-compilation
  12. Outputs with --output, --load and --push
  13. docker buildx bake

  1. What BuildKit is and how it differs from the classic builder

The classic builder ran the Dockerfile line by line, in strict order, creating one intermediate container per instruction. BuildKit replaces that with an engine that first analyzes the whole file and builds a dependency graph.

Aspect Classic builder BuildKit
Execution Sequential, instruction by instruction Dependency graph: only what is needed
Independent stages One after another In parallel
Cache Per layer, in a linear chain By content; survives reordering
Context Sent whole at the start Transferred on demand
Secrets Impossible without leaking them --mount=type=secret, no trace
Output A flat log at the end Live progress, per step and with timings
External cache No --cache-from / --cache-to with registries
Multi-architecture One docker build per platform One command, several platforms

The most useful difference day to day: if a stage contributes nothing to the final image, BuildKit does not even run it. That is why the tests stage from lesson 05-04 does not run when you build production.

  1. The graph of a multi-stage build

flowchart LR
  CTX["Context<br/>(on demand)"] --> D1["dependencies:<br/>apk add build-base"]
  CTX --> D2["development:<br/>full npm ci"]
  D1 --> D3["dependencies:<br/>npm ci --omit=dev"]
  D2 --> T["tests:<br/>npm test"]
  D3 --> P["production:<br/>COPY --from=dependencies"]
  CTX --> P
  P --> IMG["Final image"]
  T -.->|"only with --target tests"| X["does not enter the graph<br/>of the final image"]

dependencies and development do not depend on each other: BuildKit runs them at the same time. And tests hangs off development, but nothing in the final image depends on tests, so it is pruned from the graph.

  1. Enabling it, verifying it and reading its output

Since Docker 23, BuildKit is the default builder on Linux, macOS and Windows.

docker buildx version
docker build --no-cache -t aurora-api:test ./api 2>&1 | head -3
github.com/docker/buildx v0.19.3
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 1.42kB done

The numbered #n lines are BuildKit's signature; the old builder showed Step 1/12 : FROM .... If for some specific reason you need to go back, DOCKER_BUILDKIT=0 docker build ..., but treat that as a diagnostic step, not an option.

To debug a build, the progress mode changes everything:

docker build --progress=plain --no-cache -t aurora-api:dep ./api 2>&1 | grep -A3 'npm ci'
#12 [dependencies 4/4] RUN npm ci --omit=dev && npm cache clean --force
#12 3.412 added 64 packages in 3s
#12 4.108 npm warn using --force Recommended protections disabled.
#12 DONE 4.3s

--progress=plain prints each command's full output with relative timestamps, instead of the interactive view that collapses lines. It is the first thing to turn on when a build fails and you cannot see why.

  1. Buildx and builders

Buildx is the modern build client. A builder is the BuildKit instance that does the work, and not all of them have the same capabilities.

docker buildx ls
NAME/NODE       DRIVER/ENDPOINT   STATUS   PLATFORMS
default *       docker            running  linux/amd64, linux/386
  default       default           running
Driver Where it runs Multi-architecture Remote cache Outputs
docker (default) Inside the daemon No inline only Local image only
docker-container In a container of its own Yes All All (--output)
kubernetes Pods in a cluster Yes All All
remote An external BuildKit instance Yes All All

Everything that follows requires docker-container:

docker buildx create --name aurora --driver docker-container --use --bootstrap
docker buildx inspect aurora | grep -E 'Name|Status|Platforms'
Name:      aurora
Status:    running
Platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/386

That builder is an ordinary container (buildx_buildkit_aurora0) with its own cache, independent of the daemon's. You manage it with docker buildx use/stop/rm, and clean it with docker buildx prune --filter until=168h.

  1. Mounts in RUN: type=cache

A cache mount is a directory that persists between builds and is not part of any layer. It is the solution to the most common waste of all: reinstalling entire dependency trees because one line of package.json changed.

# syntax=docker/dockerfile:1
FROM node:22-alpine AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
    npm ci --omit=dev
docker buildx build --target dependencies -t aurora-api:deps ./api    # first time
# change a version in package.json and repeat
time docker buildx build --target dependencies -t aurora-api:deps ./api
First build (cold cache):   42.7s
After changing package.json, without a cache mount: 39.1s
After changing package.json, with a cache mount:     6.8s

From 39 seconds to 7. The npm cache has not been downloaded again: npm found the packages in /root/.npm and only resolved the tree. And because the mount is not incorporated into the layer, the final image does not grow by a single byte.

Parameter Values Meaning
target A path Where it is mounted inside the RUN
id Text Cache identifier; share it between Dockerfiles
sharing shared (default), locked, private What happens if two builds use it at once
mode 0755 Directory permissions
uid/gid Numbers Owner, useful if the RUN is not root

sharing=locked is the right value for package managers that do not tolerate concurrent writes (npm, apt, pip). The usual cache directories: /root/.npm (npm), /var/cache/apt and /var/lib/apt/lists (apt), /root/.cache/pip (pip), /go/pkg/mod (Go), /root/.m2 (Maven).

Warning. A cache mount is local to the builder. It does not travel with the image or between machines; to share it between your laptop and CI you use the remote cache from section 9.

  1. type=bind and type=tmpfs

type=bind mounts files from the context (or from another stage) without copying them into a layer, and type=tmpfs gives you an in-memory directory for temporary work:

RUN --mount=type=bind,source=package-lock.json,target=/tmp/lock.json \
    node -e "console.log(require('/tmp/lock.json').lockfileVersion)"

# From another stage, without dragging its layers along
RUN --mount=type=bind,from=dependencies,source=/app/node_modules,target=/deps du -sh /deps

RUN --mount=type=tmpfs,target=/tmp/work \
    tar xzf /source.tar.gz -C /tmp/work && cp /tmp/work/binary /usr/local/bin/

They are for reading or verifying something during the build without it becoming part of the image. Unpacking into tmpfs is fast and guarantees that the intermediate files never reach any layer.

  1. type=secret: credentials that leave no trace

This is the section that solves the problem demonstrated in lessons 05-03 and 05-04. Let's compare the two ways of passing a private npm token.

# ❌ UNSAFE: the ARG stays in the metadata forever
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
    npm ci && rm .npmrc
# ✅ CORRECT: the secret is mounted, used and gone
RUN --mount=type=secret,id=npm_token \
    --mount=type=cache,target=/root/.npm,sharing=locked \
    NPM_TOKEN="$(cat /run/secrets/npm_token)" \
    npm ci --omit=dev
echo "npm_tok_a91f3c" > /tmp/npm_token.txt
docker buildx build --secret id=npm_token,src=/tmp/npm_token.txt \
  --load -t aurora-api:secure ./api

# Does the token show up anywhere?
docker history --no-trunc aurora-api:secure | grep -c 'npm_tok_a91f3c'
docker image inspect aurora-api:secure --format '{{json .Config.Env}}' | grep -c 'npm_tok'
docker save aurora-api:secure | strings | grep -c 'npm_tok_a91f3c'
0
0
0

Zero matches across all three checks. Compare that with the same experiment using ARG, where docker history handed back the token in the clear. The mechanism: BuildKit mounts the file at /run/secrets/<id> as a tmpfs that exists only for the duration of that RUN; there is no layer, no metadata, no trace.

The secret can also come from an environment variable, which fits CI secret managers better:

export NPM_TOKEN=npm_tok_a91f3c
docker buildx build --secret id=npm_token,env=NPM_TOKEN --load -t aurora-api:secure ./api

  1. type=ssh: cloning private repositories

When a dependency lives in a private Git repository, the instinct is to copy a private key into the build. Never do that: type=ssh forwards your SSH agent without the key ever entering the build.

RUN --mount=type=ssh \
    mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \
    npm ci --omit=dev
ssh-add -l >/dev/null || ssh-add ~/.ssh/id_ed25519
docker buildx build --ssh default --load -t aurora-api:private ./api

The key never leaves your agent: BuildKit exposes a temporary socket inside the RUN and the signing operations happen on your machine. When the instruction ends, the socket disappears.

  1. Shared remote cache

A local cache only serves whoever generated it. In CI, every job starts on a clean machine and rebuilds everything from scratch. A remote cache solves that: it is published to a registry and any builder can reuse it.

docker buildx build \
  --cache-to   type=registry,ref=auroralibros/aurora-api:buildcache,mode=max \
  --cache-from type=registry,ref=auroralibros/aurora-api:buildcache \
  -t auroralibros/aurora-api:1.3.0 --push ./api
Backend Where it lives Advantage Drawback
registry In your registry, as one more tag Shared across machines and with CI Needs write permission on the registry
inline Inside the image itself Zero configuration; works with the docker driver Caches only the last stage (mode=min)
gha GitHub Actions cache Built in, no extra registry Limited to GitHub and a 10 GB quota
local A directory on disk Fast and network-free Not shared between machines
s3 / azblob Object storage Scalable and cheap Credential configuration

mode=max stores the cache for every stage, intermediate ones included; mode=min (the default) stores only the final image's layers. For multi-stage builds, mode=max is what really saves time, at the cost of more space in the registry.

# Clean machine: reuses the published cache
docker buildx build --cache-from type=registry,ref=auroralibros/aurora-api:buildcache \
  -t auroralibros/aurora-api:1.3.0 --load ./api 2>&1 | grep -c CACHED
9

Nine steps resolved from the cache on a machine that had never built this image. That is the piece that will make the pipeline in lesson 06-02 take seconds instead of minutes.

  1. Multi-architecture: QEMU and manifest lists

Apple Silicon laptops are arm64, and so is a good share of cloud servers (Graviton, Ampere), while most CI is still amd64. An image built only for amd64 either fails or runs painfully slowly under emulation on arm64.

docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx inspect aurora --bootstrap | grep Platforms
Platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/riscv64, linux/386

binfmt_misc is the kernel feature that associates an interpreter with binaries from another architecture; that privileged container registers the corresponding QEMU emulators (and yes, the --privileged there is justified: it registers handlers in the kernel, and it is a one-off host configuration step).

docker buildx build --platform linux/amd64,linux/arm64 \
  -t auroralibros/aurora-api:1.3.0 --push ./api
docker buildx imagetools inspect auroralibros/aurora-api:1.3.0
Name:      docker.io/auroralibros/aurora-api:1.3.0
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:c41f8a2b...

Manifests:
  Name:      auroralibros/aurora-api:1.3.0@sha256:9e2a17f4...
  Platform:  linux/amd64
  Name:      auroralibros/aurora-api:1.3.0@sha256:5b70c3d8...
  Platform:  linux/arm64

A manifest list (or image index) is an index that points to one image per platform. When somebody runs docker pull auroralibros/aurora-api:1.3.0, the client reports its architecture and the registry hands over the right manifest: the same tag works on the team's Mac and on the Graviton server, with no suffixes and no branches.

An important note: --platform with several architectures forces --push, because the daemon's local store cannot hold a multi-platform index. With --load you can only load one platform at a time.

  1. TARGETPLATFORM and cross-compilation

Emulating an architecture with QEMU works, but it is slow —three to ten times slower. The professional pattern is to compile natively and use emulation only for the final stage. BuildKit exposes automatic variables for exactly that:

Variable What it holds
BUILDPLATFORM The platform of the machine doing the building (e.g. linux/amd64)
TARGETPLATFORM The target platform (linux/arm64)
TARGETOS, TARGETARCH, TARGETVARIANT Its components separately
# The build stage ALWAYS runs on the native architecture: fast
FROM --platform=$BUILDPLATFORM node:22-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY src/ ./src/

# The final stage does belong to the target architecture
FROM node:22-alpine AS production
WORKDIR /app
COPY --from=builder --chown=node:node /app .
USER node
CMD ["node", "src/server.js"]
docker buildx build --platform linux/amd64,linux/arm64 --progress=plain \
  -t auroralibros/aurora-api:1.3.0 --push ./api 2>&1 | grep Building
#8 0.312 Building on linux/amd64 for linux/amd64
#9 0.298 Building on linux/amd64 for linux/arm64

Both builds happen on amd64: the second one compiles for arm64 without emulating. With compiled languages (Go, Rust) the gain is enormous, because GOARCH=$TARGETARCH is all it takes. With Node.js the benefit is smaller —the code is interpreted— but the pattern still avoids emulating npm ci, which is the slow part.

  1. Outputs with --output, --load and --push

With the docker-container driver, the result does not automatically go to your daemon: you have to say where you want it.

docker buildx build --load -t aurora-api:local ./api                 # to the local daemon
docker buildx build --push -t auroralibros/aurora-api:1.3.0 ./api    # to the registry
docker buildx build --output type=local,dest=./output ./api          # loose files
docker buildx build --output type=oci,dest=aurora-api-oci.tar ./api  # OCI format
Output What it produces What for
--load (= type=docker) An image in your daemon Local development and testing
--push (= type=image,push=true) An image in the registry Publishing and multi-architecture
type=local The last stage's files Extracting compiled artifacts
type=oci / type=tar An OCI archive or a tar of the filesystem Signing, scanning, daemonless environments
type=cacheonly Nothing; it only warms the cache Pre-warming CI

type=local has an elegant use: treating Docker as a reproducible build system and keeping only the result, with no image in between. And type=tar,dest=aurora-api.tar produces a tar of the filesystem for transport or analysis.

  1. docker buildx bake

Building Aurora Libros takes several long commands that are easy to mistype. Bake declares them in a file:

# docker-bake.hcl — Aurora Libros S.L.
variable "VERSION"   { default = "1.3.0" }
variable "REGISTRY"  { default = "auroralibros" }
variable "PLATFORMS" { default = "linux/amd64,linux/arm64" }

group "default" {
  targets = ["api", "web"]
}

target "common" {
  platforms  = split(",", PLATFORMS)
  cache-from = ["type=registry,ref=${REGISTRY}/buildcache"]
  cache-to   = ["type=registry,ref=${REGISTRY}/buildcache,mode=max"]
  labels = {
    "org.opencontainers.image.version" = VERSION
    "org.opencontainers.image.vendor"  = "Aurora Libros S.L."
  }
}

target "api" {
  inherits   = ["common"]
  context    = "./api"
  target     = "production"
  tags       = ["${REGISTRY}/aurora-api:${VERSION}", "${REGISTRY}/aurora-api:latest"]
  args       = { VERSION = VERSION }
}

target "web" {
  inherits = ["common"]
  context  = "./web"
  tags     = ["${REGISTRY}/aurora-web:${VERSION}"]
}

# Matrix: the same image with several Node versions
target "api-matrix" {
  inherits = ["common"]
  context  = "./api"
  name     = "api-node${node}"
  matrix   = { node = ["20", "22", "23"] }
  args     = { NODE_VERSION = node }
  tags     = ["${REGISTRY}/aurora-api:${VERSION}-node${node}"]
}
docker buildx bake --print                     # see the plan without building
docker buildx bake                             # the "default" group: api and web
docker buildx bake api --set api.tags=aurora-api:local --load
VERSION=1.4.0 docker buildx bake --push
docker buildx bake api-matrix
[+] Building 51.2s (34/34) FINISHED
 => [api] exporting to image
 => [web] exporting to image

Three things make bake worth it: inherits saves you from repeating the shared configuration, the targets are built in parallel (here, api and web at the same time), and --print shows you the resolved plan before anything runs. Bake can also read a compose.yaml directly (docker buildx bake -f compose.yaml), taking advantage of the build sections you have already written.

Common Mistakes and Tips

Using the docker driver and expecting multi-architecture or remote cache. Create a docker-container builder with docker buildx create --use.

Forgetting --load or --push with docker-container. The build finishes successfully and the image is nowhere to be found.

Trying --load with several platforms. The local store does not hold multi-platform indexes. Use --push or load a single one.

Passing secrets with --build-arg. They stay in docker history. --mount=type=secret, always.

Believing the cache mount travels with the image. It is local to the builder. Between machines, use a remote cache.

Using mode=min for the remote cache of a multi-stage build. It only caches the final image; the expensive stages are rebuilt. Use mode=max.

Letting the builder's cache grow without limit. Run docker buildx prune --filter until=168h periodically, or use --keep-storage.

Tip: when a build fails incomprehensibly, the order is --progress=plain to see the full output, --no-cache to rule out a poisoned cache, and --target <stage> to isolate the exact point of failure. Those three resolve practically every case.

Exercises

Exercise 1. Create a docker-container builder, add a type=cache mount to aurora-api's npm install and measure the difference: build once, change package.json and rebuild, with and without the mount. Explain why the final image does not grow.

Exercise 2. Prove that --mount=type=secret leaves no trace: build the same image passing a token with --build-arg and with --secret, and search for the token in the history, in the environment and in each one's exported tarball.

Exercise 3. Publish auroralibros/aurora-api:1.3.0 for linux/amd64 and linux/arm64, inspect the resulting manifest list, and explain exactly what happens when an arm64 server and an amd64 laptop pull the same tag.

Solutions

Solution 1.

docker buildx create --name aurora --driver docker-container --use --bootstrap
docker buildx build --no-cache --target dependencies --load -t a:v1 ./api 2>&1 | tail -1
sed -i 's/"express": "4.19.3"/"express": "4.19.2"/' api/package.json

# Without a cache mount (Dockerfile.nocache)
time docker buildx build -f api/Dockerfile.nocache --target dependencies --load -t a:v2 ./api
# With a cache mount
time docker buildx build --target dependencies --load -t a:v3 ./api
docker image ls a --format "{{.Tag}}\t{{.Size}}"
real    0m39.108s      <- without a cache mount
real    0m6.821s       <- with a cache mount
v2   198MB
v3   198MB

A change in package.json invalidates the COPY package.json layer and everything after it, so npm ci runs again in both cases. The difference is inside that run: without the mount, npm downloads the 64 packages all over again; with it, npm finds /root/.npm already populated from the previous build and only resolves the tree and links. The network drops out of the critical path and 6.8 seconds remain.

And the image weighs exactly the same (198 MB) because a cache mount takes no part in the layer: BuildKit mounts it before running the command and unmounts it afterwards, so by the time the layer's filesystem is consolidated, /root/.npm is no longer there. That is the essential difference from COPY: one leaves a mark, the other does not.

Solution 2.

TOKEN="npm_tok_a91f3c"
echo "$TOKEN" > /tmp/tok.txt

# A) With ARG
docker buildx build -f api/Dockerfile.arg --build-arg NPM_TOKEN="$TOKEN" --load -t leak:arg ./api
# B) With secret
docker buildx build --secret id=npm_token,src=/tmp/tok.txt --load -t secure:sec ./api

for img in leak:arg secure:sec; do
  h=$(docker history --no-trunc "$img" | grep -c "$TOKEN")
  e=$(docker image inspect "$img" --format '{{json .Config}}' | grep -c "$TOKEN")
  t=$(docker save "$img" | strings | grep -c "$TOKEN")
  echo "$img -> history:$h config:$e tarball:$t"
done
leak:arg   -> history:1 config:1 tarball:2
secure:sec -> history:0 config:0 tarball:0

The image built with ARG leaks the token through three independent channels: the build history (which keeps the RUN line with the value substituted in), the image configuration (where the ARG is recorded as metadata) and the layer content (where the .npmrc that was written and deleted is still present under a whiteout). Missing just one of them is enough for the secret to be published.

With --secret all three come back zero. BuildKit mounts the file as a tmpfs at /run/secrets/npm_token only for as long as that RUN lasts; the mount is not part of the filesystem that gets consolidated into the layer, and the value shows up in no metadata because it was never a build argument. It is, literally, the only correct way to use credentials during a build.

Solution 3.

docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null
docker buildx build --platform linux/amd64,linux/arm64 \
  -t auroralibros/aurora-api:1.3.0 --push ./api
docker buildx imagetools inspect auroralibros/aurora-api:1.3.0 \
  --format '{{range .Manifest.Manifests}}{{.Platform.OS}}/{{.Platform.Architecture}} {{.Digest}}{{"\n"}}{{end}}'
docker buildx imagetools inspect auroralibros/aurora-api:1.3.0 --raw | head -3
linux/amd64 sha256:9e2a17f4c8b3...
linux/arm64 sha256:5b70c3d8a1e6...
{
  "mediaType": "application/vnd.oci.image.index.v1+json",

The 1.3.0 tag does not point at an image: it points at an index (image.index.v1+json) containing two manifests, each with its own digest and its declared platform.

When the arm64 server runs docker pull auroralibros/aurora-api:1.3.0, its client sends the types it understands in the Accept header and reports its platform; the registry returns the index, the client looks for the linux/arm64 entry, and downloads only the sha256:5b70c3d8... manifest and its layers. The amd64 laptop, with the same command and the same tag, gets sha256:9e2a17f4.... Neither downloads a single byte belonging to the other architecture.

The three practical consequences: compose.prod.yaml can carry one image reference valid for the whole fleet; if you pin by digest (lesson 05-03) you must pin the index's digest, not that of a specific platform, or you will break portability; and if one day somebody builds without --platform on their Mac and pushes with that same tag, they will replace the index with a lone arm64 image and the amd64 servers will fail with exec format error. That is why multi-architecture builds belong in the pipeline and not on anybody's laptop.

Conclusion

BuildKit has stopped being a black box. You know that it does not run the Dockerfile line by line but builds a dependency graph, parallelizes independent stages, transfers the context on demand and prunes whatever contributes nothing to the final image —hence the tests stage not running when you build production. You know how to verify it, how to read its output and, when something fails, how to turn on --progress=plain to see the real execution. And you know Buildx and its builders: the docker driver for the basics and docker-container for everything else, with its own cache and its emulated platforms.

You have mastered the four RUN mounts. type=cache turned a 39-second dependency reinstall into 7 seconds without the image growing by a byte, because the mount is not part of the layer. type=bind reads files without copying them, type=tmpfs provides temporary space in memory, and type=secret finally closes the problem you have been carrying since lesson 02-04: a token passed with --build-arg showed up in the history, in the configuration and in the tarball; passed with --secret, zero matches in all three. type=ssh completes the picture by forwarding your agent so you can clone private repositories without any key entering the build.

And you know how to work beyond your own machine: remote cache with --cache-from/--cache-to and its backends —registry, gha, local, inline— with mode=max so the intermediate stages get reused too, which will reduce the pipeline in lesson 06-02 to seconds; multi-architecture builds with QEMU and binfmt_misc, manifest lists that make a single tag serve both the team's Mac and the Graviton server, and the --platform=$BUILDPLATFORM pattern with TARGETARCH to compile natively instead of emulating. You finish with the --output outputs and with docker buildx bake, which reduces the entire Aurora Libros build to one docker buildx bake --push with inheritance, variables and matrices.

In lesson 05-06 you stop building and start observing. You will see the logging of a running platform: logging drivers with their comparison table, mandatory rotation and exactly what happens if you do not configure it, structured JSON logs with a request identifier, and an aggregation stack with Loki, Promtail and Grafana brought up as Aurora Libros' observability profile. Then metrics: docker stats and its limits, the daemon's Prometheus endpoint, cAdvisor and node-exporter, the metrics that really get watched, the alerts that deserve to wake somebody up, and the rich health endpoint pattern that turns /health into the source of truth for the whole platform.

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