GCP Zero to Hero in 20 Days - Chapter: Learn Docker Real-time by Containerizing Super Mario Mimic
This chapter provides a hands-on, real-world guide to containerizing a TypeScript-based Super Mario mimic application and deploying it on Google Cloud Platform (GCP). It covers the fundamental problems Docker solves, the mechanics of multi-stage Docker builds, and the cloud networking required to expose containerized applications securely to the public internet.
1. The Core Problems Docker Solves
Before the advent of containerization, software development and operations teams faced two systemic bottlenecks: environment drift and resource underutilization.
Problem 1: "It Works on My Machine" (Environment Drift)
Historically, developers and system administrators (or DevOps engineers) worked in siloed environments. A developer might write and test an application on a Windows or macOS workstation, while the system administrator deployed it to a production server running a specific Linux distribution (e.g., Ubuntu, CentOS, Debian, or Alpine).
Even minor differences in system configurations, shared libraries, or runtime versions could cause the application to fail in production. Docker solves this by bundling the application code, its runtime dependencies, and system-level configurations into a single, immutable package called a Container Image.
Problem 2: Compute Resource Underutilization
In traditional infrastructure, applications were deployed directly onto physical servers. Because physical hardware is rich in resources (e.g., 100 CPUs, 100 GB RAM), deploying a single application per physical server resulted in massive waste.
Virtual Machines (VMs) improved this by partitioning physical hardware into multiple logical operating systems. However, companies still underutilized VMs by deploying only one application per VM to prevent dependency conflicts.
Docker allows organizations to run hundreds of isolated, lightweight containers on a single virtual machine. This architecture is the foundation of modern microservices and container orchestration platforms like Kubernetes.
2. Mental Model: Virtual Machines vs. Containers
To understand why containers are highly efficient, it is helpful to compare how virtual machines and containers partition compute resources.
Virtual Machine Architecture
Virtual machines rely on a hypervisor to slice physical hardware into logical machines. Each VM must run a full guest operating system, which consumes significant CPU, memory, and storage overhead.
Container Architecture
Containers run on top of a single host operating system and share its kernel. The Docker Engine isolates processes using Linux kernel features (namespaces and cgroups), eliminating the need for a guest OS for each application.
3. The DevOps Perspective on Source Code & Dependencies
A common misconception among aspiring cloud and DevOps engineers is that they must write or deeply understand the application's source code. In practice, a DevOps engineer does not need to master every programming language. Instead, they must focus on three core responsibilities:
- Understand the Structure: Identify where the application declares its dependencies.
- Know How to Build: Understand the commands required to compile or bundle the application.
- Know How to Deploy: Package the application into a container and run it on target environments.
Dependency and Build Artifacts by Language
Different programming languages use distinct files and package managers to declare dependencies and build binaries. The table below synthesizes these differences:
Note: If you are deploying an unfamiliar application, collaborate with the development team. They can provide the exact commands they use to run the application locally, which you can then translate into a Dockerfile.
4. Setting Up the Infrastructure on Google Cloud Platform (GCP)
To host the containerized Super Mario application, we will provision a virtual machine on GCP using Compute Engine.
Step-by-Step VM Provisioning
- Create a GCP Project: Create a dedicated project (e.g.,
Docker-demo) to isolate your resources.Pro-Tip: Once you finish practicing, disable billing and delete the project to prevent unexpected charges.
- Enable Compute Engine API: Navigate to Compute Engine -> VM Instances and click Enable to activate the API.
- Configure the VM Instance:
- Name:
mario-instance - Region/Zone: Select the region closest to your physical location to minimize network latency.
- Machine Type: Choose
e2-small(2 vCPUs, 2 GB RAM). Avoide2-microfor this demo, as compiling TypeScript applications requires more memory thane2-microprovides. - Provisioning Model: Change the VM provisioning model to Spot under Advanced Configuration -> VM Provisioning Model. Spot instances are highly discounted (up to 90% off) but can be reclaimed by GCP if resources are needed elsewhere. This is ideal for proof-of-concept (PoC) environments.
- Network Tag: Add a network tag named
mariounder the networking settings. This tag will be used later to apply firewall rules to this specific VM.
- Name:
5. Installing Docker on a Debian Virtual Machine
Once the VM is running, connect to it securely using the GCP Cloud Shell or SSH. Follow these steps to install Docker on a Debian-based operating system.
Step 1: Update the Package Index
Update the local package index to ensure you download the latest metadata for system packages:
sudo apt update
Step 2: Install Docker via the Official Repository
To ensure you install the latest stable version of Docker, configure the official Docker repository:
# Install prerequisite packages
sudo apt install -y ca-certificates curl gnupg
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Set up the repository
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update the package index again
sudo apt update
Step 3: Install Docker Engine and Command-Line Tools
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Step 4: Verify the Installation
Run the hello-world container to verify that the Docker daemon is active and running:
sudo docker run hello-world
6. The Evolution of the Dockerfile: Single-Stage vs. Multi-Stage Builds
When containerizing applications, writing a naive, single-stage Dockerfile can lead to bloated images and security vulnerabilities.
Single-Stage Builds (Naive Approach)
A single-stage build uses one base image to install build tools, copy source code, compile the application, and run it. The final image retains all compilers, source files, and build-time dependencies (such as TypeScript compilers or build tools), which are unnecessary at runtime.
Multi-Stage Builds (Production-Ready Approach)
A multi-stage build uses multiple FROM statements in a single Dockerfile. Each FROM instruction begins a new stage of the build with a fresh base image. You can compile your application in an environment containing all necessary build tools, and then copy only the compiled production assets (e.g., static HTML/JS files) into a lightweight runtime image (like Alpine Linux).
Benefits of Multi-Stage Builds:
- Reduced Image Size: Decreases storage and network transfer costs.
- Enhanced Security: Eliminates build tools and package managers from the final runtime container, reducing the attack surface.
- Layer Caching Optimization: Speeds up build times by separating dependency installation from source code changes.
7. Step-by-Step Breakdown of the Multi-Stage Dockerfile
Below is the production-grade, multi-stage Dockerfile used to containerize the Super Mario TypeScript application.
# ==========================================
# STAGE 1: Build Stage
# ==========================================
FROM node:18-alpine AS build
# Set the working directory inside the container
WORKDIR /app
# Copy dependency definitions first to leverage Docker caching
COPY package.json ./
# Install dependencies using npm ci (clean install for automated environments)
RUN npm ci
# Copy the rest of the application source code
COPY . .
# Compile the TypeScript application into static assets (output goes to /app/dist)
RUN npm run build
# ==========================================
# STAGE 2: Runtime Stage
# ==========================================
FROM node:18-alpine
# Set the working directory for the runtime environment
WORKDIR /app
# Copy only the compiled static assets from the build stage
COPY --from=build /app/dist ./dist
# Install a lightweight HTTP server globally to serve static files
RUN npm install -g http-server
# Document that the container listens on port 8080
EXPOSE 8080
# Run the HTTP server, serving the 'dist' folder on port 8080 with caching disabled (-c-1)
CMD ["http-server", "dist", "-p", "8080", "-c-1"]
Key Directives Explained:
FROM node:18-alpine AS build: Uses Node.js version 18 on Alpine Linux as the base image for the compilation stage, aliasing it asbuild.COPY package.json ./followed byRUN npm ci: By copying only the package file and running the install command before copying the rest of the source code, Docker caches this layer. If the source code changes but dependencies do not, Docker skips the time-consumingnpm cistep in subsequent builds.COPY --from=build /app/dist ./dist: Copies only the compiled static files from the/app/distdirectory of thebuildstage into the current stage, leaving behind all source code and node modules.http-server dist -p 8080 -c-1: Launches a simple web server to serve the static files on port 8080. The-c-1flag disables cache control, ensuring that updates to the container are served immediately without browser caching issues.
8. Real-World Debugging: Resolving the "Missing Source Code" Build Failure
During the build process, a common mistake is running the build command in a directory that does not contain the application's source code.
The Error Scenario
If you write the Dockerfile in your home directory and run docker build -t mario-game:v1 . before cloning the application code, the build will fail at the RUN npm ci step:
[stderr] npm ERR! code ENOENT
[stderr] npm ERR! syscall open
[stderr] npm ERR! path /app/package.json
[stderr] npm ERR! errno -2
[stderr] npm ERR! enoent ENOENT: no such file or directory, open '/app/package.json'
The Root Cause
The Docker daemon looks for package.json in the build context (the directory specified by the . at the end of the build command). Because the repository has not been cloned into this directory, the file does not exist, and the build fails.
The Resolution Workflow
To resolve this issue, clone the source code repository, move your Dockerfile into the repository directory, and run the build command from there:
# 1. Clone the application source code
git clone https://github.com/iam-veeramalla/super-mario-mimic.git
# 2. Move the Dockerfile into the cloned repository directory
mv Dockerfile super-mario-mimic/
# 3. Change directory to the repository root
cd super-mario-mimic
# 4. Execute the Docker build command
sudo docker build -t mario-game:v1 .
9. Building and Running the Container in the Background
Once the source code and Dockerfile are in the same directory, you can build the image and run the container.
Step 1: Build the Docker Image
Build the image and tag it as mario-game:v1:
sudo docker build -t mario-game:v1 .
Verify that the image was created successfully:
sudo docker images
Step 2: Run the Container in Detached Mode
Run the container in the background (detached mode) and map port 8080 of the host VM to port 8080 of the container:
sudo docker run -d -p 8080:8080 --name mario-container mario-game:v1
-d: Runs the container in detached mode (in the background, freeing up the terminal).-p 8080:8080: Maps port 8080 on the host VM to port 8080 inside the container.--name mario-container: Assigns a friendly name to the running container.
Step 3: Verify the Running Container
Check the status of the running container:
sudo docker ps
10. Exposing the Application: GCP Firewall Configuration
Even though the container is running and mapped to port 8080 of the VM, you cannot access it via the VM's external IP address yet. By default, cloud providers block all incoming traffic on non-standard ports for security.
To allow external traffic to reach the application, you must configure a firewall rule in GCP.
Step-by-Step Firewall Configuration
- Search for Firewall in the GCP Console search bar.
- Click Create Firewall Rule.
- Configure the rule with the following parameters:
- Name:
allow-mario-8080 - Network:
default - Targets: Select Specified target tags.
- Target Tags: Enter
mario(this must match the network tag assigned to your VM in Section 4). - Source Filter: Select IPv4 ranges.
- Source IPv4 Ranges: Enter
0.0.0.0/0(this allows traffic from any IP address on the internet). - Protocols and Ports: Check Specified protocols and ports, select TCP, and enter
8080.
- Name:
- Click Create.
Cloud Security Comparison
Different cloud providers use different mechanisms to control network access to virtual machines:
11. Complete End-to-End Workflow Walkthrough
The diagram below illustrates how an external user's request travels through the GCP network infrastructure to reach the Super Mario game running inside the Docker container.
Once the firewall rule is active, copy the VM's external IP address from the Compute Engine console, append :8080 to it, and paste it into your web browser (e.g., http://34.123.45.67:8080). The Super Mario game will load and run directly in your browser.
12. Summary and Key Takeaways
- Docker's Core Value: Docker eliminates environment drift ("works on my machine") by packaging applications with their dependencies, and maximizes hardware efficiency by running multiple isolated containers on a single host.
- DevOps Code Ownership: DevOps and cloud engineers do not need to write application code. They must understand the application's structure, build commands, and deployment requirements.
- Multi-Stage Builds: This pattern uses a heavy build environment to compile the application, and then copies only the compiled assets into a lightweight runtime image. This reduces image size and minimizes security vulnerabilities.
- Layer Caching: Copying dependency files (like
package.json) and running installation commands before copying the rest of the source code allows Docker to cache layers, significantly reducing subsequent build times. - Port Mapping: The
-p host_port:container_portflag maps network traffic from the host VM's network interface into the isolated container's network namespace. - Cloud Security: Running a containerized application on a specific port requires opening that port in the cloud provider's firewall. In GCP, this is managed by combining firewall rules with VM network tags.