Package the ABC Retail web application into Docker containers. Write your first Dockerfile, build images, manage containers, configure volumes for persistent storage, and orchestrate multiple services with Docker Compose — all running on your Ubuntu VM.
Understanding the difference between virtual machines and containers is fundamental to modern cloud engineering. VMs virtualize hardware — each VM has its own full operating system kernel. Containers virtualize the operating system — they share the host kernel but have isolated user space. This makes containers much lighter, faster to start, and more portable than VMs.
Docker Engine is the core software that runs and manages containers. We will install the official Docker Engine from Docker's repository — this is more up-to-date than the version in Ubuntu's default package list.
ca-certificates allows Ubuntu to verify SSL certificates when downloading from the internet, curl downloads files from URLs, gnupg verifies cryptographic signatures (to ensure you're installing authentic Docker packages), and lsb-release provides information about the Linux distribution version.curl -fsSL downloads the key silently, and gpg --dearmor converts it from ASCII armor format to binary format. Without this key, Ubuntu would refuse to install packages from Docker's repository as a security measure.$(dpkg --print-architecture) automatically inserts your system's CPU architecture (amd64 for 64-bit x86), and $(lsb_release -cs) inserts your Ubuntu version codename (e.g., "jammy" for 22.04). This ensures you get the version of Docker built specifically for your system.docker-ce is Docker Community Edition (the container engine), docker-ce-cli is the command-line interface for controlling Docker, containerd.io is the container runtime that actually starts and stops containers, docker-buildx-plugin provides advanced image building features, and docker-compose-plugin adds the ability to manage multi-container applications.Active: active (running) in green. Docker must be running as a background service for any Docker commands to work, just like how Nginx must be running to serve websites.sudo for every Docker command:docker group grants permission to use Docker without typing sudo every time. The usermod -aG command appends your user to the group. The newgrp docker command applies the group change immediately in the current session without requiring a logout.docker --version command shows the installed Docker Engine version. The docker compose version (note: no hyphen in newer Docker versions) confirms Docker Compose is available. Write down these version numbers — they are part of your documentation deliverable.Before building our own image, explore how Docker works. These commands will help you understand the relationship between images (blueprints) and containers (running instances).
docker pull command downloads a Docker image from a registry (Docker Hub by default). The image name format is imagename:tag where the tag specifies a version. ubuntu:22.04 downloads Ubuntu 22.04. This image is only ~29 MB — compared to the 1.5 GB Ubuntu ISO file — because it contains only the minimal OS components needed to run applications, not a full graphical desktop environment.docker images command lists all Docker images stored locally on this machine. You should see the hello-world and ubuntu:22.04 images. The columns show: REPOSITORY (image name), TAG (version), IMAGE ID (unique identifier), CREATED (when the image was built), and SIZE. Images are the "blueprints" — they are read-only templates. Running an image creates a container (the living instance).ubuntu:22.04 image. The -it flags mean: -i (interactive — keep STDIN open) and -t (tty — allocate a terminal). --name my-ubuntu-test gives the container a memorable name. /bin/bash is the command to run inside the container — starting an interactive bash shell. You will see the prompt change to something like root@abc123456:/# — you are now inside the container!ls / lists the root directory (you'll see typical Linux directories), cat /etc/os-release shows the Ubuntu 22.04 OS information, hostname shows a randomly generated container ID as the hostname, and whoami shows you're running as root inside the container. The container has its own isolated filesystem, hostname, and user space — even though it shares the host system's kernel.exit closes the bash shell inside the container, which causes the container to stop (since bash was the only process running in it). You are back in your Ubuntu VM's terminal. The container still exists in a "stopped" state — you can restart it or delete it.docker ps command lists running containers. The -a flag (all) shows both running and stopped containers. You should see your my-ubuntu-test container with STATUS "Exited". The CONTAINER ID column shows the unique ID assigned to each container. Think of containers like processes — they can be started, stopped, and removed independently of their image.docker rm command removes (deletes) a stopped container. Once a container is removed, any changes made inside it are permanently lost (unless you saved data to a Docker Volume). The underlying image (ubuntu:22.04) remains — you can always create a new container from it. This is a key difference between images (permanent, shareable) and containers (temporary instances).A Dockerfile is a text file containing instructions to build a custom Docker image. Think of it as a recipe — each line is an instruction that adds a layer to the image. This is where containerization begins: packaging your application with all its dependencies.
~/abc-retail-app (the tilde ~ represents your home directory). Inside it, we create app/ for the application code and nginx-config/ for the web server configuration. Good directory organization is essential in production projects — it makes the project understandable to other engineers and your future self.nginx:alpine (Nginx on Alpine Linux, a tiny 5 MB Linux distribution) to keep the image small. LABEL — adds metadata like author and version. RUN — executes shell commands during build (here, updating Alpine's package list and installing curl for health checks). COPY — copies files from your local machine into the image. EXPOSE — documents that the container listens on port 80 (HTTP). HEALTHCHECK — tells Docker how to verify the container is working correctly. CMD — the command that runs when the container starts (Nginx in foreground mode).Ctrl+O then Enter to save, then Ctrl+X to exit nano..dockerignore file works like .gitignore — it lists patterns of files that should NOT be copied into the Docker image. Excluding .git (version control history), log files, node_modules (reinstalled inside the container), and .env files (may contain secrets) keeps the image small and prevents accidentally exposing sensitive information. A well-crafted .dockerignore can reduce image size by hundreds of MB in larger projects.Building a Docker image reads your Dockerfile and executes each instruction to create a layered, immutable image file. This is the "compilation" step in containerization.
ls -la command lists all files including hidden ones (like .dockerignore). You should see: Dockerfile, app/ directory, nginx-config/ directory, and .dockerignore. If any are missing, go back and create them before proceeding.docker build command reads the Dockerfile and creates an image. The -t abc-retail:v1.0 flag "tags" (names and versions) the image as abc-retail with version v1.0. The . (dot) at the end tells Docker to look for the Dockerfile in the CURRENT directory. Watch the output — you will see each Dockerfile instruction executing in sequence. Each instruction creates a new layer that is cached for future builds.abc-retail with tag v1.0 in the list. Notice that the image size is very small — around 15–25 MB — because we built on the Alpine Linux base. Compare this to the full Ubuntu VM which was 20 GB. This is the power of containers: everything needed to run the application is included, but nothing unnecessary is included.docker history command shows all the layers that make up the image, from bottom (base) to top (your instructions). Each row corresponds to a Dockerfile instruction. The SIZE column shows how much each layer added to the image. Understanding image layers is important for optimization — large layers (e.g., a 500 MB RUN command) should be analyzed and reduced if possible.Now you will run your custom image as a container and verify the ABC Retail website is accessible.
-d runs the container in "detached" mode (in the background, so it doesn't block your terminal), --name abc-retail-web gives it a memorable name for easy management, -p 8080:80 maps port 8080 on the HOST (your Ubuntu VM) to port 80 INSIDE the container (where Nginx listens) — this is called "port publishing", and --restart unless-stopped automatically restarts the container if it crashes or if the Docker service restarts, except when manually stopped. This restart policy is the Docker equivalent of systemctl enable.docker ps command (without -a) shows only RUNNING containers. You should see abc-retail-web with STATUS "Up X seconds" and PORTS showing 0.0.0.0:8080->80/tcp. The arrow (->) shows the port mapping: traffic arriving on port 8080 of the host is forwarded to port 80 inside the container. The 0.0.0.0 means it listens on all network interfaces.http://[VM-IP]:8080 (use the same VM IP from Project 2 but with port 8080). Take a screenshot — this is a deliverable.docker logs command retrieves all output that the container has written to stdout and stderr since it started. For Nginx, this shows access logs (every HTTP request received) and error logs. In a production environment, these logs would be collected by a centralized logging system (like ElasticSearch or AWS CloudWatch). You should see entries for the curl request and browser requests you just made.docker inspect command returns detailed JSON information about a container. The --format flag uses Go templating to extract just the health status field. You should see healthy — which means the HEALTHCHECK instruction in your Dockerfile successfully ran the curl command and got a response from Nginx. The health check runs every 30 seconds (as configured). A container that fails health checks would show "unhealthy" — this triggers alerts in production monitoring systems.By default, any data written inside a container is lost when the container is removed. Docker Volumes solve this by storing data outside the container filesystem in a special Docker-managed location on the host.
docker volume create command creates a named volume — a Docker-managed storage location on the host filesystem (usually in /var/lib/docker/volumes/). Named volumes are more portable and manageable than "bind mounts" (which directly map host directories). Think of Docker volumes as the equivalent of cloud block storage (EBS volumes) — they exist independently of any specific container.abc-retail-data with DRIVER local. The local driver stores data on the local filesystem. In cloud environments, you would use volume drivers that connect to cloud storage services (like AWS EFS or Azure Files), allowing multiple containers to share the same volume across different servers.-v abc-retail-data:/var/log/nginx. This mounts the abc-retail-data Docker volume to the /var/log/nginx directory inside the container (where Nginx writes its logs). Now, even if the container is removed and recreated, the log files persist in the volume. This is the key difference between persistent and ephemeral container storage.abc-retail-data volume is mounted at /var/log/nginx inside the container. The output shows the source path on the host (in /var/lib/docker/volumes/) and the destination path inside the container — this confirms the volume is correctly connected.Real applications require multiple services — a web server, a database, a cache. Docker Compose lets you define and run multi-container applications using a single YAML configuration file. This is much more manageable than running multiple separate docker run commands.
web (the Nginx-based web server from your Dockerfile) and database (MariaDB, an open-source MySQL-compatible database). Key concepts: services — each named block under "services" is a container; networks — both containers are on the same abc-retail-net network, allowing them to communicate by service name (e.g., the web container can reach the database at hostname "database"); volumes — named volumes for persistent data; environment — environment variables passed to the container at startup (used to configure MariaDB). In production, passwords would be stored in Docker Secrets or a vault rather than in this file.Ctrl+O, Enter, Ctrl+X.docker compose up command reads docker-compose.yml, creates the network and volumes defined in it, and starts all services in the correct order. The -d flag runs everything in detached (background) mode. Docker Compose is smart about dependencies — if service A depends on service B, it starts B first. Watch the output: you'll see Docker pulling the MariaDB image, creating the network, creating volumes, and starting both containers.docker compose ps command shows the status of all services defined in your Compose file. You should see both abc-retail-web and abc-retail-db with STATUS "Up". The database may show "Up (health: starting)" initially — wait about 30 seconds for MariaDB to fully initialize, then run the command again. It should show "Up (healthy)".docker exec command runs a command inside an already-running container. Here we run ping database -c 3 inside the web container, pinging the "database" service by name (-c 3 means send 3 pings). This works because Docker Compose creates a shared network where containers can reach each other by their service names. You should see ping responses — proving that the web and database containers can communicate. This simulates a real application architecture where the web server connects to the database.Docker Hub is the world's largest container registry — it's where Docker images are stored and shared. Pushing your image to Docker Hub means it can be pulled and run on ANY machine with Docker, anywhere in the world.
https://hub.docker.com → click "Sign Up" → fill in your username, email, and password → verify your email.~/.docker/config.json so you don't need to log in again for future push/pull operations. In production pipelines, a service account token or robot account is used instead of personal credentials.YOUR_DOCKERHUB_USERNAME with your actual Docker Hub username. The docker tag command creates an alias for an existing image with a new name. We create two tags: v1.0 (specific version) and latest (always points to the most recent version). In production, never rely only on latest — versioned tags allow you to roll back to a specific version if the latest has a bug.docker push command uploads the image to Docker Hub. Docker uploads each layer separately — if any layer was previously pushed, it is skipped (only changed layers are uploaded). After pushing, anyone with internet access can run your container by pulling this image. Go to https://hub.docker.com/r/YOUR_USERNAME/abc-retail in your browser — you should see your image listed there!http://[VM-IP]:9090 in your browser and see the ABC Retail website — you have successfully demonstrated that your containerized application is portable and can be deployed on any Docker host worldwide. This is the fundamental value proposition of containers.This diagram shows the Docker container lifecycle — from source code to running container to registry.
docker images outputdocker compose psdocker run hello-world succeedsdocker builddocker compose up command. Deployment that previously took hours now takes seconds.