Docker
Docker Tutorial for Debian and Ubuntu Server
Section titled “Docker Tutorial for Debian and Ubuntu Server”0. Specs
Section titled “0. Specs”0.1. The What
Section titled “0.1. The What”Docker is an open-source containerization platform leveraging native Linux kernel features, specifically namespaces and cgroups.
It facilitates packaging applications—along with their runtime, libraries, and system dependencies—into portable artifacts known as images. An image can be instantiated as one or more containers, which operate as isolated processes on the host system.
Unlike virtual machines (VMs), containers share the host kernel while maintaining strict process, network, and filesystem isolation. This architecture provides:
- Minimal Overhead: Low resource consumption due to the absence of hardware emulation.
- Rapid Startup: Near-instantaneous container initialization.
- Environment Parity: Absolute consistency between development, staging, and production environments.
- Predictable Deployment: Immutable infrastructure through repeatable image-based delivery.
0.2. The Environment
Section titled “0.2. The Environment”All scenarios and commands in this documentation have been validated on:
- Debian 13 (Trixie)
- Ubuntu Server 24.04 LTS
Topology:
docker: Standalone Docker host.node1–node5: Docker Swarm cluster nodes.
This guide focuses on production-grade server management via CLI, rather than desktop-oriented development workflows.
0.3. Sources
Section titled “0.3. Sources”- Docker Official Documentation
- Gemini
- Deepseek
- ChatGPT
- Claude
- Book: Docker Quick Start Guide by Earl Waud, ISBN: 9781789347326
- Book: Getting Started with Docker by Nigel Poulton, ISBN: 9781916585300
- Book: Docker Deep Dive by Nigel Poulton, ISBN: 9781916585133
- Book: Learn Docker in a Month of Lunches 2nd Ed. by Elton Stoneman, ISBN: 9781633438460
- Book: Docker Up & Running by Sean P. Kane with Karl Matthias, ISBN: 978-1-098-13182-1
- Book: Painless Docker by Aymen El Amri, ISBN: 9798870316826
- Book: The Docker Workshop by Vincent Sesto, Onur Yılmaz, Sathsara Sarathchandra, Aric Renzo, and Engy Fouda, ISBN: 9781838983444
- Book: The Ultimate Docker Container Book by Dr. Gabriel N. Schenker, ISBN: 978-1-80461-398-6
1. Installation
Section titled “1. Installation”1.1. Installation Types
Section titled “1.1. Installation Types”Docker can be installed using:
- Distribution packages (Debian/Ubuntu repositories)
- Docker’s official upstream repositories
Distribution packages
- Integrated with the OS lifecycle
- Conservative version updates
- Suitable for long-term stability-focused environments
Docker upstream packages
- Faster release cadence
- Access to latest features (BuildKit, Compose plugin updates, etc.)
- Preferred when feature parity with upstream documentation is required
1.2. Architectural Components
Section titled “1.2. Architectural Components”Docker is composed of multiple layers:
- Docker CLI (
docker): Command-line client. Sends REST API requests to the Docker daemon. - Docker Daemon (
dockerd): Background service managing images, containers, networks, volumes, and build operations. - containerd: High-level container runtime responsible for container lifecycle management.
- runc: Low-level OCI runtime that interacts directly with Linux kernel features (namespaces, cgroups).
- Docker Compose (v2 plugin): Declarative multi-container orchestration via YAML.
Execution Flow:
Docker CLI → dockerd → containerd → runc → Linux Kernel
1.3. Debian 13 Installing from Debian Packages
Section titled “1.3. Debian 13 Installing from Debian Packages”sudo apt updatesudo apt install docker.io docker-compose1.4. Ubuntu 24.04 Installing from Ubuntu Packages
Section titled “1.4. Ubuntu 24.04 Installing from Ubuntu Packages”sudo apt updatesudo apt install docker.io docker-compose-v21.5. Debian 13 Installing From Docker Repos
Section titled “1.5. Debian 13 Installing From Docker Repos”Remove any previously installed docker packages:
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-doc podman-docker containerd runc | cut -f1)Set up Docker’s apt repository.
# Add Docker's official GPG key:sudo apt updatesudo apt install ca-certificates curlsudo install -m 0755 -d /etc/apt/keyringssudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.ascsudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:sudo tee /etc/apt/sources.list.d/docker.sources <<EOFTypes: debURIs: https://download.docker.com/linux/debianSuites: $(. /etc/os-release && echo "$VERSION_CODENAME")Components: stableSigned-By: /etc/apt/keyrings/docker.ascEOF
sudo apt updateInstall Docker packages:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin1.6. Ubuntu 24.04 Installing from Docker Repos
Section titled “1.6. Ubuntu 24.04 Installing from Docker Repos”Remove any previously installed docker packages:
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)Set up Docker’s apt repository.
# Add Docker's official GPG key:sudo apt updatesudo apt install ca-certificates curlsudo install -m 0755 -d /etc/apt/keyringssudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.ascsudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:sudo tee /etc/apt/sources.list.d/docker.sources <<EOFTypes: debURIs: https://download.docker.com/linux/ubuntuSuites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")Components: stableSigned-By: /etc/apt/keyrings/docker.ascEOF
sudo apt updateInstall Docker packages:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin1.7. Post-Installation Steps
Section titled “1.7. Post-Installation Steps”User Permissions: Add the current user to the docker group to enable execution without sudo (requires logout/login to take effect).
sudo usermod -aG docker $USER⚠ Security Note: Membership in the docker group effectively grants root-level privileges on the host system. This should be treated accordingly in production environments.
Verification:
docker versiondocker infodocker compose versiondocker container run hello-world2. Docker Images
Section titled “2. Docker Images”2.1. Overview
Section titled “2.1. Overview”A Docker Image is a read-only, immutable template used to instantiate containers. Conceptually similar to a “Class” in Object-Oriented Programming, it encapsulates the root filesystem, application code, binaries, libraries, and environment variables required for execution.
Images are hosted in Container Registries. Common registries include:
- Public: Docker Hub, GitHub Container Registry (GHCR), Quay.io
- Cloud-Specific: Amazon ECR, Azure ACR, Google Artifact Registry
- Self-Hosted: Harbor
Image Naming Convention:
An image is identified by a four-part fully qualified name (defaults in parentheses):
[Registry (docker.io)] / [Owner (library)] / [Image Name] : [Tag (latest)]
Examples:
All the following commands pull the same nginx image from Docker Hub:
docker image pull docker.io/library/nginx:latestdocker image pull library/nginx:latestdocker image pull nginx:latestdocker image pull nginxList the downloaded images:
docker image lsExample output:
REPOSITORY TAG IMAGE ID CREATED SIZEnginx latest 058f4935d1cb 8 days ago 152MBRemove an image by Name or ID (partial ID is sufficient):
docker image rm nginxdocker image rm 058f2.2. Image Layers
Section titled “2.2. Image Layers”A Docker image consists of multiple layers, which are immutable filesystem differences (diffs). When a container is started, Docker utilizes a Storage Driver (typically overlay2) to stack these layers into a single unified root filesystem.
Key characteristics:
- Immutability: Existing layers cannot be changed.
- Efficiency: Layers are cached and shared across multiple images to save disk space and reduce download times.
- Copy-on-Write (CoW): Only changes made during the container’s runtime are written to a temporary “container layer” on top.
2.3. Creating a Docker Image - Dockerfile
Section titled “2.3. Creating a Docker Image - Dockerfile”To create a Docker image, we need an empty folder and create a Dockerfile containing the image definition there.
An image is defined using a Dockerfile, a plain-text manifest containing ordered instructions.
Example Build Workflow:
mkdir ~/testimage && cd ~/testimagenano DockerfileDockerfile Content:
# Step 1: Define Base ImageFROM debian:trixie
# Step 2: Install dependencies (Grouped to minimize layer count)RUN apt-get update && apt-get install -y \ fortune-mod \ && rm -rf /var/lib/apt/lists/*
# Step 3: Set environment variablesENV USER_NAME="Dear User"
# Step 4: Define default execution commandCMD ["/usr/games/fortune"]Building and Publishing:
# Build with a local tagdocker image build -t welcome:1.0.0 .
# Build for a specific registry (e.g., Docker Hub user 'exforge')docker image build -t exforge/welcome:1.0.0 .
# Authentication and Pushdocker logindocker image push exforge/welcome:1.0.0Some common keywords of Dockerfile syntax:
- FROM: Sets the starting point. Every Dockerfile must start with this.
- RUN: Executes a command during the build. This creates a new permanent layer.
- ENV: Sets environment variables that the container can use while running.
- COPY: Moves files from the computer into the image.
- CMD: The default behavior when running the container. (Think of it like an entry in /etc/init.d/ or a systemd ExecStart).
2.4. Image Optimization & Multi-Stage Builds
Section titled “2.4. Image Optimization & Multi-Stage Builds”Production-grade images must be minimal to reduce the attack surface and deployment latency.
Optimization Strategies:
- Layer Minimization: Chain commands (e.g.,
apt update && apt install) to reduce the number of layers. - Cleanup: Remove package manager caches (
/var/lib/apt/lists/*) in the sameRUNinstruction where they were created. - Multi-Stage Builds: The most effective method for compiled languages. Use a “heavy” image for building and a “light” image for the final runtime.
Multi-Stage Example (Go):
Create a directory and a simple Go web server:
mkdir ~/gocontainer && cd ~/gocontainernano main.gomain.go content:
package main
import ( "fmt" "net/http")
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "This Go program runs in a optimized container!") })
fmt.Println("Server starting on port 8080...") http.ListenAndServe(":8080", nil)}Create the Multi-Stage Dockerfile:
nano DockerfileFill as below:
# --- STAGE 1: Build ---FROM golang:1.21-alpine AS builderWORKDIR /appCOPY main.go .RUN go mod init go-app && \ CGO_ENABLED=0 GOOS=linux go build -o go-binary .
# --- STAGE 2: Runtime ---FROM alpine:latestWORKDIR /root/# Copy ONLY the artifact from the builder stageCOPY --from=builder /app/go-binary .EXPOSE 8080CMD ["./go-binary"]Build the image:
docker image build -t go-app .2.5. “docker image” Commands
Section titled “2.5. “docker image” Commands”Some important docker image commands:
- docker image ls: Lists all locally stored images
- docker image pull: Downloads an image from a registry (e.g., Docker Hub).
- docker image build: Creates an image from a Dockerfile.
- docker image inspect: Displays low-level information of an image (JSON).
- docker image history: Shows the history/layers of an image.
- docker image tag: Creates a new tag (alias) for an image.
- docker image rm: Removes one or more images from the host.
- docker image prune: Removes all unused (dangling) images.
- docker image save: Saves an image to a .tar archive.
- docker image load: Loads an image from a .tar archive.
3. Docker Containers
Section titled “3. Docker Containers”3.1. Overview
Section titled “3.1. Overview”A container is a functional instance of a Docker image. While images are static and read-only, containers are dynamic, stateful (unless ephemeral), and executable.
Docker leverages Linux Namespaces to provide isolation for:
- PID: Process isolation (the container has its own
initprocess). - NET: Dedicated network stack (interfaces, routing tables, firewall rules).
- MNT: Isolated mount points and filesystem.
- UTS: Independent hostname and domain name.
- IPC: Inter-process communication isolation.
We can compare a container and a Virtual Machine in the following table :
| Feature | Virtual Machine | Container (Docker) |
|---|---|---|
| Kernel | Separate Kernel per VM | Shared Host Kernel |
| Isolation | Hardware-level (Hypervisor) | Process-level (Namespaces) |
| Speed | Slow (Boots OS) | Instant (Starts Process) |
| Size | Large (GBs) | Small (MBs) |
| Performance | Near-native (with overhead) | Native execution speed |
3.2. Running Containers
Section titled “3.2. Running Containers”To instantiate a container from the welcome image:
docker container run exforge/welcome:1.0.0The container executes the defined CMD, outputs the result to stdout, and immediately transitions to an Exited state.
Monitoring and Cleanup:
# List only active (running) containersdocker container ls
# List all containers (including Exited/Stopped)docker container ls -a
# Remove a specific container using Name or partial IDdocker container rm <ID_or_Name>
# Run and automatically remove the container upon exit (Ephemeral)docker container run --rm exforge/welcome:1.0.03.3. Running Background Services (Detached Mode)
Section titled “3.3. Running Background Services (Detached Mode)”For long-running services like web servers, we use Detached Mode (-d).
Practical Example: Apache on Alpine
mkdir ~/simpleapache && cd ~/simpleapachenano DockerfileDockerfile:
FROM alpine:latest
# Install Apache and initialize the runtime directoryRUN apk update && \ apk add --no-cache apache2 && \ mkdir -p /run/apache2
# Inject a static index pageRUN echo "<html><body><h1>Production Node</h1><p>Status: Online</p></body></html>" > /var/www/localhost/htdocs/index.html
# Documentation for the listener portEXPOSE 80
# Execute Apache in the foreground to keep the container aliveCMD ["httpd", "-D", "FOREGROUND"]Deployment:
# 1. Build the imagedocker image build -t exforge/simpleapache .
# 2. Deploy multiple instances with Port Mapping (-p host_port:container_port)docker container run -d --name web01 -p 8080:80 exforge/simpleapachedocker container run -d --name web02 -p 8088:80 exforge/simpleapacheResource Monitoring:
# Real-time CPU, Memory, and I/O usagedocker container statsAccessing containers:
http://docker:8080/http://docker:8088/Removing Containers
# Stopdocker container stop web01 web02# And removedocker container rm web01 web023.4. Advanced Runtime Configurations
Section titled “3.4. Advanced Runtime Configurations”System administrators can enforce constraints and modify container behavior using the following flags:
Resource Constraints (Capping)
--memory="512m": Limits the RAM usage; prevents a single container from exhausting host memory.--cpus="1": Restricts the container to a specific number of CPU shares.--restart: Defines the recovery policy (no,on-failure,always,unless-stopped).
Network & Identity
-h/--hostname: Sets the internal hostname (essential for cluster identification).--network: Attaches the container to a specific Docker network (e.g.,bridge,host,none).--add-host: Injects custom entries into/etc/hostsfor local DNS resolution.
Security Hardening
--read-only: Mounts the container’s root filesystem as read-only. Mandatory for high-security environments.--user: Runs the process as a non-root UID/GID to mitigate privilege escalation risks.--privileged: (Use with Caution) Grants the container direct access to host hardware and kernel capabilities.
3.5. “docker container” Commands
Section titled “3.5. “docker container” Commands”Some important docker container commands:
- docker container run: Create + Start.
- docker container create: Create only.
- docker container start: Start a stopped container. Use
-ato attach to the output. - docker container stop: Graceful stop.
- docker container kill: Forced stop.
- docker container restart: Stop + Start. Restarts the process inside.
- docker container ls: List running, like
ps - docker container ls -a: List all (even dead), like
ps -aux - docker container logs -f: Follow logs, like
tail -f /var/log/... - docker container top: Show processes, like
toporhtop - docker container stats: Resource usage, like
vmstatoriostat - docker container inspect: Detailed config, like reading a
.conffile. - docker container exec -it
bash : Opens a shell inside an already running container. - docker container cp <local_path> <container_name>:
: Just like scpbut between the host and the container. - docker container rm
: Removes a stopped container.
- docker container rm -f
: Forces the removal of a running container (Stops then Removes). - docker container prune: Deletes every stopped container on the system.
4. Docker Volumes
Section titled “4. Docker Volumes”4.1. Overview
Section titled “4.1. Overview”By design, containers are ephemeral. Any data written to the container’s writable layer is lost when the container is deleted. To decouple data from the container lifecycle, Docker provides two primary mechanisms:
| Feature | Bind Mounts | Named Volumes |
|---|---|---|
| Host Location | Any user-defined path (e.g., /opt/app/conf) | Managed by Docker (/var/lib/docker/volumes/) |
| Management | Managed by the OS/Sysadmin | Managed via Docker CLI/API |
| ”Copy-up” | No (Host content overwrites container path) | Yes (Image content populates empty volumes) |
| Use Case | Configuration files and source code sharing | Database storage and production data |
| Isolation | Low (Container can modify sensitive host files) | High (Data is isolated from host users) |
4.2. Bind Mounts: Direct Host Mapping
Section titled “4.2. Bind Mounts: Direct Host Mapping”Bind mounts are ideal to provide specific host files—such as configuration or static assets—to a container.
Practical Exercise:
Create a local directory and a custom index file:
mkdir ~/htmlecho "<html><body><h1>Status: Bind Mount Active</h1></body></html>" > ~/html/index.htmlDeploy the container using the -v (or --mount) flag:
docker container run -d -p 8080:80 \ --name bind-test \ -v $HOME/html:/var/www/localhost/htdocs \ exforge/simpleapacheNote: In bind mounts, the host path must be an absolute path (using $HOME or /home/user).
If we point our browser to: http:/docker:8080/ we’ll see our new Bind Mount welcome page.
Remove the container after testing it:
docker container stop bind-testdocker container rm bind-testOr combined:
docker container rm -f bind-test4.3. Named Volumes: Docker-Managed Storage
Section titled “4.3. Named Volumes: Docker-Managed Storage”Named volumes are the preferred method for persistent data in production. Docker handles storage driver optimizations and filesystem permissions automatically.
The “Copy-up” Feature: Unlike bind mounts, when mounting an empty named volume to a container path that already contains data (from the image), Docker copies that data into the volume first.
Workflow:
- Create the volume:
docker volume create lv-web-data- Instantiate the container:
docker container run -d -p 8081:80 \ --name volume-test \ -v lv-web-data:/var/www/localhost/htdocs \ exforge/simpleapacheIf we point our browser to: http:/docker:8081/ we’ll see the original welcome page.
Inspection & Maintenance:
The actual data resides in the Docker root directory (typically /var/lib/docker/volumes/). We can inspect the metadata using:
docker volume inspect lv-web-dataOutput (JSON):
{ "CreatedAt": "2026-03-04T01:00:00Z", "Driver": "local", "Mountpoint": "/var/lib/docker/volumes/lv-web-data/_data", "Scope": "local"}Remove the container and the volume after testing it:
docker container rm -f volume-testdocker volume rm lv-web-dataSome volume management commands:
docker volume ls: Lists all volumes managed by the local daemon.docker volume inspect <name>: Displays detailed path and driver information.docker volume rm <name>: Deletes a volume (fails if attached to a container).docker volume prune: Cleanup: Removes all volumes not currently used by any container.
4.4. Case Study 1: Persistent Database Management
Section titled “4.4. Case Study 1: Persistent Database Management”This scenario demonstrates decoupling data from the MariaDB engine. We will verify that data survives a container’s destruction and recreation.
Provision the Volume:
docker volume create mariadb_dataDeploy the Primary Instance:
Attach the volume to MariaDB’s internal data directory (/var/lib/mysql) and set the root password via Environment Variables.
docker container run -d \ --name db-master \ -e MARIADB_ROOT_PASSWORD=SysAdminPass123 \ -v mariadb_data:/var/lib/mysql \ mariadb:latestData Injection (SQL Operations): Access the MariaDB shell inside the running container:
docker exec -it db-master mariadb -u root -pSysAdminPass123Execute the following SQL commands:
CREATE DATABASE prod_db;USE prod_db;CREATE TABLE assets (id INT AUTO_INCREMENT PRIMARY KEY, hostname VARCHAR(50));INSERT INTO assets (hostname) VALUES ('srv-web-01'), ('srv-db-01'), ('srv-proxy-01');SELECT * FROM assets;EXIT;Destruction and Recovery: Stop and remove the container, then instantiate a new one using the same volume.
docker container rm -f db-master
docker container run -d \ --name db-recovery \ -e MARIADB_ROOT_PASSWORD=SysAdminPass123 \ -v mariadb_data:/var/lib/mysql \ mariadb:latestVerification and Backup: Verify data persistence and perform a logical backup to the host filesystem:
# Direct query from hostdocker exec -it db-recovery mariadb -u root -pSysAdminPass123 -e "SELECT * FROM prod_db.assets;"
# Perform mysqldump to hostmkdir -p ~/backupsdocker exec db-recovery \ /usr/bin/mariadb-dump -u root -pSysAdminPass123 prod_db \ > ~/backups/prod_db_$(date +%F).sqlAfter testing, delete the container and the volume:
docker container rm -f db-recoverydocker volume rm mariadb_data4.5. Case Study 2: Shared Storage for Log Aggregation
Section titled “4.5. Case Study 2: Shared Storage for Log Aggregation”In this scenario, we use a shared volume to allow a secondary container to monitor logs from a web server. We apply the Read-Only (:ro) flag for the watcher container to ensure data integrity.
Create the Shared Volume:
docker volume create shared_logsDeploy the Producer (Web Server):
docker container run -d \ --name web-server \ -p 8085:80 \ -v shared_logs:/var/log/apache2 \ exforge/simpleapacheDeploy the Consumer (Log Watcher): The watcher mounts the same volume as Read-Only to prevent accidental log modification.
docker container run -d \ --name log-watcher \ -v shared_logs:/mnt/logs:ro \ alpine tail -f /mnt/logs/access.logTesting the Pipeline:
Monitor the logs of the log-watcher while generating traffic:
# In Terminal 1docker logs -f log-watcher
# In Terminal 2 (Generate traffic)curl http://localhost:8085Cleanup:
docker container rm -f web-server log-watcherdocker volume rm shared_logs4.6. “docker volume” Commands
Section titled “4.6. “docker volume” Commands”Some important docker volume commands:
- docker volume create: Manual volume provisioning. Useful for setting up NFS or cloud storage.
- docker volume ls: Lists all volumes. Filtering helps find “orphan” volumes taking up space. |
- **docker volume inspect:**Returns JSON metadata. Essential for finding the Mountpoint path.
- docker volume rm: Hard deletion of a volume. Only works if no container (even stopped) is using it.
- docker volume prune: Bulk cleanup. Deletes every volume not currently attached to a container.
docker volume rm $(docker volume ls -q): (Bash trick) Force-delete all volumes (except those in use). A quick “reset” for admins.
5. Docker Networking
Section titled “5. Docker Networking”5.1. Standards: CNM vs. CNI
Section titled “5.1. Standards: CNM vs. CNI”Networking in the container ecosystem is governed by two competing specifications:
-
CNM (Container Network Model): The native Docker standard, implemented via
libnetwork. It focuses on a modular design using Sandboxes (isolated stacks), Endpoints (interfaces), and Networks (virtual switches). It is deeply integrated into the Docker Engine and supports features like Docker Swarm’s routing mesh. -
CNI (Container Network Interface): A simpler, vendor-neutral specification used by Kubernetes, OpenShift, and Podman. CNI focuses solely on the connectivity of the container at creation. It relies on external plugins (e.g., Calico, Flannel, Cilium) to manage the network lifecycle.
5.2. Native Docker Network Drivers
Section titled “5.2. Native Docker Network Drivers”Upon installation, Docker initializes three default networks. We can verify these with:
docker network ls
5.2.1. Bridge (The Default)
Section titled “5.2.1. Bridge (The Default)”- Implementation: Acts as a software-defined virtual switch (typically interface
docker0on the host). - Connectivity: Each container is assigned a private IP (e.g.,
172.17.0.x/16). Communication with the external world is handled via Source NAT (SNAT), while inbound traffic requires Port Mapping (-p). - Use Case: Standard standalone applications requiring isolation with managed access.
- Note: All containers utilize this driver unless the
--networkflag is specified.
5.2.2. Host
Section titled “5.2.2. Host”- Implementation: Removes the network stack isolation. The container shares the host’s IP address, routing table, and ports directly.
- Performance: Offers the highest throughput and lowest latency as it bypasses the Docker NAT and bridge overhead.
- Constraints: Port mapping (
-p) is ignored. Only one process can bind to a specific port on the host at a time. - Use Case: Network-intensive applications (e.g., VoIP, high-load load balancers, or monitoring agents like Prometheus Exporters).
Practical Example: Deploying Apache on the host network makes it instantly reachable on the host’s port 80:
docker container run -d --name web-host --network host exforge/simpleapacheCaution: Starting a second instance with --network host will result in a “Port already in use” error.
Cleanup:
docker container rm -f web-host5.2.3. None
Section titled “5.2.3. None”- Implementation: The container is provided with a loopback interface (
lo) only. No external network interfaces are provisioned. - Security: Provides the highest level of network air-gapping.
- Use Case: Sensitive batch processing, cryptographic operations, or any local filesystem-only task where network exfiltration must be physically impossible.
Verification:
docker run --rm --network none alpine ip addr5.3. User-Defined Bridge Networks
Section titled “5.3. User-Defined Bridge Networks”In production environments, using the default bridge is considered a legacy practice. For professional deployments, User-Defined Bridge Networks are mandatory due to the following architectural advantages:
- Automatic Service Discovery (Built-in DNS): Unlike the default bridge where containers must communicate via static IP addresses, user-defined networks provide a resident DNS server. Containers can resolve and reach each other using their names as hostnames.
- Granular Network Isolation: We can create dedicated segments (e.g.,
web-dmz,app-internal,db-secure) to ensure that only authorized containers can communicate with sensitive services (like databases). - Hot-Plugging: Network interfaces can be attached to or detached from running containers dynamically, without requiring a container restart.
Practical Exercise 1: Service Discovery Verification
Section titled “Practical Exercise 1: Service Discovery Verification”This exercise demonstrates how Docker handles internal name resolution.
Provision a Custom Network:
docker network create net-internalDeploy Interconnected Nodes:
docker container run -d --name srv-alpha --network net-internal alpine sleep 3600docker container run -d --name srv-beta --network net-internal alpine sleep 3600Validate DNS Resolution:
# Testing connectivity from alpha to beta using its namedocker container exec -it srv-alpha ping -c 3 srv-betaClean Up:
# Remove the containersdocker container rm -f srv-alpha srv-beta
# Remove the internal networkdocker network rm net-internalPractical Exercise 2: Multi-Homed Containers & Segregation
Section titled “Practical Exercise 2: Multi-Homed Containers & Segregation”In this scenario, we architect a two-tier application where the web server acts as a gateway (bridge) between a public-facing network and a secure backend.
Network Infrastructure:
docker network create frontend-netdocker network create backend-netSecure Database Tier (Backend Only):
docker container run -d --name srv-db \ --network backend-net \ -e MARIADB_ROOT_PASSWORD=topsecret \ mariadb:latestApplication Tier (Dual-Homed):
We start the web server in frontend-net, then manually bridge it into backend-net.
docker container run -d --name srv-web --network frontend-net exforge/simpleapache
# Hot-plugging the second interfacedocker network connect backend-net srv-webNetwork Audit & Security Validation:
Verify that the srv-web has two interfaces and can reach the database, while a container only on frontend-net (e.g., a “hacker” or “unauthorized node”) is blocked.
# Check connectivity from web server to DBdocker container exec -it srv-web ping -c 2 srv-db
# Audit network configurationsdocker network inspect frontend-netdocker network inspect backend-netCleanup:
docker container rm -f srv-db srv-webdocker network rm frontend-net backend-net5.4. Enterprise Network Drivers: Macvlan & Overlay
Section titled “5.4. Enterprise Network Drivers: Macvlan & Overlay”For complex infrastructure requirements, Docker provides advanced drivers that extend connectivity beyond a single host or integrate with existing physical network topologies.
5.4.1. Macvlan: Containers as Physical Network Nodes
Section titled “5.4.1. Macvlan: Containers as Physical Network Nodes”The Macvlan driver allows assigning a unique MAC address to each container, making it appear as a distinct physical device on the network.
- How it works: Instead of using the host’s IP and port mapping (NAT), the container binds directly to the host’s physical interface (e.g.,
eth0). - Key Benefits:
- Legacy Support: Ideal for applications that expect to be on a direct physical network rather than behind a NAT.
- External Visibility: Network monitoring tools, firewalls, and IDSs can track and filter container traffic based on their individual MAC/IP addresses.
- VLAN Integration: Can be configured to work with existing 802.1Q VLAN trunking.
5.4.2. Overlay: Multi-Host Mesh Networking
Section titled “5.4.2. Overlay: Multi-Host Mesh Networking”The Overlay driver is the backbone of distributed systems. It creates a virtual logical network that spans across multiple Docker hosts.
- How it works: It uses VXLAN encapsulation to “tunnel” traffic between containers residing on different physical servers.
- Key Requirement: Requires Docker Swarm mode (or an external Key-Value store) to manage the control plane and IP routing between nodes.
- Use Case: Microservices architecture where
container-AonHost-1must communicate withcontainer-BonHost-2securely and transparently.
5.5. “docker network” Commands
Section titled “5.5. “docker network” Commands”Some important docker network commands:
docker network create [NAME]Creates a new virtual switch (network).docker network connect [NET] [CON]Connects an existing container to a network.docker network disconnect [NET] [CON]Safely removes a container from a network.docker network inspect [NAME]Shows which containers are currently on this network and their IPs.docker network rm [NAME]Deletes the network (only if no containers are using it).docker network pruneDeletes all unused networks (not used by any containers).
6. Docker Compose
Section titled “6. Docker Compose”6.1. Overview
Section titled “6.1. Overview”Docker Compose is an orchestration tool used to define and manage multi-container applications. Instead of executing lengthy, error-prone CLI commands for each resource, we define our infrastructure in a declarative YAML file (docker-compose.yml).
A standard Compose file is organized into three primary top-level sections:
- Services: Defines the containers (e.g.,
web,db,cache). This includes the image/build context, port mapping, and environment variables. - Networks: Defines the virtual switches. Compose automatically creates a default network for the project, but custom networks allow for strict traffic isolation.
- Volumes: Defines persistent storage. Compose manages the lifecycle of these volumes, ensuring data survives service restarts.
Key Benefits for Admins:
- Reproducibility: The exact same environment can be stood up on any host with a single command.
- Documentation: The YAML file serves as living documentation of the application’s infrastructure.
- Project Isolation: Compose uses “project names” (usually the folder name) to keep different environments on the same host from colliding.
6.2. Practical Exercise: Multi-Tier Go Application
Section titled “6.2. Practical Exercise: Multi-Tier Go Application”In this scenario, we deploy a compiled Go web application and a MariaDB database with proper network segregation and scaling.
Workspace Setup:
mkdir ~/compose-project && cd ~/compose-projectCreate the main.go program for the web app:
nano main.goContents:
package main
import ( "fmt" "net/http")
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "This Go program runs in a optimized container!") })
fmt.Println("Server starting on port 8080...") http.ListenAndServe(":8080", nil)}Create the Multi-Stage Dockerfile for the web app:
nano DockerfileContents:
# --- STAGE 1: Build (Compile) ---# Use a heavy image containing the Go SDKFROM golang:1.21-alpine AS builder
WORKDIR /appCOPY main.go .
# Initialize module and compile a static binaryRUN go mod init go-app && \ CGO_ENABLED=0 GOOS=linux go build -o go-binary .
# --- STAGE 2: Final (Run) ---# Use a tiny Alpine image for productionFROM alpine:latest
WORKDIR /root/
# Copy ONLY the compiled binary from the builder stageCOPY --from=builder /app/go-binary .
# Document the port the app listens onEXPOSE 8080
CMD ["./go-binary"]Defining the Infrastructure (docker-compose.yml):
nano docker-compose.ymlservices: webapp: build: . # Map a range of host ports to the container's 8080 ports: - "8080-8090:8080" networks: - frontend-net - backend-net depends_on: - database
database: image: mariadb:latest environment: MARIADB_ROOT_PASSWORD: "AdminPassword123" volumes: - db-data:/var/lib/mysql networks: - backend-net
networks: frontend-net: backend-net:
volumes: db-data:Operational Commands:
- Deployment: Compose builds the image, creates networks/volumes, and respects the
depends_onorder.
docker compose up -d- Horizontal Scaling: Increase the number of web server instances.
docker compose up -d --scale webapp=3- Monitoring:
docker compose ls # List active compose projectsdocker compose ps # Status of containers in the current projectdocker compose top # Display processes running within services- Decommissioning:
# Stop and remove containers/networks (Preserves Volumes)docker compose down
# Full Cleanup (Deletes Volumes as well)docker compose down -v6.3. “docker compose” Commands
Section titled “6.3. “docker compose” Commands”Some important docker compose commands:
- docker compose up -d: Builds, (re)creates, starts, and detaches to containers for a service in the background.
- docker compose down: Stops and removes containers, networks, images, and volumes defined in the file.
- docker compose ps: Lists the status of containers in the current project.
- docker compose logs -f: Follows the log output from the services.
- docker compose pause: Pause services
- docker compose scale: Scale services
- docker compose top: Display the running processes
- docker compose unpause: Unpause services
7. Advanced Management & Housekeeping
Section titled “7. Advanced Management & Housekeeping”7.1. Overview
Section titled “7.1. Overview”While most daily operations involve containers and images, high-level management requires interacting with the Docker Engine’s global state and external endpoints. This is handled through three primary command groups:
docker context: Manage connectivity to multiple Docker nodes (Local, Remote, or Cloud).docker system: Monitor resource consumption and perform global cleanup.docker trust: Enforce content trust through digital signatures (Image Provenance).
7.2. Multi-Node Management: docker context
Section titled “7.2. Multi-Node Management: docker context”docker context allows a sysadmin to switch the target of the Docker CLI seamlessly. Instead of setting environment variables like DOCKER_HOST, we define persistent named contexts.
Practical Use Case: Remote Management via SSH To manage a production server from local workstation without exposing a TCP socket:
Define a new context:
docker context create prod-srv-01 \ --description "Production Node 01" \ --docker "host=ssh://adminuser@192.168.1.50"Switch the active target:
docker context use prod-srv-01Now, any command like docker ps or docker stats runs directly on the production server.
Return to local daemon:
docker context use defaultQuick Reference:
docker context ls # list all contextsdocker context show # show current active contextdocker context create <name> ... # create a new contextdocker context rm <name> # delete a context7.3. Engine Housekeeping: docker system
Section titled “7.3. Engine Housekeeping: docker system”This group is essential for maintaining host health and monitoring the daemon’s internal state.
- docker system df: Displays a disk usage summary. Essential for identifying if images, containers, or local volumes are exhausting host storage.
- docker system info: Provides a comprehensive report on the daemon (Kernel version, Storage Driver, Cgroup version, and Security Options).
- docker system events: Real-time logging of daemon activity (e.g., container start/die, image pull, volume mount).
The “Prune” Logic (Garbage Collection):
System administrators should use prune regularly to reclaim space.
# Standard: Removes stopped containers, unused networks, and dangling images.docker system prune
# Aggressive: Removes ALL unused images (not just dangling ones).docker system prune -a
# Nuclear: Includes unused volumes. Use with extreme caution in production!docker system prune -a --volumes7.4. Security & Supply Chain: docker trust
Section titled “7.4. Security & Supply Chain: docker trust”docker trust implements Docker Content Trust (DCT). It ensures that the images the infrastructure pulls are signed by authorized parties and have not been altered in transit.
- Verification:
docker trust inspect <image>checks if the image has a valid signature. - Signing:
docker trust sign <image>pushes a signed version of the image to a registry. - Enforcement: In high-security environments, setting
export DOCKER_CONTENT_TRUST=1in the shell will cause the Docker CLI to refuse to pull or run any unsigned images.
Quick Reference:
docker trust inspect <image> # show signing info for an imagedocker trust sign <image> # sign an imagedocker trust revoke <image> # revoke a signaturedocker trust key generate <name> # generate a signing key pairdocker trust key load <pem> # load an existing keydocker trust signer add <name> # add a signer to a repodocker trust signer remove <name> # remove a signer8. Docker Swarm Orchestration
Section titled “8. Docker Swarm Orchestration”8.1. Architectural Overview
Section titled “8.1. Architectural Overview”Docker Swarm is the native clustering and container orchestration solution for the Docker platform. It enables the management of a group of Docker hosts as a single, resilient, virtual entity.
Core Concepts:
-
Nodes: Any Docker-enabled host participating in the cluster.
- Manager Nodes: Handle cluster state management, task scheduling, and servicing HTTP API endpoints.
- Worker Nodes: Execute containers (tasks) dispatched by the managers. In small clusters, managers often act as workers as well.
-
Services & Tasks: A Service is the declarative definition of the state (e.g., “Run 3 Nginx replicas”). A Task is the atomic unit of the Swarm—essentially a container instance assigned to a node.
-
Reconciliation Loop (Desired State): The Swarm Manager constantly monitors the cluster. If a node fails and a container stops, the manager automatically reschedules that task on an available node to maintain the “Desired State.”
-
Ingress Routing Mesh: A routing layer that allows any node in the cluster to accept connections for any published service port, regardless of whether the specific container is running on that node.
Sysadmin Pro-Tip:
- Scale: Swarm is ideal for small to medium deployments (up to 100 nodes, though it excels in the 3–20 range).
- Quorum: Always use an odd number of managers (3, 5, or 7) to maintain a Raft consensus and prevent “split-brain” scenarios. More than 7 managers can introduce unnecessary latency in state synchronization.
8.2. Cluster Deployment Workflow
Section titled “8.2. Cluster Deployment Workflow”Following these steps ensures a stable, production-ready Swarm initialization.
8.2.1. Network Requirements
Section titled “8.2.1. Network Requirements”Ensure the following ports are open between all participating nodes (firewall/security groups):
- 2377/tcp: Cluster management communications.
- 7946/tcp & udp: Node-to-node control plane communication.
- 4789/udp: Overlay network data traffic (VXLAN).
8.2.2. Initializing the Leader
Section titled “8.2.2. Initializing the Leader”On the primary node (e.g., node1), initialize the cluster:
docker swarm init --advertise-addr <MANAGER-IP>The output will provide a join token for both workers and managers.
8.2.3. Expanding the Cluster
Section titled “8.2.3. Expanding the Cluster”On all other nodes, use the token provided to join the Swarm:
# To join as a workerdocker swarm join --token <WORKER-TOKEN> <MANAGER-IP>:23778.2.4. Managing Node Roles
Section titled “8.2.4. Managing Node Roles”From the leader node, promote workers to managers for high availability:
docker node promote node2 node38.2.5. Image Distribution Strategy
Section titled “8.2.5. Image Distribution Strategy”Since containers can run on any node, images must be available cluster-wide.
- Option A (Recommended): Use a private/public Registry (Docker Hub, ECR, self-hosted Harbor).
- Option B (Air-gapped): Use
docker image saveanddocker image loadto manually sync images across nodes.
8.2.6. Stack Deployment
Section titled “8.2.6. Stack Deployment”Define the infrastructure in a docker-stack.yml file (similar to Compose) and deploy it:
docker stack deploy -c docker-stack.yml my_production_stack8.2.7. Cluster Health Audit
Section titled “8.2.7. Cluster Health Audit”Use these essential commands to verify the operational state:
docker node ls: Lists all nodes and their status/role.docker stack ls: Lists active stacks in the cluster.docker stack services <name>: Checks the status of services within a stack.docker service logs -f <name>: Follows cluster-wide logs for a specific service.docker service inspect --pretty <name>: Shows detailed configuration of a service.
8.3. Practical Lab: Deploying a 5-Node Cluster
Section titled “8.3. Practical Lab: Deploying a 5-Node Cluster”In this scenario, we will implement a fault-tolerant Swarm cluster. To maintain a quorum while distributing the workload, we will configure three nodes as Managers and two nodes as dedicated Workers.
Node Inventory:
| Hostname | IP Address | Role | OS |
|---|---|---|---|
node1 | 192.168.1.201 | Manager (Leader) | Debian 13 / Ubuntu 24.04 |
node2 | 192.168.1.202 | Manager (Reachable) | Debian 13 / Ubuntu 24.04 |
node3 | 192.168.1.203 | Manager (Reachable) | Debian 13 / Ubuntu 24.04 |
node4 | 192.168.1.204 | Worker | Debian 13 / Ubuntu 24.04 |
node5 | 192.168.1.205 | Worker | Debian 13 / Ubuntu 24.04 |
8.3.1. Cluster Initialization
Section titled “8.3.1. Cluster Initialization”Initialize the Swarm on node1. We specify --advertise-addr to ensure the manager explicitly listens on the correct network interface for cluster traffic.
Run on node1:
docker swarm init --advertise-addr 192.168.1.201Execution Output: The daemon will generate a unique join token. All subsequent nodes will join as workers by default using this token:
Swarm initialized: current node (zm22...) is now a manager.
To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-2xqz... 192.168.1.201:23778.3.2. Joining the Cluster
Section titled “8.3.2. Joining the Cluster”Execute the join command provided in the previous step on the remaining four nodes (node2 through node5).
Run on node2, node3, node4, and node5:
docker swarm join --token <WORKER_TOKEN> 192.168.1.201:23778.3.3. Role Promotion
Section titled “8.3.3. Role Promotion”For high availability, we need three managers. We will promote node2 and node3 from their default worker status to managers.
Run on node1:
docker node promote node2 node38.3.4. Post-Deployment Audit
Section titled “8.3.4. Post-Deployment Audit”Verify the cluster topology from any of the manager nodes.
Run on node1, node2, or node3:
docker node lsExpected Output Summary:
- node1: Status: Ready, Availability: Active, Manager Status: Leader
- node2 & node3: Status: Ready, Availability: Active, Manager Status: Reachable
- node4 & node5: Status: Ready, Availability: Active, Manager Status: (Empty/Worker)
8.4. Scenario 1: Deploying a Global Web Service
Section titled “8.4. Scenario 1: Deploying a Global Web Service”In this initial exercise, we will deploy a static web site across the entire cluster. We will use Global Mode, which instructs Swarm to instantiate exactly one container instance on every available node in the cluster.
8.4.1. Image Preparation (Distributed Build)
Section titled “8.4.1. Image Preparation (Distributed Build)”For this example, we will manually build the image on all nodes. In production, this is typically handled by a Central Registry (which we will cover in the next section).
Run on all 5 nodes:
mkdir ~/example1 && cd ~/example1
# Create a sample index pagecat <<EOF > index.html<html><head><title>Swarm Node</title></head><body> <h1>Distributed Web Service</h1> <p>This page is served by Docker Swarm Ingress Mesh.</p></body></html>EOF
# Define the Dockerfilecat <<EOF > DockerfileFROM nginx:alpineCOPY index.html /usr/share/nginx/html/index.htmlEOF
# Build the local imagedocker image build -t web-server:v1 .8.4.2. Defining the Stack Configuration (stack.yaml)
Section titled “8.4.2. Defining the Stack Configuration (stack.yaml)”We will now create the orchestration manifest. This file defines the services, networks, and deployment policies.
Run on node1 (Leader) only:
nano stack.yamlYAML Content:
services: # Service name: This will be the DNS name for internal communication. web-server: # The image to be deployed. Must be available on all nodes (or in a registry). image: web-server:v1
ports: # Port Mapping (Host Port : Container Port) # Thanks to Ingress Routing Mesh, the service is accessible on any node's port 8080. - "8080:80"
networks: # The virtual network this service will join. - web-net
# 'deploy' block contains Swarm-specific configurations. # Note: These settings are ignored by 'docker-compose up'. deploy: # Deployment Mode: # global: Runs exactly one instance on every node in the cluster. # replicated: Runs a specific number of instances distributed across the cluster. mode: global
# Policy for container restarts in case of failure. restart_policy: # condition: on-failure -> Restart only if the container exits with an error. condition: on-failure # How long to wait between restart attempts. delay: 5s # Maximum number of attempts before giving up. max_attempts: 3 # Time window used to decide if a restart was successful. window: 120s
# Configuration for rolling updates (e.g., when changing the image version). update_config: # Number of containers to update at a time. parallelism: 1 # Delay between updating successive container groups. delay: 10s
# Resource Constraints: Prevents a single container from consuming all host resources. resources: limits: # Limit CPU usage to 50% of a single core. cpus: '0.50' # Limit RAM usage to 512 Megabytes. memory: 512M
# Network Definitionsnetworks: web-net: # driver: overlay -> Enables multi-host communication. # Containers on different physical nodes can communicate as if on the same LAN. driver: overlay # attachable: true -> Allows standalone containers (outside this stack) # to manually connect to this network for debugging. attachable: true8.4.3. Stack Deployment & Verification
Section titled “8.4.3. Stack Deployment & Verification”Deploy the stack to the cluster from the manager node.
Run on node1:
docker stack deploy -c stack.yaml example1Audit the Deployment:
# Check service status and replica countsdocker service ls
# Verify task distribution across nodesdocker stack ps example18.4.4. Accessing the Service
Section titled “8.4.4. Accessing the Service”Thanks to the Ingress Routing Mesh, we can access the web service using the IP address of any node in the cluster (even if a specific node were only acting as a routing jump).
http://192.168.1.201:8080/http://192.168.1.202:8080/http://192.168.1.203:8080/http://192.168.1.204:8080/http://192.168.1.205:8080/
8.4.5. Teardown
Section titled “8.4.5. Teardown”To remove the stack and all associated containers/networks:
docker stack rm example18.5. Deploying a Private Local Registry
Section titled “8.5. Deploying a Private Local Registry”Building images manually on every node is inefficient and error-prone. While public registries like Docker Hub are convenient, a Private Local Registry provides lower latency and keeps the proprietary images within the local network.
8.5.1. Configure Insecure Registry Access
Section titled “8.5.1. Configure Insecure Registry Access”By default, Docker requires HTTPS for registry communication. Since our local registry uses a self-signed or HTTP setup, we must explicitly tell the Docker daemon on every node to trust our registry.
Run on all nodes:
sudo nano /etc/docker/daemon.jsonJSON Configuration:
{ "insecure-registries" : ["192.168.1.201:5000"]}Apply changes:
sudo systemctl restart docker8.5.2. Deploy the Registry Service
Section titled “8.5.2. Deploy the Registry Service”We will deploy the registry as a Swarm Service. To ensure the persistent data (the stored images) remains consistent, we use a Node Constraint to keep the service running on node1, where the volume resides.
Run on node1:
# Provision persistent storagedocker volume create local-registry-volume
# Deploy the registry servicedocker service create --name my-registry \ --publish published=5000,target=5000 \ --mount type=volume,source=local-registry-volume,destination=/var/lib/registry \ --constraint 'node.hostname == node1' \ registry:28.5.3. Image Lifecycle: Build, Tag, and Push
Section titled “8.5.3. Image Lifecycle: Build, Tag, and Push”To use the local registry, images must follow a specific naming convention: [REGISTRY-IP]:[PORT]/[IMAGE-NAME]:[TAG].
Run on node1:
mkdir ~/registry-test && cd ~/registry-test
# Simple Dockerfile for verificationcat <<EOF > DockerfileFROM debian:trixieCMD ['bash']EOF# Build and Tag the image for the local registrydocker build -t 192.168.1.201:5000/local-test:v1 .
# Push the image to the local repositorydocker push 192.168.1.201:5000/local-test:v18.5.4. Verification from Other Nodes
Section titled “8.5.4. Verification from Other Nodes”Since the registry is now reachable across the network, any node in the Swarm can pull this image directly:
Run on any other node (e.g., node4):
docker pull 192.168.1.201:5000/local-test:v18.6. Scenario 2: Two-Tier Microservices Architecture
Section titled “8.6. Scenario 2: Two-Tier Microservices Architecture”8.6.1. Architectural Overview
Section titled “8.6.1. Architectural Overview”In this advanced scenario, we will deploy a decoupled infrastructure consisting of a Python Flask Backend and an Nginx Reverse Proxy Frontend.
Key Engineering Objectives:
- Service Discovery: The Frontend will route traffic using the logical service name (
backend-service) instead of volatile IP addresses. - Internal Load Balancing: Swarm will automatically distribute incoming requests from the Frontend across multiple Backend replicas using Virtual IP (VIP).
- Network Segregation: We will implement two distinct overlay networks. The
internal-netwill isolate the Backend from public access, while thepublic-netserves as the entry point. - Workload Placement: We will use Placement Constraints to ensure that application workloads run exclusively on Worker nodes, preserving Manager node resources for cluster orchestration.
8.6.2. Backend Development (Python/Flask)
Section titled “8.6.2. Backend Development (Python/Flask)”This service will identify which node and container is responding, allowing us to verify the load balancing in action.
Build on node1:
# Workspace setupmkdir -p ~/example2/backend && cd ~/example2/backend
# Application logiccat <<EOF > app.pyfrom flask import Flaskimport os, socket
app = Flask(__name__)
@app.route('/')def hello(): # Node name will be injected via environment variables in the stack file node_name = os.getenv('MY_NODE_NAME', 'unknown_node') container_id = socket.gethostname() return f"Response from Backend - Node: {node_name} | Container ID: {container_id}\n"
if __name__ == "__main__": app.run(host='0.0.0.0', port=5000)EOF
# Dockerfile definitioncat <<EOF > DockerfileFROM python:3.14-slimRUN pip install flaskWORKDIR /appCOPY app.py .EXPOSE 5000CMD ["python", "app.py"]EOF
# Build, Tag, and Push to Local Registrydocker image build -t 192.168.1.201:5000/example2-backend:v1 .docker image push 192.168.1.201:5000/example2-backend:v18.6.3. Frontend Development (Nginx Reverse Proxy)
Section titled “8.6.3. Frontend Development (Nginx Reverse Proxy)”The Frontend acts as the gateway. Its configuration points to the Backend service name, which Docker Swarm’s internal DNS resolves to the service’s Virtual IP.
Build on node1:
# Workspace setupmkdir -p ~/example2/frontend && cd ~/example2/frontend
# Custom Nginx Reverse Proxy Configurationcat <<EOF > custom.confserver { listen 80;
location / { # Routing to the logical service name defined in stack.yaml proxy_pass http://backend-service:5000; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; }}EOF
# Dockerfile definitioncat <<EOF > DockerfileFROM nginx:alpineCOPY custom.conf /etc/nginx/conf.d/default.confEOF
# Build, Tag, and Push to Local Registrydocker image build -t 192.168.1.201:5000/example2-frontend:v1 .docker image push 192.168.1.201:5000/example2-frontend:v18.6.4. Orchestration & Deployment (The Stack)
Section titled “8.6.4. Orchestration & Deployment (The Stack)”We will now define the relationship between our services. Note the use of Docker Template Variables to inject host-specific data into the containers.
Run on node1:
cd ~/example2nano stack.yamlYAML Manifest:
services: # BACKEND: Isolated tier running on worker nodes only backend-service: image: 192.168.1.201:5000/example2-backend:v1 environment: # Templating: Automatically injects the host's name into the container MY_NODE_NAME: "{{.Node.Hostname}}" networks: - internal-net deploy: mode: replicated replicas: 2 placement: constraints: # High Availability Rule: Keep application logic away from Manager nodes - "node.role == worker"
update_config: parallelism: 1 delay: 10s # Zero-Downtime Strategy: Spawns new version before terminating the old one order: start-first
# FRONTEND: Gateway tier acting as the cluster entry point frontend-service: image: 192.168.1.201:5000/example2-frontend:v1 ports: - "80:80" networks: - internal-net - public-net deploy: # Ensures high availability by running on every node (Manager & Worker) mode: global
networks: internal-net: driver: overlay attachable: true public-net: driver: overlayDeploying the Stack:
docker stack deploy -c stack.yaml example2Verification & Monitoring: Because of the Ingress Routing Mesh, any node IP will serve the response from the backend through the Nginx frontend. Refreshing the browser should show responses from different Container IDs due to the internal load balancing.
# Check task distribution and placement constraintsdocker stack ps example2
# View overall service statusdocker service ls8.6.5. Versioning & Rolling Updates
Section titled “8.6.5. Versioning & Rolling Updates”In production, updating a service must be handled without interrupting the user experience. Docker Swarm facilitates this through Rolling Updates, where it replaces containers one by one (or in batches) according to a defined strategy.
Code Modification & Versioning We will create a new version of our backend to reflect a logic change.
Run on node1:
cd ~/example2/backend# Modify the app logicsed -i 's/Response from Backend/Updated response (v2) from Backend/g' app.py
# Build and Push the NEW versiondocker image build -t 192.168.1.201:5000/example2-backend:v2 .docker image push 192.168.1.201:5000/example2-backend:v2Implementing the Advanced Update Strategy
We modify the stack.yaml to point to the new image and define how Swarm should handle the transition and potential failures.
Update stack.yaml on node1:
cd ~/example2nano stack.yamlChange as below:
services: backend-service: # We will change this to :v2 for the update test image: 192.168.1.201:5000/example2-backend:v2 environment: MY_NODE_NAME: "{{.Node.Hostname}}" networks: - internal-net deploy: mode: replicated replicas: 2 placement: constraints: - "node.role == worker"
# --- ADVANCED UPDATE STRATEGY --- update_config: # How many containers to update at once parallelism: 1 # Delay between updating each container group delay: 15s # start-first: New container starts before old one stops (Zero downtime) # stop-first: Old container stops before new one starts (Saves resources) order: start-first # What to do if the update fails? (continue, rollback, pause) failure_action: rollback # Maximum failure rate allowed during an update max_failure_ratio: 0.1 # Time to wait after a container starts to consider it "healthy" monitor: 20s
# --- ROLLBACK STRATEGY --- # Defines what happens if the update fails and triggers a rollback rollback_config: parallelism: 2 order: stop-first
frontend-service: image: 192.168.1.201:5000/example2-frontend:v1 ports: - "80:80" networks: - internal-net - public-net deploy: mode: global
networks: internal-net: driver: overlay public-net: driver: overlayExecuting the Zero-Downtime Update
Re-deploying the stack triggers the update. Swarm detects the image change and begins the rolling update based on the parameters.
Run on node1:
docker stack deploy -c stack.yaml example2While the update is in progress, we can watch Swarm killing the old tasks and spawning new ones:
# Watch the rolling transition in real-timewatch docker stack ps example2As we refresh our browser, we will initially see a mix of v1 and v2 responses, eventually transitioning completely to v2 as the rollout completes.
Final Cleanup
Once the verification is complete:
docker stack rm example28.7. Scenario 3: Reverse Proxy with TLS
Section titled “8.7. Scenario 3: Reverse Proxy with TLS”8.7.1. Architectural Design
Section titled “8.7.1. Architectural Design”In this scenario, we move towards a production-hardened infrastructure. We will implement a high-security boundary using Nginx as a TLS-terminating Reverse Proxy.
Key Strategic Objectives:
- Security (Docker Secrets): Use native Swarm Secrets to securely inject TLS certificates and private keys into the Proxy container at runtime. These never touch the disk in an unencrypted state.
- Configuration Management (Docker Configs): Externalize the Nginx configuration. This allows updating the proxy’s behavior without rebuilding the Nginx image.
- Role Separation:
- Proxy Tier: Runs on Manager Nodes (often acting as the cluster edge/gateway).
- Application Tier: Runs on Worker Nodes with multiple replicas for horizontal scaling.
- Network Isolation: The Backend remains completely hidden from the public internet, accessible only via the
proxy-netoverlay network.
8.7.2. Backend Development (Stateless API)
Section titled “8.7.2. Backend Development (Stateless API)”Our backend will be a lightweight Python service designed to report its internal state, allowing us to verify versioning and load distribution.
Build on node1:
# Environment setupmkdir -p ~/example3/backend && cd ~/example3/backend
# Application Logic: Simple HTTP Response with Versioningcat <<EOF > app.pyfrom http.server import BaseHTTPRequestHandler, HTTPServerimport osimport socket
VERSION = os.environ.get("APP_VERSION", "1.0")
class Handler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-type", "text/plain") self.end_headers()
response = f"""Service: Backend APIVersion: {VERSION}Pod/Container: {socket.gethostname()}""" self.wfile.write(response.encode())
if __name__ == "__main__": server = HTTPServer(("0.0.0.0", 8080), Handler) print(f"Server started on port 8080 (v{VERSION})") server.serve_forever()EOF
# Multi-stage optimized Dockerfilecat <<EOF > DockerfileFROM python:3.14-alpine
WORKDIR /appCOPY app.py .
# Metadata and Default VersionENV APP_VERSION=1.0EXPOSE 8080
CMD ["python", "app.py"]EOF
# Build, Tag, and Push to Enterprise Registrydocker image build -t 192.168.1.201:5000/example3-backend:1.0 .docker image push 192.168.1.201:5000/example3-backend:1.08.7.3. Infrastructure Security: Secrets & Configs
Section titled “8.7.3. Infrastructure Security: Secrets & Configs”In this step, we prepare the sensitive credentials and externalize our configuration. Instead of baking these into an image, we inject them into the Swarm control plane.
Generating TLS Assets (Self-Signed)
We will generate a 2048-bit RSA key pair for our proxy. In a production environment, we would replace these with certificates from a CA (like Let’s Encrypt or an internal PKI).
Run on node1:
cd ~/example3# Generate a self-signed certificate valid for 1 yearopenssl req -x509 -nodes -days 365 \ -newkey rsa:2048 \ -keyout domain.key \ -out domain.crt \ -subj "/CN=example3.local"Provisioning Docker Secrets
When we create a secret, Docker Swarm encrypts it and distributes it only to the nodes running the specific service that requires it.
docker secret create tls_cert domain.crtdocker secret create tls_key domain.key
# Audit the secret storedocker secret lsNote: Secrets are mounted as read-only files inside the container at /run/secrets/.
Externalizing Nginx Configuration (Docker Configs) We define our Nginx behavior in a standalone file. This configuration uses the mounted secrets for SSL and defines the upstream load balancing to our backend.
nano nginx.confConfiguration Content:
events { worker_connections 1024; }
http { upstream backend_cluster { # Swarm's internal DNS resolves 'backend' to the Service VIP server backend:8080; }
server { listen 443 ssl; server_name example3.local;
# Direct reference to paths where Docker mounts the secrets ssl_certificate /run/secrets/tls_cert; ssl_certificate_key /run/secrets/tls_key;
# Standard SSL hardening (optional but recommended) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
location / { proxy_pass http://backend_cluster;
# Preserve client context proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }}Registering the Config Object Similar to secrets, Configs allow updating the service’s behavior without rebuilding the Nginx image.
docker config create nginx_conf nginx.conf
# Verify registrationdocker config ls8.7.4. Orchestration: Deploying the Secure Stack
Section titled “8.7.4. Orchestration: Deploying the Secure Stack”The final piece of our puzzle is the stack.yaml file. This manifest bridges our local registry images with the pre-defined Docker Secrets and Configs stored in the Swarm control plane.
Run on node1:
nano stack.yaml############################################################# Swarm Stack: Reverse Proxy + Replicated Backend## Demonstrates:# - overlay networks# - secrets# - configs# - placement constraints# - rolling updates# - restart policies############################################################
services:
########################################################## # Reverse Proxy (TLS Termination) ########################################################## proxy: image: nginx:alpine
ports: # Expose HTTPS externally - "443:443"
networks: - frontend
# Swarm secrets (mounted under /run/secrets/) secrets: - tls_cert - tls_key
# Swarm config (overrides default nginx.conf) configs: - source: nginx_conf target: /etc/nginx/nginx.conf
deploy: replicas: 1
placement: # Run proxy only on manager nodes constraints: - node.role == manager
restart_policy: condition: on-failure delay: 5s max_attempts: 3
update_config: # Rolling update strategy parallelism: 1 delay: 10s order: start-first
########################################################## # Backend Service (Internal Only) ########################################################## backend: image: 192.168.1.201:5000/example3-backend:1.0
networks: - frontend
deploy: replicas: 3
placement: # Run backend only on worker nodes constraints: - node.role == worker
restart_policy: condition: on-failure delay: 5s
update_config: parallelism: 1 delay: 10s order: start-first
############################################################# Overlay Network############################################################networks: frontend: driver: overlay
############################################################# External Secrets############################################################secrets: tls_cert: external: true tls_key: external: true
############################################################# External Configs############################################################configs: nginx_conf: external: trueDeployment Execution
Deploy the stack using the defined manifest. Docker Swarm will fetch the secrets and configs from its internal encrypted store and mount them into the containers.
docker stack deploy -c stack.yaml example3Monitor the deployment until all replicas are in the Running state:
docker stack services example3docker stack ps example3Testing the TLS endpoint:
Use curl -k (to ignore the self-signed certificate warning) and observe the Hostname changing with each request. This confirms that the Nginx proxy is successfully round-robin load balancing across the three backend replicas on the worker nodes.
# Repeat this command to see different Hostnames (Container IDs)curl -k https://192.168.1.201If we run repeatedly, we can see that hostname changes.
Accessing via Browser
we can point our browser to any node IP in the cluster (e.g., https://192.168.1.204). The Ingress Routing Mesh will intercept the traffic on port 443 and route it to the proxy service running on the Manager node, which in turn proxies it to a backend task on a Worker node.
8.7.5. Executing the Rolling Update (v1.0 to v2.0)
Section titled “8.7.5. Executing the Rolling Update (v1.0 to v2.0)”In this phase, we will perform a Zero-Downtime update of our backend. Thanks to our start-first configuration, Docker Swarm will ensure the new version is healthy before deactivating the old one.
Rebuilding the Backend Image We will increment the versioning inside the Dockerfile to track the rollout.
Run on node1:
cd ~/example3/backend# Update the environment variable for visibilitysed -i 's/APP_VERSION=1.0/APP_VERSION=2.0/g' Dockerfile
# Build and Push the v2.0 image to our local registrydocker image build -t 192.168.1.201:5000/example3-backend:2.0 .docker image push 192.168.1.201:5000/example3-backend:2.0Update image definition in stack file too:
Change the line:
image: 192.168.1.201:5000/example3-backend:1.0to:
image: 192.168.1.201:5000/example3-backend:2.0cd ~/example3nano stack.yamlThe final state of the file will be as below:
############################################################# Swarm Stack: Reverse Proxy + Replicated Backend## Demonstrates:# - overlay networks# - secrets# - configs# - placement constraints# - rolling updates# - restart policies############################################################
services:
########################################################## # Reverse Proxy (TLS Termination) ########################################################## proxy: image: nginx:alpine
ports: # Expose HTTPS externally - "443:443"
networks: - frontend
# Swarm secrets (mounted under /run/secrets/) secrets: - tls_cert - tls_key
# Swarm config (overrides default nginx.conf) configs: - source: nginx_conf target: /etc/nginx/nginx.conf
deploy: replicas: 1
placement: # Run proxy only on manager nodes constraints: - node.role == manager
restart_policy: condition: on-failure delay: 5s max_attempts: 3
update_config: # Rolling update strategy parallelism: 1 delay: 10s order: start-first
########################################################## # Backend Service (Internal Only) ########################################################## backend: image: 192.168.1.201:5000/example3-backend:2.0
networks: - frontend
deploy: replicas: 3
placement: # Run backend only on worker nodes constraints: - node.role == worker
restart_policy: condition: on-failure delay: 5s
update_config: parallelism: 1 delay: 10s order: start-first
############################################################# Overlay Network############################################################networks: frontend: driver: overlay
############################################################# External Secrets############################################################secrets: tls_cert: external: true tls_key: external: true
############################################################# External Configs############################################################configs: nginx_conf: external: trueTriggering the Deployment
Apply the updated manifest. Swarm will detect the image change and begin the rolling update according to the parallelism and delay rules we defined.
docker stack deploy -c stack.yaml example3Monitoring the Rollout Strategy
We can monitor the state of the tasks during the transition.
# Watch the transition in real-time (Ctrl+C to exit)watch docker service ps example3_backend
# Audit the update policy and status in detaildocker service inspect example3_backend --prettyVerification via Ingress Mesh
While the update is running, we can hit any node in the cluster. we will notice a period where both Version: 1.0 and Version: 2.0 responses appear as the load balancer routes traffic to old and new containers simultaneously.
# Test from terminalcurl -k https://192.168.1.201Expected Outcome: Eventually, all responses will show Backend Version: 2.0. The TLS proxy (Nginx) continues to run without interruption on the Manager node throughout this process.
8.7.6. Secret Rotation: Updating TLS Certificates
Section titled “8.7.6. Secret Rotation: Updating TLS Certificates”Docker Secrets are immutable; once created, they cannot be updated or overwritten. To rotate a certificate or password, we must create a new secret and update the service to point to the new resource.
We will use a mapping technique to ensure the container sees the new secret at the same file path, avoiding the need to modify our Nginx configuration file.
Generate the New TLS Certificate (v2) Run on node1:
cd ~/example3# Create a fresh certificate for the next yearopenssl req -x509 -nodes -days 365 \ -newkey rsa:2048 \ -keyout domain_v2.key \ -out domain_v2.crt \ -subj "/CN=example3.local"Register New Secrets in Swarm
docker secret create tls_cert_v2 domain_v2.crtdocker secret create tls_key_v2 domain_v2.key
# Verify the new objects coexist with the old onesdocker secret ls3. Update the Stack Manifest with Target Mapping
The secret to a smooth rotation is the source and target definition. We change the source to the new version, but keep the target as tls_cert so Nginx finds it at /run/secrets/tls_cert.
Edit stack.yaml on node1:
nano stack.yamlThe parts to change:
services: proxy: # ... (other settings) secrets: # Use the new version but mount it with the original filename - source: tls_cert_v2 target: tls_cert - source: tls_key_v2 target: tls_key
# ... (backend service remains v2.0)
# Declare the new external secretssecrets: tls_cert_v2: external: true tls_key_v2: external: trueAnd the final state of the file:
############################################################# Swarm Stack: Reverse Proxy + Replicated Backend## Final State:# - Backend version 2.0# - TLS secrets rotated (v2)# - Rolling update strategy enabled# - Placement constraints enforced############################################################
services:
########################################################## # Reverse Proxy (TLS Termination) ########################################################## proxy: image: nginx:alpine
ports: - "443:443" # Expose HTTPS externally
networks: - frontend
# Secrets mounted under /run/secrets/ secrets: # Rotate source secret but keep target filename stable - source: tls_cert_v2 target: tls_cert - source: tls_key_v2 target: tls_key
# Override default nginx configuration configs: - source: nginx_conf target: /etc/nginx/nginx.conf
deploy: replicas: 1
placement: constraints: - node.role == manager
restart_policy: condition: on-failure delay: 5s max_attempts: 3
update_config: parallelism: 1 delay: 10s order: start-first
########################################################## # Backend Service (Internal Only) ########################################################## backend: image: 192.168.1.201:5000/example3-backend:2.0
networks: - frontend
deploy: replicas: 3
placement: constraints: - node.role == worker
restart_policy: condition: on-failure delay: 5s
update_config: parallelism: 1 delay: 10s order: start-first
############################################################# Overlay Network############################################################networks: frontend: driver: overlay
############################################################# External Secrets (Must Already Exist in Swarm)############################################################secrets: tls_cert_v2: external: true tls_key_v2: external: true
############################################################# External Configs (Must Already Exist in Swarm)############################################################configs: nginx_conf: external: trueExecute the Rotation
Apply the stack. Swarm will perform a rolling update of the proxy service, unmounting the old secrets and mounting the new v2 versions.
docker stack deploy -c stack.yaml example3Post-Rotation Cleanup
Once the update is verified and the old containers are gone, we can safely decommission the old secret objects from the cluster database.
docker secret rm tls_cert tls_keyStack Cleanup
After finishing our test, we can remove the full stack.
docker stack rm example38.8. Docker Swarm stack.yml Cheat Sheet
Section titled “8.8. Docker Swarm stack.yml Cheat Sheet”Basic Structure:
services: service_name: image: image:tag ports: - "published:target" networks: - network_name volumes: - volume_name:/path environment: - KEY=value secrets: - secret_name configs: - config_name deploy: replicas: 1 placement: constraints: - node.role == worker restart_policy: condition: on-failure update_config: parallelism: 1 delay: 10s order: start-first
networks: network_name: driver: overlay
volumes: volume_name:
secrets: secret_name: external: true
configs: config_name: external: trueFrequently Used deploy Options
Replication:
deploy: replicas: 3Global Mode (run on every node):
deploy: mode: globalPlacement Constraints:
deploy: placement: constraints: - node.role == worker - node.hostname == node3 - node.labels.region == euTo add a label:
docker node update --label-add region=eu node3
Restart Policy:
restart_policy: condition: on-failure delay: 5s max_attempts: 3Rolling Update Strategy:
update_config: parallelism: 1 delay: 10s order: start-firstExplanations:
parallelism: how many tasks to update simultaneouslydelay: wait time between batchesorder:start-first→ minimal downtimestop-first→ resource conservative
Secrets Usage
Simple
secrets: - my_secretMounted at:
/run/secrets/my_secretAdvanced (rename target inside container):
secrets: - source: db_password_v2 target: db_passwordSecrets are immutable. Rotation = new secret + service update.
Configs:
configs: - source: nginx_conf target: /etc/nginx/nginx.confUnlike secrets; Not encrypted at rest, Intended for non-sensitive configuration
Networking Patterns
Internal Service (not exposed):
networks: - backend_netNo ports: → not reachable from host.
Public Service:
ports: - "443:443"Volumes (Persistent Storage):
volumes: - db_data:/var/lib/mysqlDeclare at bottom:
volumes: db_data:Common Patterns
Reverse Proxy + Backend:
- Proxy publishes ports
- Backend internal only
- Shared overlay network
Database + App
- DB secret for password
- App reads secret via
_FILE - Volume for DB persistence
Useful Commands for Stack Debugging
Deploy:
docker stack deploy -c stack.yaml mystackList services:
docker stack services mystackWatch tasks:
docker service ps mystack_serviceInspect service:
docker service inspect mystack_service --prettyRemove stack:
docker stack rm mystack9. Maintenance & Troubleshooting
Section titled “9. Maintenance & Troubleshooting”As a system admin, our job starts after the deployment. Keeping the host healthy and knowing how to drain traffic is crucial.
9.1. Log Rotation (Preventing Disk Exhaustion)
Section titled “9.1. Log Rotation (Preventing Disk Exhaustion)”By default, Docker captures the stdout/stderr of all containers and stores them in JSON files. Without limits, these files can grow indefinitely and fill the /var partition.
Best Practice: Configure global limits in /etc/docker/daemon.json on all nodes.
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }}This ensures no container uses more than 30MB (3x10MB) of log space.
9.2. Node Maintenance (Drain Mode)
Section titled “9.2. Node Maintenance (Drain Mode)”When we need to perform maintenance on a physical server (e.g., kernel update, hardware upgrade), we shouldn’t just shut it down. we must first tell Swarm to migrate the tasks to other nodes.
Set node to Drain mode:
docker node update --availability drain node4Swarm will immediately stop tasks on node4 and reschedule them on other active nodes.
Perform maintenance & Reboot.
Set node back to Active mode:
docker node update --availability active node4Note: Swarm won’t automatically move old tasks back. New tasks or service updates will now consider this node again.
9.3. Monitoring & Resource Auditing
Section titled “9.3. Monitoring & Resource Auditing”When a service is slow, use these tools to identify the bottleneck:
docker stats: Real-time stream of CPU, memory, and network usage for all containers on the local host.docker service ps --filter "desired-state=shutdown" <service>: Helps finding why a service is constantly restarting (Crash looping).docker service inspect --pretty <service>: Check if a service is hitting itsresources.limits. If a container hits its memory limit, Docker will OOM-Kill (Out Of Memory) it.
9.4. Troubleshooting Workflow
Section titled “9.4. Troubleshooting Workflow”If a service in Swarm isn’t working:
- Check Service Status:
docker service ls(Look for0/3replicas). - Locate the Error:
docker service ps <service_name>(Look for theERRORcolumn). - Inspect Logs:
docker service logs -f <service_name>. - Check Daemon Health:
docker system events --since 30m(See what the engine has been doing recently).