Containerize and Network a Web App

Build and network Docker containers with DNS discovery and network isolation.

Introduction

30 Second Summary

Every time you visit a website, your request hits a container running somewhere on a network. Understanding how those containers talk to each other is the foundation of modern infrastructure security.

In this project, you will build a web application inside a Docker container, publish it to your browser, and then wire up a second container that can find the first one by name over an isolated network.

What You'll Build

You will open your browser, see a web page served from inside a container you built from scratch, then hop into a second container and prove it can reach the first one by name alone.

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

  • A custom Nginx container serving your own HTML page that you can open in a browser and show to anyone on your machine.
  • A user-defined bridge network where containers discover each other automatically through DNS resolution, not hardcoded IP addresses.
  • Hands-on network troubleshooting experience using dig, ping, and curl from inside a diagnostic container to prove connectivity between services.
  • Secret Mission: Lock down a backend container using an internal network so it has zero access to the outside internet.

Are there any prerequisites?

You need basic familiarity with the Linux terminal (cd, ls, cat, editing files). No Docker experience is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for a networks and security role.

Install Docker Engine and Verify

Before you can build or run any containers, you need the Docker runtime installed on your machine.

In this step, you'll set up Docker's official package repository, install Docker Engine, and configure your user so you can run Docker commands without sudo.

In this step, get ready to:
  • Set up Docker's apt repository and install Docker Engine packages.
  • Configure your user to run Docker without sudo.
  • Run the hello-world container to confirm everything works.
Set up Docker's apt repository

Docker Engine is not included in Ubuntu's default package list. You need to add Docker's official repository so your system knows where to download the packages from.

  • Update your package index and install the tools needed to add the repository securely by running these commands:
# Update the package index
sudo apt update

# Install tools for adding repos over HTTPS
sudo apt install ca-certificates curl

What does this command do?

ca-certificates lets your system verify HTTPS connections. curl is used to download Docker's GPG key in the next command.

  • The installer will ask you to confirm. Type y and press Enter when prompted.
  • Add Docker's official GPG key and repository to your system by running these commands:
# Create the keyrings directory
sudo install -m 0755 -d /etc/apt/keyrings

# Download Docker's official GPG key
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

# Add the Docker repository to your apt sources
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 index with the new repo
sudo apt update

What does this command do?

The GPG key lets your system verify that the Docker packages you download are genuine and haven't been tampered with.

The tee command writes the repository configuration file that tells apt where to find Docker's packages. The final sudo apt update refreshes the package list so Docker's packages are now available.

  • You should see output that includes a line referencing download.docker.com in the apt update output. This confirms the repository was added successfully.

Seeing a GPG error or 404?

If you see a GPG error, make sure the curl command completed successfully. Run it again and check that /etc/apt/keyrings/docker.asc exists.

If you get a 404 error on apt update, your Ubuntu version codename may not be detected correctly. Run cat /etc/os-release to check your version.

Help me troubleshoot my Docker apt repository setup

Install Docker Engine packages
  • Install Docker Engine and its companion tools by running:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

What are these packages?

  • docker-ce is the Docker Engine itself (the daemon that runs containers).
  • docker-ce-cli is the docker command you'll type in your terminal.
  • containerd.io is the low-level container runtime that Docker uses under the hood.
  • Type y and press Enter when prompted to confirm the installation.
  • Wait for the installation to finish. You'll see the terminal return to a fresh prompt when it's done.
Configure your user to run Docker without sudo

By default, Docker commands require sudo (root privileges). Adding your user to the docker group lets you run Docker commands directly.

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

What does this command do?

usermod -aG docker $USER appends (-a) your current user to the docker group (-G). Members of this group can communicate with the Docker daemon without needing sudo.

  • Apply the new group membership to your current terminal session by running:
newgrp docker

Why newgrp?

Normally, group changes only take effect after you log out and back in. newgrp docker activates the group immediately in your current shell so you can keep working without interruption.

Run the hello-world container to confirm everything works

Docker ships with a tiny test image called hello-world. Running it proves that Docker can pull images from the internet, create containers, and execute them.

Before you run the next command, what do you think will happen? Will Docker find the image locally or need to download it first?

  • Test your Docker installation by running:
docker run hello-world

You should see output that starts with "Hello from Docker!" along with an explanation of what just happened.

What just happened?

Docker looked for the hello-world image on your machine, didn't find it, downloaded it from Docker Hub, and then ran a container from that image. The container printed its message and exited.

Notice you did NOT need sudo in front of the command. That confirms your group membership is working correctly.

Seeing 'permission denied' or 'Cannot connect to the Docker daemon'?

If you see a permission error, the newgrp docker command may not have taken effect. Close your terminal completely, open a new one, and try docker run hello-world again.

If the Docker daemon isn't running, start it manually with sudo systemctl start docker and try again.

Help me fix my Docker permission or daemon error

Docker Engine is installed and running on your machine. Next up, you'll build your own container image from scratch and see it live in your browser.

Build and Run Your First Container

Docker is installed and working. Now it is time to make it do something visible. A running container you can open in your browser is the fastest proof that Docker actually works.

You will create a simple HTML page, package it into a container image using a Dockerfile, and run it so it is accessible from your browser. Without this hands-on step, networking concepts stay abstract.

In this step, get ready to:
  • Create a project directory with an HTML page and a Dockerfile.
  • Build and run your container with port mapping.
  • Inspect the running container's network settings.
Create your project files

Every Docker project starts with a directory containing your application files and a Dockerfile that describes how to build the image.

  • Create your project directory and move into it by running these commands:
mkdir ~/docker-lab
cd ~/docker-lab
  • Create the HTML file that your container will serve. Start with the document head by running this command:
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Docker Networking Lab</title>
    <style>
        body {
            font-family: system-ui, sans-serif;
            max-width: 600px;
            margin: 80px auto;
            padding: 0 20px;
            background: #1a1a2e;
            color: #e0e0e0;
        }
        h1 { color: #0fbcf9; }
        .status { 
            background: #16213e;
            border-left: 4px solid #0fbcf9;
            padding: 16px;
            margin: 20px 0;
            border-radius: 4px;
        }
    </style>
</head>
EOF

What does this command do?

The cat > index.html << 'EOF' syntax is called a heredoc. It writes everything between the first EOF and the closing EOF into the file index.html.

The HTML head section sets up the page title, a dark colour scheme, and the styling for a status box you will see in the browser.

  • Confirm the file was created by running:
ls

You should see index.html in the output.

  • Complete the HTML file by appending the body section:
cat >> index.html << 'EOF'
<body>
    <h1>Docker Networking Lab</h1>
    <div class="status">
        <p>This page is served from inside a Docker container.</p>
        <p>Container port: 80</p>
        <p>Host port: 8080</p>
    </div>
    <p>If you can see this, your container is running and port publishing is working correctly.</p>
</body>
</html>
EOF

What does the body section contain?

The body renders the page heading and a status box that tells you which ports are in use. This gives you immediate visual confirmation that port publishing is working.

Notice cat >> uses double arrows to append to the existing file instead of overwriting it.

  • Verify the full HTML file looks correct by running:
cat index.html

You should see the complete HTML document starting with <!DOCTYPE html> and ending with </html>.

✔️ Awesome, I've got everything!

Great. Make sure your file starts with <!DOCTYPE html> and ends with </html>. Next up, the Dockerfile.

ⓧ I'd like to double check the full code

Scroll up and confirm both chunks are present in order:

  • The head section chunk (from <!DOCTYPE html> through </head>).
  • The body section chunk (from <body> through </html>).

If anything looks wrong, delete the file with rm index.html and re-run both cat commands above.

  • Create your Dockerfile by running:
cat > Dockerfile << 'EOF'
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
EOF

What does this Dockerfile do?

  • FROM nginx:alpine starts from the official Nginx image built on Alpine Linux. This gives you a production-grade web server in a tiny package.
  • COPY index.html /usr/share/nginx/html/index.html places your HTML file into the directory where Nginx serves content from by default.
  • EXPOSE 80 documents that the container listens on port 80. It does not publish the port by itself.
  • Confirm both files exist by running:
ls

You should see Dockerfile and index.html listed.

✔️ Awesome, I've got everything!

Great. Double check you have saved the Dockerfile correctly.

ⓧ I'd like to double check the full code

FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

Don't see both files?

  • Make sure you ran cd ~/docker-lab before creating the files. If you are in the wrong directory, the files landed elsewhere.
  • If index.html is missing, re-run both cat heredoc commands (the > one first, then the >> one).

Why aren't my Docker project files appearing?

Build the image and run your container

A Dockerfile is just a recipe. You need to run docker build to bake that recipe into an image, then docker run to launch a container from the image.

  • Build your container image by running this command:
docker build -t webapp .

What does this command do?

  • -t webapp tags (names) the resulting image as webapp so you can reference it later by name.
  • . tells Docker to use the current directory as the build context. Docker looks for a file named Dockerfile in this location.

You should see Docker pull the nginx:alpine base image (the first time only) and then copy your HTML file into it.

Build failed?

  • If you see a permissions error, confirm you ran the newgrp docker command from Step 1.
  • If you see unable to prepare context, make sure you are inside the ~/docker-lab directory where both files live.

Why is my Docker build failing?

  • Start a container from your new image by running this command:
docker run -d --name webapp -p 8080:80 webapp

What do these flags mean?

  • -d runs the container in detached mode (in the background) so your terminal stays free.
  • --name webapp gives the container the name webapp so you can reference it by name instead of a random ID.
  • -p 8080:80 publishes port 80 inside the container to port 8080 on your host machine. This is how traffic from your browser reaches the container.
  • Confirm the container is running by checking:
docker ps

You should see a row with webapp in the NAMES column and 0.0.0.0:8080->80/tcp in the PORTS column.

Container not showing in docker ps?

  • Run docker ps -a to see all containers including stopped ones. If the webapp container has status Exited, something went wrong at startup.
  • If you see an error about the name webapp already being in use, remove the old one with docker rm webapp and re-run the docker run command.

Why isn't my Docker container running?

Confirm the page and inspect network settings

Before you open your browser, what do you expect to see at http://localhost:8080? Think about what your HTML file contains.

  • Open your web browser and navigate to http://localhost:8080.

You should see your Docker Networking Lab page with a dark background, a blue heading, and a status box showing the container and host ports.

Page not loading?

  • Run docker ps to verify the container is still running.
  • Make sure you typed http://localhost:8080 (port 8080, not 80).
  • If you are on a remote server, replace localhost with the server's IP address.

Why can't I see my Docker container's web page?

Your web page is running inside a container. Now look at the network settings Docker assigned automatically.

  • Inspect the full configuration of your running container by running:
docker inspect webapp

What should I look for?

The output is a large JSON document. Scroll down to the NetworkSettings section near the bottom. Inside Networks you will see a key called bridge. This tells you the container was placed on Docker's default bridge network automatically.

The IPAddress field shows the container's IP on that bridge (something like 172.17.0.x). This is the internal address other containers would need to use to reach it on the default bridge.

  • Extract just the container's IP address by running this formatted version:
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' webapp

You should see a single IP address printed, such as 172.17.0.2. This is the address Docker assigned to your webapp container on the default bridge network.

Why does the IP address matter?

On the default bridge network, containers can only reach each other by IP address. They cannot use container names. In the next step, you will see this limitation firsthand when a second container tries to reach webapp by name and fails.

Your container is running, serving a web page, and connected to Docker's default bridge network. Next, you will add a second container and discover why the default bridge falls short for real multi-container applications.

Connect a Second Container on the Default Bridge

Your webapp container is serving pages, but real applications rarely run in isolation. Web servers connect to databases, APIs talk to caches, and microservices call each other constantly.

When you start a container without specifying a network, Docker places it on the default bridge network. Your webapp is already there. In this step, you will add a second container and discover a critical limitation of the default bridge.

In this step, get ready to:
  • Start a network diagnostic container on the default bridge network.
  • Inspect the bridge network and confirm both containers are connected.
  • Attempt container-to-container communication by name and observe the result.
Start a network troubleshooting container

The nicolaka/netshoot image is a purpose-built toolkit for network troubleshooting inside Docker. It ships with dig, ping, curl, and other networking utilities pre-installed.

  • Start a nettools container by running this command:
docker run -d --name nettools nicolaka/netshoot sleep infinity

What does this command do?

  • -d runs the container in the background (detached mode).
  • --name nettools gives the container a memorable name you can reference later.
  • nicolaka/netshoot is the image packed with network diagnostic tools.
  • sleep infinity keeps the container running indefinitely so you can exec into it whenever you need.
  • Confirm both containers are running:
docker ps

You should see both webapp and nettools listed in the output with a status of Up.

Don't see nettools running?

  • If you see a name conflict error, a container named nettools already exists from a previous attempt. Remove it with docker rm -f nettools and try again.
  • If you see an image pull error, check your internet connection. Docker needs to download the nicolaka/netshoot image the first time.

Help me troubleshoot why my nettools container won't start.

Inspect the bridge network and test DNS resolution

Both containers are running, and neither specified a network. That means Docker placed both of them on the default bridge. Let's verify this with Docker's network inspection tool.

  • View the containers connected to the default bridge network by running:
docker network inspect bridge

What should I see?

A JSON output with a Containers section listing both webapp and nettools. Each container has an IPv4Address assigned from the bridge network's subnet (something like 172.17.0.x/16).

This confirms both containers are on the same network and should theoretically be able to reach each other.

Both containers are on the same network. Before you run the next command, think about this: do you expect pinging nettools by name from the webapp container to succeed or fail?

  • Test DNS resolution between containers by running:
docker exec webapp ping -c 2 nettools

The command fails with:

ping: nettools: Name or address not found

This is the expected result. The default bridge network does NOT provide DNS resolution between containers. Even though both containers are on the same network, they cannot find each other by name.

Why does this fail?

The default bridge is a legacy network that predates Docker's built-in DNS service. Containers on the default bridge can only communicate using IP addresses, not names.

Docker's embedded DNS server (at 127.0.0.11) only activates on user-defined bridge networks. On the default bridge, you would need to know and hardcode each container's IP address. That IP changes every time a container restarts, making it fragile and impractical.

Seeing a different error?

  • If you see OCI runtime exec failed, make sure the webapp container is still running. Check with docker ps.
  • If the ping actually succeeds, your containers may be on a user-defined network instead of the default bridge. Run docker network inspect bridge again and confirm both containers appear in the Containers section.

Help me troubleshoot my docker exec ping command.

You have just discovered the biggest limitation of Docker's default bridge network. Next up, you will create a custom network that solves this problem with automatic DNS discovery.

Create a Custom Network with DNS Discovery

In the previous step, you proved that the default bridge network cannot resolve container names. Pinging webapp by name from nettools failed with a "Name or address not found" error.

User-defined bridge networks solve this exact problem. Docker runs an embedded DNS server on user-defined networks that automatically registers every container's name as a DNS entry.

In this step, get ready to:
  • Create a user-defined bridge network.
  • Remove your old containers and re-launch them on the new network.
  • Verify DNS-based container discovery with ping, curl, and dig.
Create a user-defined bridge network

A user-defined bridge network gives every container its own DNS entry automatically. The embedded DNS server runs at 127.0.0.11 inside the network and resolves container names to IP addresses.

  • Create a new network called my-app-net by running this command:
docker network create my-app-net

You should see a long hash string printed to your terminal. That is the network ID confirming your network was created.

  • Confirm the network exists by running:
docker network ls

You should see my-app-net listed with a bridge driver. The bridge driver is the default when you create a network without specifying one.

Why a user-defined bridge instead of the default?

Both the default bridge and user-defined bridges let containers communicate by IP address. The difference is DNS. User-defined bridges automatically register each container's name so other containers can reach it without knowing its IP.

In production, container IPs change every time a container restarts. DNS-based discovery means your services keep working even after restarts or redeployments.

Remove old containers and re-launch on the new network

Your existing containers are attached to the default bridge. You need to remove them and start fresh on the new network.

  • Remove both running containers by running this command:
docker rm -f webapp nettools

What does this command do?

The -f flag force-removes containers even if they are still running. Without it, you would need to stop the containers first with docker stop before removing them.

You should see both container names printed back to you, confirming they were removed.

  • Re-launch the webapp container attached to the new network by running:
docker run -d --name webapp -p 8080:80 --network my-app-net webapp

What does this command do?

This is the same run command from before, with one addition: --network my-app-net tells Docker to place the container on your custom network instead of the default bridge.

  • Confirm the web page still loads by opening http://localhost:8080 in your browser.

You should see the same Docker Networking Lab page as before. Port publishing works the same regardless of which Docker network the container sits on.

  • Now launch the nettools container on the same network by running:
docker run -d --name nettools --network my-app-net nicolaka/netshoot sleep infinity

What does sleep infinity do?

The sleep infinity command keeps the container running indefinitely so you can exec into it later. Without it, the container would start and immediately exit because it has no long-running process.

Seeing a name conflict error?

If you see a message about the container name already being in use, the previous removal did not complete. Run docker rm -f nettools and try again.

Need more help?

Why is my container name conflicting even after I removed it?

Verify DNS-based container discovery

Both containers are now on the same user-defined network. Time to test whether DNS resolution actually works.

Before you run the next command, think back to the previous step. You ran the same ping command on the default bridge and it failed. Do you think it will succeed this time?

  • Test DNS resolution by pinging the webapp container by name:
docker exec nettools ping -c 2 webapp

You should see successful replies with a resolved IP address. The output starts with something like PING webapp (172.18.0.X) followed by two reply lines showing round-trip times. This proves Docker's DNS resolved the name webapp to an IP address automatically.

  • Now use curl to make an HTTP request to the webapp container by name:
docker exec nettools curl webapp

You should see the raw HTML of your Docker Networking Lab page printed to the terminal. This confirms that the nettools container can reach the webapp's web server using just its name.

  • Finally, inspect the actual DNS record Docker created by running:
docker exec nettools dig webapp

In the output, look for the ANSWER SECTION. You should see an A record showing webapp resolving to an IP address. Also notice the SERVER line at the bottom shows 127.0.0.11. That is Docker's embedded DNS server handling all name resolution on this network.

What is an A record?

An A record is a DNS entry that maps a name to an IPv4 address. When you type a website address in your browser, a DNS server looks up the A record to find the IP address to connect to. Docker does the same thing inside user-defined networks for container names.

Your containers can now find each other by name. No hardcoded IPs. No manual configuration. Docker's DNS handles it all. Next up, you will dig into the network's internals to see exactly how Docker organizes the subnets, gateways, and port mappings behind the scenes.

Inspect and Troubleshoot the Network

Your containers are talking to each other by name. That is great for running services, but in a networks and security role you need more than "it works."

You need to verify which containers can reach which, what IPs they hold, and what ports are exposed to the host. Docker ships built-in inspection tools that answer all three questions from the command line.

In this step, get ready to:
  • Inspect your custom network's subnet, gateway, and connected containers.
  • View a container's network interfaces from inside and compare against the inspect output.
  • Confirm published port mappings match what you specified at launch.
Inspect the custom network

Every user-defined bridge network has its own subnet and gateway. The docker network inspect command shows these details alongside every container currently connected.

  • View the full configuration of your network by running this command:
docker network inspect my-app-net

You should see a large JSON block. Look for the IPAM section near the top and the Containers section near the bottom.

What should I see?

The JSON output contains several key sections:

  • IPAM > Config > Subnet shows the network's address range (something like 172.18.0.0/16).
  • IPAM > Config > Gateway shows the gateway IP Docker uses for this network.
  • Containers lists every container on the network with its name, MAC address, and IPv4 address.

You should see both webapp and nettools listed here with their assigned IPs.

Keep note of those IPs

Write down or remember the IPv4 address shown next to nettools in the output. You will compare it against what the container itself reports in the next substep.

Seeing an empty Containers section?

If the Containers section shows {}, your containers may have stopped or were created on a different network.

  • Run docker ps to confirm both webapp and nettools are listed as running.
  • Check the NETWORKS column shows my-app-net for both containers.

Help me troubleshoot docker network inspect

View network interfaces from inside a container

The inspect command shows you Docker's view from the outside. Now look at the same network from inside the container to confirm they match.

  • Check the network interfaces inside the nettools container by running:
docker exec nettools ip addr show

You should see output listing multiple interfaces. Look for the eth0 interface and its inet line.

What does this output mean?

The ip addr show command lists every network interface the container has. You will see:

  • lo is the loopback interface (127.0.0.1). Every container has this.
  • eth0 is the container's connection to the Docker bridge network. The IP address on this interface should match the one you saw in docker network inspect for the nettools container.

If both IPs match, Docker's internal DNS records and the actual network configuration are consistent. This is how you confirm that inspect output reflects reality.

  • Compare the IP address on eth0 against the IP shown for nettools in the docker network inspect output from the previous substep.

They should be identical. This proves Docker's metadata is an accurate reflection of the container's actual network state.

IPs don't match?

If the IP on eth0 differs from what inspect reported, the container may have been restarted between the two commands. Docker assigns IPs dynamically each time a container starts.

  • Re-run docker network inspect my-app-net and compare again.

Help me understand why my container IP changed

Confirm published port mappings

Port publishing is what makes your container reachable from the host machine's browser. The docker port command shows exactly which host ports map to which container ports.

  • Before you run the next command, predict what you will see. Which host port did you map to which container port when you launched webapp?
  • Check the port mapping by running:
docker port webapp

You should see 80/tcp -> 0.0.0.0:8080 in the output. This confirms that traffic arriving on host port 8080 is forwarded to port 80 inside the container.

What does 0.0.0.0 mean?

The 0.0.0.0 address means the port is bound on all network interfaces of your host machine. Any IP that reaches your host on port 8080 will be forwarded into the container.

If you had specified -p 127.0.0.1:8080:80 instead, only localhost connections would reach the container. This distinction matters for security when running containers on internet-facing servers.

Now run docker network inspect one final time to see the full picture with both containers, their IPs, and their aliases all in one view.

  • View the final state of your network by running:
docker network inspect my-app-net

You now have a complete mental model of what is happening under the hood. Both containers are listed with their IP addresses and network aliases on your custom bridge network.

You have now seen Docker networking from every angle. From the outside with docker network inspect, from the inside with ip addr show, and at the port level with docker port. These are the same tools you will reach for when debugging connectivity issues in production container environments.

Secret mission

Isolate a Backend with an Internal Network

In production, backend services like databases and caches should never be reachable from the public internet. Can you use Docker's internal network flag to completely cut off a container from external access while still letting trusted containers communicate with it?

Clean Up Your Resources

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 locally, so there are no ongoing costs. Docker images and containers consume disk space only.

Resources you used:

  • Docker containers: webapp, nettools, backend.
  • Docker networks: my-app-net, backend-net.
  • Docker images: webapp (custom built), nginx:alpine, nicolaka/netshoot, hello-world.
  • Project directory: ~/docker-lab (contains index.html and Dockerfile).
  • Docker Engine (installed via apt).

Keep everything running

No action needed. Choose this if you are still actively building or plan to continue with the next project in this series.

  • Your containers did not use --restart flags, so they will stop automatically when you reboot your machine.
  • Docker images stay cached locally and are ready for instant reuse.
  • Docker Engine continues running as a system service. It uses minimal resources when no containers are active.

Pause - I'll come back to this later

Stop running containers to free up memory while keeping your files and images for later.

  • Stop all running containers by running:
  • Your images and project files remain on disk, ready for you to restart with docker start webapp when you return.
docker stop webapp nettools backend

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps in order.

  • Stop and remove all project containers by running:
docker stop webapp nettools backend
docker rm webapp nettools backend
  • Remove the custom networks by running:
docker network rm my-app-net backend-net
  • Remove all unused images, stopped containers, and dangling resources by running:
docker system prune -a
  • Remove your project directory by running:
rm -rf ~/docker-lab
  • If you also want to remove Docker Engine entirely, uninstall the packages and remove Docker data by running:
sudo apt purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd

What does this remove?

The apt purge command removes the Docker Engine packages. The two rm -rf commands delete all Docker data (images, volumes, containers) stored on your system.

If you want to keep Docker installed for future projects but remove only this project's resources, skip the apt purge step. The steps above it are sufficient.

Nice Work!

Nice Work!

You just built and networked Docker containers from scratch, proving you can isolate services and control how they communicate at the network level.

What you learned:

  • Built a custom container image from a Dockerfile and ran it with port publishing so you could reach it from your browser.
  • Discovered that the default bridge network does NOT provide DNS resolution between containers, then solved it by creating a user-defined bridge network with automatic service discovery.
  • Used docker network inspect, dig, and ping to troubleshoot container networking and verify connectivity between services.
  • Secret Mission: Isolated a backend container using an internal network with zero access to the outside internet, enforcing network-level security.

Ready to quiz yourself?