3-Tier Web App with Docker: Full Hands-On Tutorial
Welcome to the definitive hands-on capstone project for containerizing a real-world, full-stack application. In this chapter, we will build, containerize, and orchestrate QuickBite—a complete 3-tier food delivery web application—from scratch using Docker, custom Dockerfiles, and Docker Compose. We will examine system architecture, write production-grade multi-stage and custom build scripts, manage container networking, and implement persistent storage using Docker Volumes.
1. Why Do We Need Containerized 3-Tier Applications?
In modern software development, monolithic applications are frequently broken down into independent layers, or tiers, to simplify scaling, maintenance, and deployment. However, managing dependencies across multiple tiers (frontend, backend, database) in traditional environments often leads to "it works on my machine" syndromes.
- Environment Consistency: Docker guarantees that the exact runtime environment present on a developer's laptop is replicated identically in staging and production servers.
- Dependency Isolation: Node.js versions, Python packages, and database engines run inside isolated namespaces without interfering with one another or the host operating system.
- Rapid Provisioning: Lightweight container images allow infrastructure to spin up or scale out in seconds rather than minutes.
2. Core Mental Model & Real-Life Analogy
Real-Life Analogy: The Restaurant Workflow
Think of a 3-tier web application like a busy restaurant:
- Frontend (The Dining Room / Waitstaff): Interacts directly with the client (customer). It handles presentation, user interface, menus, and takes the initial order.
- Backend (The Kitchen / Line Cooks): Processes the business logic. When the waitstaff brings an order slip, the kitchen prepares the meal, calculates bills, and manages order statuses.
- Database (The Pantry / Recipe Vault): Stores permanent records (inventory, user accounts, order history). Even if the kitchen staff goes home for the night, the pantry retains all ingredients and ledgers safely on the shelves.
Client (Browser)
|
| HTTP Request (Port 80)
v
Frontend (Nginx Alpine)
|
| API Call (Port 50001)
v
Backend (Node.js 18)
|
| Database Query (Port 27017)
v
Database (MongoDB) + Docker Volume (Data Persistence)
3. System Architecture & Tech Stack
The QuickBite application is structured around a classic 3-tier architecture with dedicated services for the user interface, application server, and data persistence layer.
Architectural Data Flow
4. Infrastructure Provisioning & Prerequisites
We begin by provisioning a fresh cloud virtual machine to replicate a real-world deployment pipeline from scratch.
EC2 Instance Configuration
- AMI: Ubuntu (Latest LTS)
- Instance Type:
t3.small(2 vCPUs, 2 GB RAM) - Security Group Inbound Rules: Open Port
80(HTTP) and Port50001(Backend API).
Installing Docker and Docker Compose on Ubuntu
Connect to your instance via SSH and run the official Docker installation pipeline:
# Update package lists and install required certificates
sudo apt-get update
sudo apt-get 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/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Set up the stable repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine and Docker Compose Plugin
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Verify installations
docker --version
docker compose version
Note: To prevent
permission deniederrors when running Docker commands withoutsudo, add your current user to thedockersecurity group:BASHsudo usermod -aG docker $USER newgrp docker
5. Cloning the Repository & Cleaning Workspace
Clone the QuickBite repository from GitHub and remove any pre-existing Docker configuration files so we can write them completely from scratch.
# Clone the repository
git clone https://github.com/your-username/quickbite.git
cd quickbite
# Inspect repository contents
ls -la
# Remove existing build files to start fresh
rm -f Dockerfile* docker-compose*.yml
6. Writing Custom Dockerfiles from Scratch
To optimize deployment speed and maintain strict minimalism, we will use lightweight alpine base images for both our frontend and backend services.
Frontend Dockerfile (Dockerfile.front-end)
Create the frontend container instruction file using an Alpine-based Nginx image:
# Use lightweight Nginx Alpine base image
FROM nginx:alpine
# Copy static frontend web assets into Nginx's default public directory
COPY frontend/ /usr/share/nginx/html/
# Copy custom Nginx configuration for routing
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose HTTP port
EXPOSE 80
# Start Nginx in the foreground (daemon off)
CMD ["nginx", "-g", "daemon off;"]
Backend Dockerfile (Dockerfile.back-end)
Create the backend container instruction file using Node.js 18 Alpine:
# Use lightweight Node.js 18 Alpine base image
FROM node:18-alpine
# Set working directory inside the container
WORKDIR /usr/src/app
# Copy package dependency manifests first to leverage Docker cache layers
COPY backend/package*.json ./
# Install application dependencies
RUN npm install
# Copy all remaining backend source code
COPY backend/ ./
# Expose backend API port
EXPOSE 50001
# Start the Node.js application
CMD ["npm", "start"]
7. Orchestrating Services with Docker Compose
Rather than managing containers individually, we define our entire 3-tier application stack inside a single orchestration manifest: docker-compose.yml.
The Complete docker-compose.yml File
Create and populate the compose file:
version: '3.8'
services:
# 1. Database Tier (MongoDB)
db:
image: mongo:latest
container_name: quickbite-db
restart: always
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
networks:
- quickbite-net
# 2. Backend Tier (Node.js API)
backend:
build:
context: .
dockerfile: Dockerfile.back-end
container_name: quickbite-backend
restart: always
ports:
- "50001:50001"
environment:
- MONGO_URI=mongodb://db:27017/foodiehub
- PORT=50001
depends_on:
- db
networks:
- quickbite-net
# 3. Frontend Tier (Nginx)
frontend:
build:
context: .
dockerfile: Dockerfile.front-end
container_name: quickbite-frontend
restart: always
ports:
- "80:80"
depends_on:
- backend
networks:
- quickbite-net
volumes:
mongodb_data:
driver: local
networks:
quickbite-net:
driver: bridge
8. Building and Launching the Stack
With our configuration files securely in place, spin up the entire application stack in detached mode:
# Build and start all containers in the background
docker compose up -d --build
Verifying Container Health
Check that all three containers (quickbite-frontend, quickbite-backend, and quickbite-db) are running smoothly:
docker ps
Expected output structure:
9. Real-World Pitfalls & Debugging Mindset
When deploying multi-container applications for the first time, developers frequently encounter common stumbling blocks. Here is how to diagnose and resolve them:
1. Build Context Errors (COPY failed: file not found)
- Symptom: Docker complains it cannot locate
frontend/orbackend/directories duringdocker compose up. - Root Cause: The build
contextindocker-compose.ymlwas set incorrectly (e.g., inside a subdirectory rather than the project root). - Fix: Ensure
context: .points to the root directory containing both source folders and your Dockerfiles.
2. YAML Indentation Validation Failures
- Symptom:
yaml: line X: mapping values are not allowed in this context. - Root Cause: YAML is strictly whitespace-sensitive. Mixing tabs and spaces or using incorrect indentation levels breaks the parser.
- Fix: Use 2 spaces for indentation and validate your syntax using online YAML linters or VS Code extensions.
3. Environment Variable Typoes (environments vs environment)
- Symptom: Docker ignores defined variables or throws property validation warnings.
- Root Cause: Using plural
environments:instead of the correct singularenvironment:key in Docker Compose.
10. Data Persistence & Volume Verification Test
A critical test of any production-grade architecture is proving that data survives container termination.
- Access the Web App: Navigate to
http://<your-ec2-public-ip>in your browser. - Create a User Account: Sign up with a username (e.g.,
harsh), password, and select a city (Kanpur). - Place an Order: Add items (e.g., Butter Chicken, Gulab Jamun) to your cart and complete checkout.
- Tear Down the Stack: Bring down all running containers to simulate a server crash or maintenance window:
BASH
docker compose down - Inspect Volume Preservation: Verify that the Docker volume retained our database records:
BASH
docker volume ls - Restart the Stack:
BASH
docker compose up -d - Verify Data Integrity: Log back into the QuickBite app with the user account created earlier. Notice that your account credentials and order history remain fully intact, proving the efficacy of our Docker volume mount (
mongodb_data:/data/db).
Summary and Key Takeaways
- 3-Tier Architecture Principles: Separating concerns into isolated frontend, backend, and database containers simplifies debugging, maintenance, and horizontal scaling.
- Lightweight Base Images: Leveraging
alpinevariants (likenginx:alpineandnode:18-alpine) drastically reduces image size and accelerates deployment times. - Custom Dockerfile Authoring: Writing bespoke Dockerfiles for frontend and backend components grants precise control over working directories, dependency installations, and startup commands.
- Declarative Orchestration: Docker Compose eliminates manual container linking by managing multi-container lifecycles through a single declarative YAML manifest.
- Automatic Bridge Networking: Docker Compose automatically provisions an isolated internal bridge network (
quickbite-net), allowing secure container-to-container communication. - Data Persistence with Volumes: Mapping container database directories (
/data/db) to named Docker volumes ensures business-critical data survives container restarts and teardowns. - Production Readiness: Combining health checks (
depends_on), environment variable injection, and restart policies (restart: always) produces resilient, production-ready cloud deployments.