Production Orchestration with Docker Swarm

Deploy a multi-service stack on Docker Swarm with rolling updates and secrets.

Introduction

30 Second Summary

Shipping software used to mean crossing your fingers and hoping it worked on someone else's machine. Containers solved that problem by packaging applications with everything they need to run anywhere.

In this series, you will master Docker through 6 hands-on projects. You will go from installing Docker Engine on Ubuntu to orchestrating production services with Docker Swarm.

What You'll Build

A complete Docker portfolio covering container fundamentals, multi-container orchestration, image optimization, CI/CD automation, and production deployment.

By the end of this series, you'll have:

  • Docker Engine installed on Ubuntu with hands-on experience building, tagging, and pushing container images to Docker Hub.
  • Multi-container application stacks orchestrated with Docker Compose, featuring optimized and security-hardened images.
  • An automated CI/CD pipeline using GitHub Actions and a Docker Swarm cluster with rolling updates, secrets, and health checks.
  • A Secret Mission in every project that challenges you to go beyond the basics, from persistent database volumes to multi-platform image builds.

Are there any prerequisites?

You need an Ubuntu Linux machine (22.04, 24.04, or 26.04), basic comfort with the terminal, and a basic understanding of what a web application is. No prior Docker experience is required.

Install Docker Engine on Ubuntu

Before you can run containers, you need Docker Engine installed on your Ubuntu machine.

Docker Engine is the core runtime that builds and runs containers on Linux. In this step, you will install it from Docker's official apt repository to get the latest stable version with automatic updates.

In this step, get ready to:
  • Remove any conflicting Docker packages from your system.
  • Set up Docker's official apt repository with a GPG key.
  • Install Docker Engine and its companion packages.
Remove conflicting packages

Ubuntu may come with unofficial Docker-related packages like docker.io or podman-docker. These conflict with the official Docker Engine packages and need to be removed first.

  • Remove any conflicting packages by running this command:
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)

What does this command do?

This command checks which of the listed unofficial packages are installed on your system and removes them. If none are installed, apt reports that there is nothing to remove. That is perfectly normal.

Seeing an error from dpkg?

If apt says no packages matched or there is nothing to remove, that means nothing conflicting was installed. You can safely move on to the next substep.

Help me troubleshoot removing conflicting Docker packages.

Set up Docker's official apt repository

Docker publishes its packages in a dedicated repository. To use it, you need to add Docker's GPG signing key and register the repository as a package source on your system.

  • Install prerequisite packages and download Docker's GPG key by running these commands:
# Install packages needed to download securely over HTTPS
sudo apt update
sudo apt install ca-certificates curl

# Create the keyrings directory and download Docker's GPG key
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

What does this code do?

  • sudo apt update refreshes your system's package list so it knows what is available to install.
  • ca-certificates and curl let your system securely download files from the internet over HTTPS.
  • The last three lines create a directory for signing keys and download Docker's official GPG key into it. This key lets apt verify that packages actually come from Docker.

The terminal may ask you to confirm the installation of ca-certificates and curl.

  • If prompted, type Y and press Enter to confirm.
  • Add Docker's repository to your system's package sources by running this command:
# Add Docker's official repository as a package source
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

# Refresh the package list to include Docker's new repository
sudo apt update

What does this code do?

  • The sudo tee block creates a new file at /etc/apt/sources.list.d/docker.sources that tells apt where to find Docker's packages.
  • The Suites line automatically detects your Ubuntu version codename (like noble or jammy) so the correct packages are fetched for your system.
  • The final sudo apt update refreshes the package list again, this time pulling in packages from Docker's repository.
  • Verify the repository was added by checking for Docker packages:
apt list docker-ce

You should see at least one docker-ce package version listed. This confirms your system can now find Docker's official packages.

Not seeing any docker-ce packages listed?

  • Make sure you ran sudo apt update after adding the repository source file.
  • Check that the GPG key file exists by running ls /etc/apt/keyrings/docker.asc. If it is missing, re-run the GPG key commands from the previous block.
  • Verify your Ubuntu version is supported (22.04, 24.04, 25.10, or 26.04) by running lsb_release -a.

Help me troubleshoot my Docker apt repository setup.

Install Docker Engine packages

With the repository configured, you can now install Docker Engine and its companion tools.

  • Install all the Docker Engine packages by running this command:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

What are all these packages?

  • docker-ce is Docker Engine itself, the daemon that manages containers.
  • docker-ce-cli is the command-line tool you use to interact with Docker.
  • containerd.io is the low-level container runtime that Docker Engine uses under the hood.
  • docker-buildx-plugin adds enhanced image building capabilities.
  • docker-compose-plugin lets you define and run multi-container apps with Docker Compose. You will use this in a later project.
  • When prompted, type Y and press Enter to confirm the installation.

The installation may take a minute or two depending on your internet speed.

  • Verify that Docker Engine is running by checking its service status:
sudo systemctl status docker

You should see active (running) highlighted in green. This confirms Docker Engine is installed and running on your machine.

  • Press q to exit the status view and return to your terminal prompt.

Docker is not showing as active?

If the status shows inactive or dead, start Docker manually by running:

sudo systemctl start docker

Then run sudo systemctl status docker again to confirm it shows active (running).

Help me get Docker Engine running on Ubuntu.

Docker Engine is installed and running. Next up, you will configure Docker so you can use it without sudo and run your very first container.

Configure Docker and Run Your First Container

Docker Engine is installed on your Ubuntu machine. Right now, every docker command requires sudo because only the root user can communicate with the Docker daemon.

In this step, you'll fix that by adding yourself to the docker group. Then you'll explore what Docker tells you about itself and run your first interactive container to experience isolation firsthand.

In this step, get ready to:
  • Add your user to the docker group for non-root access.
  • Explore Docker Engine details with version and info commands.
  • Run an interactive Ubuntu container and explore its isolated filesystem.
Add your user to the docker group

The Docker daemon listens on a Unix socket that only the root user can access by default. The docker group grants your user permission to communicate with that socket directly.

  • Add your user to the docker group by running this command:
sudo usermod -aG docker $USER

What does this command do?

  • usermod -aG docker appends (-a) your user to the docker group (-G) without removing you from any existing groups.
  • $USER is an environment variable that automatically resolves to your current username.

The group change doesn't take effect in your current terminal session until you refresh it.

  • Activate the new group membership in your current session by running:
newgrp docker

What does newgrp do?

newgrp docker starts a new shell session where the docker group is already active. This saves you from having to log out and back in.

  • Verify that Docker works without sudo by running:
docker run hello-world

You should see a message that starts with Hello from Docker! followed by an explanation of the steps Docker just performed to run this container.

What just happened?

Docker pulled the hello-world image from Docker Hub, created a container from it, ran the program inside, and then the container exited. All of that happened without sudo.

Seeing permission denied?

  • Make sure you ran newgrp docker after the usermod command. Without it, your current session doesn't know about the new group yet.
  • If newgrp doesn't help, try logging out of your Ubuntu session completely and logging back in.
  • Confirm your user is in the docker group by running groups. You should see docker in the list.

Help me fix this Docker permission denied error

Explore Docker system information

Docker provides built-in commands that tell you exactly what version you're running and how your installation is configured. This is useful for debugging and for confirming your setup is correct.

  • Check your Docker client and server versions by running:
docker version

What does this output show?

The output is split into two sections. Client is the Docker CLI that you type commands into. Server is the Docker Engine (the daemon) that actually builds and runs containers. They communicate over that Unix socket you just granted your user access to.

  • Get a more detailed view of your Docker installation by running:
docker info

What does docker info show?

This command gives you a comprehensive report on your Docker installation. Look for these key details:

  • Containers shows how many containers exist on your system (running, paused, and stopped).
  • Images shows how many images have been pulled or built locally.
  • Storage Driver tells you how Docker stores image layers on disk.
Run an interactive Ubuntu container

Running a container in interactive mode gives you a live shell inside an isolated environment. It's like opening a terminal on a separate machine, except the "machine" is a lightweight container sharing your host's kernel.

  • Pull and start an Ubuntu container with an interactive bash shell by running:
docker run -it ubuntu bash

What do these flags mean?

  • -i keeps STDIN open so you can type commands into the container.
  • -t allocates a pseudo-terminal so the shell behaves like a normal terminal session.
  • ubuntu is the image to run. Docker pulls it from Docker Hub if it's not already on your machine.
  • bash is the command to execute inside the container.

Your terminal prompt changes to something like root@a1b2c3d4e5f6:/#. You are now inside the container. The random string after @ is the container's ID, which Docker uses as the container's hostname.

  • Explore the container's isolated filesystem by running these commands one at a time:
ls /
hostname
cat /etc/os-release

What should I see?

  • ls / shows the container's own root filesystem. It has standard Linux directories like bin, etc, and usr, but these are completely separate from your host machine's files.
  • hostname prints the container's ID. This confirms the container has its own network identity.
  • cat /etc/os-release shows Ubuntu version details. Even if your host runs a different Ubuntu version, the container has its own.
  • Leave the container by running:
exit

Your prompt returns to your normal host terminal. The container stops as soon as the bash process inside it ends.

  • Confirm the container has stopped by running:
docker ps -a

You should see two containers listed. The hello-world container from earlier and the ubuntu container you just exited. Both show a status of Exited.

Why docker ps -a instead of docker ps?

docker ps only shows running containers. The -a flag includes stopped containers too. Since both of your containers have exited, docker ps alone would show an empty list.

Don't see the ubuntu container in the list?

  • Make sure you included the -a flag. Without it, stopped containers are hidden.
  • If the container image failed to pull, check your internet connection and try running docker run -it ubuntu bash again.

Help me troubleshoot my docker run command

You've configured Docker for daily use and experienced container isolation firsthand. Next up, you'll learn to manage the full container lifecycle with commands to run, inspect, and control long-running containers.

Manage Container Lifecycle

In the last step, you ran an interactive container and explored an Ubuntu filesystem from the inside. That container stopped the moment you typed exit.

Real-world containers behave differently. Web servers, databases, and APIs need to keep running in the background without tying up your terminal. In this step, you'll learn how Docker runs long-lived containers and how to manage their full lifecycle.

In this step, get ready to:
  • Run an Nginx container in detached (background) mode.
  • Inspect a running container with logs and metadata commands.
  • Execute commands inside a running container, then stop and remove it.
Run a detached Nginx container

When you ran the Ubuntu container in Step 2, your terminal was locked to it. Nginx is a web server that needs to keep running in the background, just like a real production service. Docker's -d flag starts a container in detached mode, which frees up your terminal while the container keeps running.

  • Start an Nginx container in detached mode by running this command:
docker run -d --name my-nginx nginx

Docker automatically downloads the nginx image if it doesn't exist on your machine yet. After pulling, Docker prints a long container ID and returns you to your terminal prompt.

What do these flags do?

  • -d runs the container in detached mode. Instead of attaching your terminal to the container's output, Docker runs it in the background.
  • --name my-nginx assigns a friendly name to the container. Without this, Docker generates a random name like quirky_darwin. Named containers are much easier to manage.
  • Confirm that the container is running by running this command:
docker ps

You should see a table with one row showing your my-nginx container. The STATUS column reads Up followed by how long the container has been running.

Seeing a name conflict error?

If you see Conflict. The container name "/my-nginx" is already in use, it means a container with that name already exists from a previous attempt. Remove it first by running docker rm -f my-nginx, then try the docker run command again.

Still stuck? Help me fix this container name conflict.

Inspect the running container

Your Nginx container is now running silently in the background. Docker provides several commands to check on it without interrupting it.

  • View the container's log output by running this command:
docker logs my-nginx

You should see Nginx startup messages showing the server version and a line like start worker processes. These logs capture everything the container prints to standard output, which is how you debug containers that run in the background.

  • Get detailed metadata about the container by running this command:
docker inspect my-nginx

What does docker inspect show?

This command returns a large JSON object with everything Docker knows about the container. You don't need to read all of it right now. Here are the key sections to notice:

  • State tells you whether the container is running, paused, or exited.
  • NetworkSettings.IPAddress shows the container's internal IP address on Docker's network.
  • Config shows the image, environment variables, and startup command the container is using.

Seeing "No such container"?

If any of these commands return No such container: my-nginx, the container may have stopped or the name could be misspelled. Run docker ps -a to list all containers, including stopped ones, and check the name.

Need a hand? Help me troubleshoot my missing container.

Execute commands inside the container and clean up

Sometimes you need to get inside a running container to check its files or debug a problem. The docker exec command lets you run a new process inside a container that's already running.

  • Open a bash shell inside the running container by running this command:
docker exec -it my-nginx bash

How is this different from docker run -it?

In Step 2, you used docker run -it to start a brand new container with an interactive shell. docker exec -it is different. It opens a shell inside a container that's already running. The container keeps doing its job (serving Nginx) while you poke around.

Your terminal prompt changes to something like root@a1b2c3d4e5f6:/#. You're now inside the Nginx container.

  • List the default web page files inside the container by running:
ls /usr/share/nginx/html/

You should see two files: 50x.html and index.html. These are the default pages Nginx serves to visitors.

  • View the default welcome page by running:
cat /usr/share/nginx/html/index.html

You should see the HTML source for the "Welcome to nginx!" page. In the next step, you'll replace this file with your own custom page.

  • Leave the container shell by running:
exit

You're back on your host machine. The container is still running in the background.

Now it's time to learn the other half of the container lifecycle: stopping and cleaning up.

  • Stop the running container by running this command:
docker stop my-nginx

What does stopping a container do?

Docker sends a graceful shutdown signal to the container's main process. If the process doesn't stop within 10 seconds, Docker forces it to stop. A stopped container still exists on disk. It just isn't running.

  • Verify the container has stopped by running:
docker ps -a

The -a flag shows all containers, including stopped ones. You should see my-nginx listed with a STATUS of Exited.

  • Remove the stopped container entirely by running this command:
docker rm my-nginx
  • Confirm the container is completely gone by running:
docker ps -a

The my-nginx container should no longer appear in the list. It has been fully removed from your system, freeing the name for future use.

Getting a "removal in progress" or "is running" error?

You can only remove a stopped container. If you skipped the docker stop step, run it first, then try docker rm again. Alternatively, docker rm -f my-nginx force-removes a container even while it's running.

Still stuck? Help me remove a container that won't go away.

You just managed the full lifecycle of a container: run, inspect, interact, stop, and remove. Next up, you'll run Nginx again with port mapping so you can actually see the web page in a browser.

Port Mapping and Serving a Custom Page

In the previous step, you ran containers, inspected their logs, and managed their lifecycle. Those containers were running in complete isolation from your host machine.

Containers have their own internal network by default. To reach a web server running inside a container, you need port mapping. In this step, you'll map a port on your machine to a port inside an Nginx container and serve your own custom web page.

In this step, get ready to:
  • Run an Nginx container with port mapping.
  • Access the web server from your host machine.
  • Serve a custom HTML page from the container.
Run Nginx with port mapping

Nginx listens on port 80 inside its container, but that port is invisible to your host machine. Port mapping creates a tunnel from a port you choose on your host to port 80 inside the container.

  • Start an Nginx container with port mapping by running this command:
docker run -d --name web -p 8080:80 nginx

What does this command do?

  • -d runs the container in detached mode (in the background).
  • --name web gives the container the name web so you can reference it easily.
  • -p 8080:80 maps port 8080 on your host machine to port 80 inside the container. The format is host:container.
  • nginx is the image to run.
  • Confirm the container is running by running this command:
docker ps

You should see a container named web with 0.0.0.0:8080->80/tcp in the PORTS column.

Seeing a port conflict error?

If you see an error like port is already allocated, another process is using port 8080. Try a different host port instead, like -p 8081:80. Remember to use that port in all following steps.

If you see an error about the name web already being in use, remove the old container first with docker rm -f web and try again.

Still stuck? Help me troubleshoot my docker run port mapping error.

Access the default Nginx page

Your Nginx container is now serving its default welcome page. Thanks to port mapping, you can reach it from your host machine through port 8080.

  • Access the Nginx welcome page by running this command:
curl http://localhost:8080

You should see a block of HTML in your terminal output. Look for Welcome to nginx! in the response.

How does port mapping work?

When you visit localhost:8080, your machine forwards that request through the tunnel you created with -p 8080:80. Docker routes it to port 80 inside the web container, where Nginx is listening.

Without the -p flag, the container's port 80 would be completely unreachable from your host. Port mapping is how you expose any containerized service to the outside world.

Not seeing the welcome page?

Make sure the container is still running with docker ps. If it's not listed, restart it with the docker run command from the previous substep.

Check that you're using the correct port number (8080, not 80). If you changed the port earlier due to a conflict, use that port instead.

Need more help? Help me troubleshoot why curl localhost:8080 is not returning the Nginx welcome page.

Serve a custom page and clean up

The default Nginx page confirms port mapping works. Now you'll replace it with your own custom HTML page using docker cp, a command that copies files between your host and a running container.

  • Create a custom HTML file by running this command:
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>My Docker Page</title>
</head>
<body>
    <h1>Hello from Docker!</h1>
    <p>This page is served by Nginx running inside a container.</p>
</body>
</html>
EOF

What does this command do?

This uses a heredoc (<< 'EOF') to create a file called index.html with everything between the two EOF markers as its content. It's a quick way to create a file directly from the terminal without opening a text editor.

  • Confirm the file was created by running:
ls index.html

You should see index.html in the output.

  • Copy your custom page into the running container by running this command:
docker cp index.html web:/usr/share/nginx/html/index.html

What does docker cp do?

docker cp copies files between your host machine and a container. Here, it replaces the default Nginx welcome page with your custom index.html.

The path /usr/share/nginx/html/ is where Nginx looks for files to serve. By placing your file there, Nginx will serve your custom page on the next request.

  • Verify your custom page is being served by running:
curl http://localhost:8080

You should see Hello from Docker! in the output instead of the default Nginx welcome page. Your custom HTML is now being served from inside the container.

Still seeing the default Nginx page?

Make sure the destination path is exactly /usr/share/nginx/html/index.html. A typo in the path means your file was copied to the wrong location.

You can check if the file landed correctly by running:

docker exec web cat /usr/share/nginx/html/index.html

This prints the contents of the file inside the container. You should see your custom HTML. Help me figure out why my custom page is not showing up after running docker cp.

Now that you've confirmed the custom page is working, it's time to clean up the container.

  • Remove the web container by running this command:
docker rm -f web

What does the -f flag do here?

The -f flag forces removal of a running container. Without it, you'd need to stop the container first with docker stop and then remove it with docker rm. The -f flag combines both steps into one.

  • Confirm no containers remain by running:
docker ps -a

You should see an empty list with no containers. Everything is cleaned up.

Still seeing containers listed?

If you see leftover containers from earlier steps, remove them all at once by running:

docker rm -f $(docker ps -aq)

This grabs the IDs of all containers (running or stopped) and force-removes them. Help me clean up leftover Docker containers.

Secret mission

Run a PostgreSQL Container with Persistent Data

Containers are ephemeral by default. When you remove one, all the data inside vanishes. In this mission, you will use a Docker volume to make a PostgreSQL database survive container deletion.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on your local machine using Docker Swarm, so there are no ongoing cloud costs.

Resources you used:

  • Docker Swarm cluster (single-node).
  • Swarm services (web, API, database replicas).
  • Docker stack (myapp).
  • Docker secrets (database credentials).
  • Docker images built and pulled during the project.
  • Overlay networks created by the stack.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your Swarm cluster and services.

  • Your Swarm cluster, stack, and services will continue running in the background.
  • Docker services consume memory and CPU on your machine, so keep an eye on system performance.

Pause - I'll come back to this later

Remove the stack to free up memory and CPU, but keep the Swarm initialized so you can redeploy quickly later.

  • Remove the stack by running:
docker stack rm myapp
  • Your Swarm cluster stays initialized. When you are ready to come back, just redeploy with docker stack deploy.
  • Your Docker images and Compose files remain on your machine for quick redeployment.

Delete - I don't want to use this again

Remove all project resources and start fresh. This tears down everything created during the project.

  • Remove the stack:
docker stack rm myapp
  • Leave the Swarm:
docker swarm leave --force
  • Clean up all unused images, containers, and networks:
docker system prune -a
  • Confirm the prune when prompted. This removes all stopped containers, unused networks, and all images not currently in use.
  • Delete your project directory if you no longer need the source files.

Mission accomplished

Congratulations, You've Completed Docker Mastery

You did it. You've gone from installing Docker Engine for the first time all the way to deploying a production-grade, multi-service stack on Docker Swarm with rolling updates, secrets, and health checks. That's six projects covering every major Docker concept that shows up in DevOps job descriptions and interviews.

In this final project, you learned how to:

  • Initialized a Docker Swarm cluster and deployed a multi-service stack with docker stack deploy.
  • Configured rolling updates for zero-downtime deployments and scaled services up and down with a single command.
  • Secured database credentials with Docker secrets and added health checks for production reliability.
  • Implemented blue-green deployments for safe, zero-risk releases.

Ready to quiz yourself?