You have spent six modules using containers. This lesson explains why they work. The thesis is uncomfortable and liberating at the same time: a container does not exist. There is no "container" entity in the Linux kernel. There are ordinary processes with three mechanisms applied on top, and every one of them can be handled by hand.
Contents
- The thesis: three mechanisms and no object
- The seven namespaces
- The same process, two different PIDs
/proc/<pid>/ns/,lsnsand comparing containers- Getting in by hand with
nsenter - Creating a namespace from scratch with
unshare - The user namespace and UID remapping
- Cgroups v2: the hierarchy
aurora-db's limits, read from the kernel- The OOM killer through
memory.events - OverlayFS: the real mount
- Copy-on-write in the
upperdir - Capabilities and seccomp at kernel level
- What
runcactually does - Docker Desktop: all of this happens in a VM
- The thesis: three mechanisms and no object
When you run docker run, no magical structure is created. An ordinary process is launched and three things are applied to it:
| Mechanism | Answers | What you see in Docker |
|---|---|---|
| Namespaces | What does the process see? | Isolation of processes, network, files and users |
| Cgroups | How much can it consume? | --memory, --cpus, --pids-limit |
| Layered filesystem | What filesystem does it have? | Images, layers, copy-on-write |
flowchart TB
P["An ordinary Linux process<br/>(node src/server.js)"]
NS["NAMESPACES<br/>pid · net · mnt · uts<br/>ipc · user · cgroup"]
CG["CGROUPS v2<br/>memory.max · cpu.max<br/>pids.max · io.max"]
FS["OVERLAYFS<br/>lowerdir + upperdir<br/>= merged"]
P --> NS --> C(("What we call<br/>a «container»"))
P --> CG --> C
P --> FS --> C
K["The host kernel: ONE, shared by everybody"] --- C
Everything else —images, registries, Compose, healthchecks— is machinery built around those three kernel primitives.
- The seven namespaces
A namespace is a partial view of a global system resource. The processes in a namespace see their version of the resource and not anybody else's.
| Namespace | Isolates | Docker option |
|---|---|---|
pid |
The process tree | --pid=host disables it; --pid=container:X shares it |
net |
Interfaces, routes, iptables, ports |
--network (lesson 05-01) |
mnt |
Mount points | The container's root and every -v |
uts |
Host name and domain name | --hostname |
ipc |
Shared memory, message queues | --ipc=host, --shm-size |
user |
UID and GID mapping | --userns-remap, rootless mode (lesson 05-03) |
cgroup |
The view of the cgroup hierarchy | --cgroupns |
An eighth one, time (the monotonic clock), has existed in the kernel since 5.6 but Docker does not use it yet.
- The same process, two different PIDs
This is the demonstration that convinces people fastest.
docker compose exec aurora-api ps -eo pid,comm
pid=$(docker inspect aurora-libros-aurora-api-1 --format '{{.State.Pid}}')
ps -o pid,ppid,user,comm -p "$pid"PID COMMAND <- from INSIDE the container
1 node
28 ps
PID PPID USER COMMAND <- from the HOST
48122 4791 1000 nodeIt is the same process. Inside it is PID 1 —hence everything you learned in lesson 03-02 about signals and PID 1—; on the host it is 48122, a child of the containerd shim and running as user 1000. There is no virtual machine: there is a node in your process list that happens to see a different tree from yours.
NSpid says it all: 48122 in the host's namespace, 1 in its own. One process with two simultaneous identities.
/proc/<pid>/ns/, lsns and comparing containers
/proc/<pid>/ns/, lsns and comparing containersEach namespace is a special file whose inode identifies it:
cgroup -> cgroup:[4026532897]
ipc -> ipc:[4026532835]
mnt -> mnt:[4026532833]
net -> net:[4026532838]
pid -> pid:[4026532836]
user -> user:[4026531837]
uts -> uts:[4026532834]Comparing two containers is comparing those numbers:
pdb=$(docker inspect aurora-libros-aurora-db-1 --format '{{.State.Pid}}')
for ns in net pid mnt user; do
a=$(sudo readlink /proc/$pid/ns/$ns); b=$(sudo readlink /proc/$pdb/ns/$ns)
[ "$a" = "$b" ] && echo "$ns: SHARED" || echo "$ns: different"
done
echo "host user ns: $(sudo readlink /proc/1/ns/user)"The most revealing result is the last one: aurora-api and aurora-db share the user namespace, and it happens to be the host's. Without --userns-remap or rootless mode, UID 0 inside is literally UID 0 on the host. There it is, measured: the claim from lesson 05-03 about why root in a container is dangerous.
sudo lsns -t net -o NS,PID,COMMAND | head -4
# 4026532838 48122 node src/server.js
# 4026532901 48310 postgres
# 4026532955 48502 nginx: master process
- Getting in by hand with
nsenter
nsenterdocker exec is not magic: it enters the process's namespaces and launches a command. nsenter does the same thing, and without going through Docker.
a91f3c8d2e10
app bin dev etc home lib proc root sys tmp usr var
eth0 UP 172.21.0.5/16
PID COMMAND
1 nodeThe flags are exactly the namespaces: -m (mnt), -u (uts), -i (ipc), -n (net), -p (pid). You can enter only some of them, and that is what makes the technique useful: -n on its own gives you the container's network with the host's tools —precisely what you were doing in lesson 05-01— and it works even with distroless images that do not have an sh inside.
- Creating a namespace from scratch with
unshare
unshareTo see that there is no magic, let's build one by hand.
sudo unshare --pid --fork --mount-proc --uts --net --mount \
sh -c 'hostname aurora-manual; ps -eo pid,comm; ip -brief addr; hostname'No Docker, no images and no daemon: a shell that believes it is PID 1, with its own host name and an empty network stack. That is a container's isolation. What Docker adds on top is everything else: the image's filesystem, the network configuration with its veth and its bridge, the cgroups, the capabilities, seccomp and an API to manage it all.
- The user namespace and UID remapping
The user namespace lets the same UID mean different things inside and outside. It is the basis of rootless mode (lesson 05-03).
unshare --user --map-root-user sh -c 'id -u; cat /proc/self/uid_map; cat /etc/shadow' 2>&1 | tail -3
id -uInside you are root (id -u = 0) without having used sudo; the map says that UID 0 inside corresponds to 1000 outside, with length 1. Outside you are still 1000.
Root inside, but the kernel checks the file's permissions against the real host UID. That is the exact mechanism that makes the escalation from lesson 05-03 stop working in rootless mode: it is not a check Docker adds, it is UID arithmetic in the kernel.
- Cgroups v2: the hierarchy
Control groups limit and account for resources. Version 2, the unified one, has been the standard since 2022 and is exposed as a directory tree under /sys/fs/cgroup.
docker info --format 'Cgroup Driver: {{.CgroupDriver}} | Version: {{.CgroupVersion}}'
ls /sys/fs/cgroup/system.slice/ | grep docker | head -1| Controller | Key files | What it governs |
|---|---|---|
memory |
memory.max, memory.current, memory.high, memory.events |
RAM and the OOM killer trigger |
cpu |
cpu.max, cpu.stat, cpu.weight |
CPU quota and throttling |
io |
io.max, io.stat |
Disk bandwidth and IOPS |
pids |
pids.max, pids.current |
Number of processes (fork-bomb defense) |
The essential difference from v1: in v2 a process belongs to a single cgroup and every controller acts on that one, instead of a separate hierarchy per resource. That simplifies the reasoning enormously.
aurora-db's limits, read from the kernel
aurora-db's limits, read from the kernelNow the check that closes the circle with lesson 03-07 and with Compose: what you declared in YAML, read straight from the kernel.
id=$(docker inspect aurora-libros-aurora-db-1 --format '{{.Id}}')
cg=/sys/fs/cgroup/system.slice/docker-$id.scope
for f in memory.max memory.current memory.high pids.max pids.current cpu.max; do
printf '%-16s %s\n' "$f" "$(cat $cg/$f)"
donememory.max 2147483648
memory.current 432791552
memory.high max
pids.max 200
pids.current 14
cpu.max 200000 100000Line by line: memory.max is 2,147,483,648 bytes = 2 GiB, the limits: { memory: 2G } from compose.prod.yaml; memory.current is what docker stats shows; pids.max is pids_limit: 200; and cpu.max with 200000 100000 means 200 ms of CPU for every 100 ms period, that is, cpus: "2.0".
docker stats --no-stream --format '{{.Name}} {{.MemUsage}}' aurora-libros-aurora-db-1
# aurora-libros-aurora-db-1 412.7MiB / 2GiBAn exact match. docker stats calculates nothing: it reads these files. And cpu.max finally explains the semantics of --cpus: it is not a number of assigned cores, it is a time quota per period; with 2.0, the container can consume 200 ms of CPU every 100 ms, spread across however many cores there are.
- The OOM killer through
memory.events
memory.eventsdocker run -d --name victim --memory 64m --memory-swap 64m alpine:3 \
sh -c 'x=""; while true; do x="$x$(head -c 1048576 /dev/zero | tr "\0" "a")"; done'
sleep 6
id=$(docker inspect victim --format '{{.Id}}')
cat /sys/fs/cgroup/system.slice/docker-$id.scope/memory.events 2>/dev/null || echo "(cgroup already removed)"
docker inspect victim --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'
docker rm -f victimThe counters tell the whole story: max 412 is the number of times the process hit its limit and the kernel had to reclaim memory; oom 1 says there came a point where it could reclaim no more; oom_kill 1 is the execution. The outcome, exit code 137, which you have known since lesson 03-02, is simply 128 + 9: terminated by SIGKILL.
A high max with oom_kill 0 is an extremely valuable signal that almost nobody watches: the container has not died, but it is fighting its limit and paying for it in latency. It is exactly what the MemoryNearLimit alert from lesson 05-06 catches before it is too late.
- OverlayFS: the real mount
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/QW3F:/var/lib/docker/overlay2/l/K7RT:/var/lib/docker/overlay2/l/P2MN,upperdir=/var/lib/docker/overlay2/9be21c/diff,workdir=/var/lib/docker/overlay2/9be21c/work)The container's root / is an overlay mount, with the four directories from lesson 05-02. And the lowerdirs correspond one to one with the image's layers:
docker image inspect auroralibros/aurora-api:1.3.0 --format '{{len .RootFS.Layers}} layers'
docker compose exec aurora-api sh -c 'mount|grep " / "' | grep -o 'lowerdir=[^,]*' | tr ':' '\n' | wc -l
# 3 layers
# 3Three layers in the image manifest, three lowerdirs in the mount. The correspondence is literal: every image layer is a directory on the host's disk, and OverlayFS stacks them in order.
- Copy-on-write in the
upperdir
upperdirup=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo "note" > /tmp/note.txt' # 1) create
docker compose exec aurora-api sh -c 'echo "// touched" >> /app/package.json' # 2) modify
docker compose exec aurora-api sh -c 'rm -f /app/src/util.js' # 3) delete
sudo ls -l "$up/tmp/note.txt" "$up/app/src/util.js"
docker diff aurora-libros-aurora-api-1 | head -3-rw-r--r-- 1 node node 5 Aug 5 11:02 .../diff/tmp/note.txt
c--------- 1 root root 0, 0 Aug 5 11:02 .../diff/app/src/util.js
A /tmp/note.txt
C /app/package.json
D /app/src/util.jsThe three operations, with their three distinct effects in the upperdir:
- A new file: it appears as it is. That is the
Aindocker diff. - A modified file: the kernel performs a copy-up —copying the whole original from the
lowerdir— and writes on top of it. That is theC, and it is the expensive operation you measured in lesson 05-02. - A deleted file: a character device 0/0 appears, the famous whiteout. It deletes nothing: it hides the file from the lower layer, which still takes up space. That is the
D, and it is the reason why deleting in a later layer does not slim down an image (lesson 05-04).
There it is, in three lines of ls: the physical explanation of lessons 01-05, 05-02 and 05-04.
- Capabilities and seccomp at kernel level
The capabilities from lesson 05-03 are a bitmask in the process descriptor.
sudo grep -E '^(CapEff|CapBnd|Seccomp|NoNewPrivs)' /proc/$pid/status
capsh --decode=$(sudo awk '/^CapEff/{print $2}' /proc/$pid/status)CapEff at zero: aurora-api runs with no effective capabilities at all, exactly as you asked for with cap_drop: [ALL]. Compare it with a default container:
docker run -d --name defaults alpine:3 sleep 60
p2=$(docker inspect defaults --format '{{.State.Pid}}')
capsh --decode=$(sudo awk '/^CapEff/{print $2}' /proc/$p2/status) | tr ',' '\n' | head -4
docker rm -f defaultsDocker grants fourteen capabilities by default. The difference between that list and a CapEff of zero is the whole surface you have removed, and now you see it for what it is: bits in /proc.
Seccomp: 2 means BPF filter mode is active (0 would be disabled); NoNewPrivs: 1 is the no-new-privileges:true from compose.prod.yaml. Everything you configured in YAML ends up as two numbers in a file under /proc.
- What
runc actually does
runc actually doesThe chain from lesson 01-03, now seen from the inside: dockerd → containerd → shim → runc → process.
runc is a small binary that receives a directory with two things: the already-mounted filesystem and a config.json carrying the container's OCI specification.
{
"ociVersion": "1.2.0",
"process": {
"user": { "uid": 1000, "gid": 1000 },
"args": ["node", "src/server.js"],
"capabilities": { "bounding": [], "effective": [], "permitted": [] },
"noNewPrivileges": true
},
"root": { "path": "rootfs", "readonly": true },
"linux": {
"namespaces": [ { "type": "pid" }, { "type": "network" }, { "type": "ipc" },
{ "type": "uts" }, { "type": "mount" }, { "type": "cgroup" } ],
"resources": {
"memory": { "limit": 536870912 },
"cpu": { "quota": 200000, "period": 100000 },
"pids": { "limit": 200 }
},
"seccomp": { "defaultAction": "SCMP_ACT_ERRNO" }
}
}You recognize every line: they are your docker run and Compose options translated into the standard. runc's steps are: create the namespaces (clone with the CLONE_NEW* flags), write the cgroups, pivot the root to rootfs (pivot_root), apply capabilities and seccomp, and finally execve the command. That is where its job ends: runc exits, and the process is left orphaned under the shim.
That is the role of the shim (containerd-shim-runc-v2): keeping stdin/stdout/stderr open, reporting the exit code and —crucially— allowing dockerd to restart without killing your containers, because the parent is not the daemon but the shim.
ps -o pid,ppid,comm -p "$pid"; ps -o pid,comm -p "$(ps -o ppid= -p $pid | tr -d ' ')"
# 48122 4791 node
# 4791 containerd-shimThe fact that all of this is an open standard (the OCI Runtime Specification) is what lets you swap runc for crun, youki or gVisor without changing anything else. That ecosystem is lesson 07-05.
- Docker Desktop: all of this happens in a VM
If you are on macOS or Windows, none of the above happens in your operating system: namespaces and cgroups are exclusively Linux kernel mechanisms. Docker Desktop runs a lightweight Linux virtual machine and the daemon lives inside it.
| Aspect | Linux | macOS / Windows with Docker Desktop |
|---|---|---|
| Where the daemon runs | In your kernel | In a Linux VM |
/var/lib/docker |
A path on your disk | Inside the VM's virtual disk |
The host's ps aux |
You see the containers' processes | You do not: they are in the VM |
| Bind mounts | Zero cost | They cross the VirtioFS/WSL 2 boundary: slower |
nsenter, lsns, docker0, cgroups |
Direct | Only inside the VM |
To reproduce this lesson from macOS or Windows, get into the VM:
docker run -it --rm --privileged --pid=host justincormack/nsenter1 /bin/sh
# once inside the VM:
ls /sys/fs/cgroup/ && lsns -t pid | head -3And this explains earlier lessons along the way: the slowness of bind mounts (05-02 and 04-07) is not Docker's doing, it is the boundary between two filesystems; and that is why paths and processes "do not show up" where you expect them.
Common Mistakes and Tips
Believing a container is a lightweight machine. It is a process with three mechanisms on top and one single shared kernel. That is where the whole of lesson 05-03 comes from.
Looking for cgroups v1 on a modern system. Since 2022 it has been unified v2: one cgroup per process and different files (memory.max, not memory.limit_in_bytes). And do not write directly into /sys/fs/cgroup: Docker overwrites it when it reconciles; use docker update or Compose.
Running nsenter without -p and expecting to see the isolated process tree. Each namespace is entered separately; without -p you will see the host's processes.
Assuming that an oom_kill 0 means everything is fine. A high max indicates constant memory pressure and latency, even though nobody has died.
Trying lsns or /sys/fs/cgroup from macOS. They do not exist outside Linux. Get into Docker Desktop's VM.
Tip: when something does not add up, go down a level. A limit that seems not to apply is checked in memory.max; a networking problem, in /proc/<pid>/ns/net and nsenter -n; a file that appears or disappears, in the upperdir. The kernel does not lie and it can always be queried.
Exercises
Exercise 1. Prove that a container is a host process: find aurora-api's real PID, check that inside it is PID 1, verify with NSpid that it is the same process, and enter its network namespace with nsenter without using docker exec.
Exercise 2. Check that Compose's limits are exactly the cgroup files: read memory.max, cpu.max and pids.max for aurora-db, translate them back into the options that produced them, change them with docker update and verify that the file changes on the fly.
Exercise 3. Trigger the three copy-on-write operations (create, modify and delete) in aurora-api and locate their three distinct effects in the upperdir. Explain which of the three explains why deleting files does not slim down an image.
Solutions
Solution 1.
pid=$(docker inspect aurora-libros-aurora-api-1 --format '{{.State.Pid}}')
echo "PID on the host: $pid"
docker compose exec aurora-api sh -c 'echo "PID inside: $$"'
sudo grep NSpid /proc/$pid/status
sudo nsenter -t "$pid" -n ip -brief addr
sudo nsenter -t "$pid" -n -p -m ps -eo pid,comm | head -2PID on the host: 48122
PID inside: 1
NSpid: 48122 1
eth0 UP 172.21.0.5/16
eth1 UP 172.22.0.4/16
PID COMMAND
1 nodeNSpid: 48122 1 is the definitive proof: a single process with two identities, the host namespace's and its own. There is no copy, no emulation and no virtual machine; there is a mapping table in the kernel.
And nsenter demonstrates the second point: you have obtained exactly what docker exec gives you without talking to the daemon at any moment. That has two practical consequences. The first is diagnostic: if the daemon is hung or the image is distroless and has no sh, nsenter still works because it uses the host's binaries against the container's namespaces. The second is about security: anybody with root on the host gets into any container without leaving a trace in Docker's logs, which reinforces why root access to the host is the boundary that really matters.
Solution 2.
id=$(docker inspect aurora-libros-aurora-db-1 --format '{{.Id}}')
cg=/sys/fs/cgroup/system.slice/docker-$id.scope
printf 'memory.max %s | cpu.max %s | pids.max %s\n' \
"$(cat $cg/memory.max)" "$(cat $cg/cpu.max)" "$(cat $cg/pids.max)"| Kernel file | Value | The option that produced it |
|---|---|---|
memory.max |
2,147,483,648 B = 2 GiB | deploy.resources.limits.memory: 2G |
cpu.max |
200000 / 100000 | cpus: "2.0" (200 ms for every 100 ms) |
pids.max |
200 | pids_limit: 200 |
docker update --memory 1g --memory-swap 1g --cpus 1.5 aurora-libros-aurora-db-1
printf 'memory.max %s | cpu.max %s\n' "$(cat $cg/memory.max)" "$(cat $cg/cpu.max)"
docker stats --no-stream --format '{{.MemUsage}}' aurora-libros-aurora-db-1The change is instant and requires no container restart: cgroups v2 lets you rewrite the limit on the fly, and the process does not even notice until it tries to exceed it. docker stats reflects the new value because, literally, it reads that file.
The underlying reading is that docker run --memory and deploy.resources are not Docker abstractions: they are a convenient way of writing a number into /sys/fs/cgroup. And the interpretation of cpu.max clears up the most common confusion in the course: --cpus 1.5 does not reserve one and a half cores, it grants 150 ms of CPU time every 100 ms period, which can be spread across the host's eight cores.
Solution 3.
up=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo hello > /tmp/new.txt
echo "//x" >> /app/package.json
rm -f /app/src/util.js'
sudo stat -c '%n -> %F (%s bytes)' "$up/tmp/new.txt" "$up/app/package.json" "$up/app/src/util.js"
docker diff aurora-libros-aurora-api-1 | grep -E 'new|package.json|util.js'.../diff/tmp/new.txt -> regular file (5 bytes)
.../diff/app/package.json -> regular file (612 bytes)
.../diff/app/src/util.js -> character special file (0 bytes)
A /tmp/new.txt
C /app/package.json
D /app/src/util.jsThree operations, three physically distinct representations in the upperdir:
| Operation | In the upperdir |
docker diff |
|---|---|---|
| Create | An ordinary 5-byte file | A |
| Modify | An ordinary 612-byte file: the complete copy, not the delta | C |
| Delete | A character device 0/0 of 0 bytes: a whiteout | D |
The third one explains the phenomenon from lesson 05-04. Deletion removes nothing from the lowerdir —it is read-only, it cannot—: it creates a special marker that makes OverlayFS hide the file when it resolves the unified view. Inside the container, ls answers No such file or directory; on disk, the original file is still intact in its layer, downloaded on every docker pull and stored on every node.
Hence the rule you applied with the &&, which now has its full explanation: if you create and delete inside the same RUN, both operations happen before the layer is consolidated, and the file never comes to exist in anybody's lowerdir. And that is why a credential written and deleted in separate instructions is still extractable with docker save: it is not a Docker bug, it is how a union filesystem works.
And look at the second row, which has consequences of its own: modifying one byte stores the entire file in the upperdir. With package.json that is 612 bytes; with a PostgreSQL data file it would be hundreds of megabytes. That, measured in lesson 05-02 and explained here, is the physical reason databases use volumes.
Conclusion
A container does not exist. What exists is an ordinary Linux process —you have seen it with its real PID in the host's ps and its NSpid carrying two identities— to which the kernel applies three mechanisms: namespaces for what it sees, cgroups for what it consumes and OverlayFS for the filesystem it has. You know the seven namespaces and which Docker option leans on each one, you know how to read them in /proc/<pid>/ns/, compare them with lsns, enter them by hand with nsenter without going through the daemon, and create one from scratch with unshare, confirming that the isolation is nothing magical. And you have discovered, by comparing inodes, that without rootless or --userns-remap all your containers share the host's user namespace: the exact explanation of why root inside is root outside.
You have read your own limits in the kernel: memory.max with the 2 GiB from compose.prod.yaml, pids.max with the 200, and cpu.max with 200000 100000, which reveals that --cpus 2.0 is a time quota per period and not a reservation of cores. You have changed them on the fly with docker update and watched the file move. You have seen the OOM killer from the inside, in memory.events, with its oom_kill 1 and the exit code 137 you already knew. And you have handled copy-on-write with your own hands: a new file, a complete copy for modifying a single byte, and a character device 0/0 for a deletion, which is the physical explanation of why deleting files neither slims down an image nor removes a secret. You close the security circle by seeing CapEff at zero decoded with capsh, Seccomp: 2 and NoNewPrivs: 1; and you know what runc does with its OCI-specification config.json and why the shim allows dockerd to restart without killing anything. Plus the essential note for anybody working on macOS or Windows: all of this happens inside a Linux VM, and that is where those paths and performance figures that did not add up were coming from.
And with that, module 5 closes. You came in knowing how to use Docker very well and you leave knowing exactly what happens when you run it. You have opened up networking down to the bridges, the veth pairs and the iptables rules, with the warning that publishing a port bypasses the host firewall; storage down to overlay2 and a backup strategy with verified restores; security down to the threat model, rootless mode, capabilities, seccomp, CVE scanning and Cosign signing, with Aurora Libros hardened in an annotated compose.prod.yaml; optimization, which took the API from 142 MB to 102 MB with multi-stage builds and constant measurement; BuildKit and Buildx, with cache mounts, traceless secrets, remote cache and multi-architecture; observability, with structured logs, Loki, Prometheus, ratio-based alerts and a /health that tells the truth; and, finally, the kernel that holds it all up.
In module 6, Aurora Libros leaves your machine. You will prepare the definitive production image, set up a CI/CD pipeline that builds, scans, signs and publishes using the remote cache you already know how to use, and take the platform to a cluster: first with Docker Swarm and its overlay networks, then with Kubernetes —its objects, the real deployment of the four services, scaling and load balancing— and you will finish with the deployment and rollback strategies that let you publish a new version without anybody losing the ability to buy a book.
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
