Docker Level 4, Task 3: Deploying a Two-Tier Application with Docker Compose¶
Today's task was the culmination of everything I've learned about Docker Compose. I was tasked with defining and deploying a classic two-tier application stack—a PHP web server and a MariaDB database—using a single configuration file. This is a perfect, real-world example of how modern web applications are developed and deployed in a containerized environment.
I learned how to define multiple, interconnected "services" in one docker-compose.yml file, and how to use environment variables to securely configure the database on its first startup. This was a fantastic exercise in orchestrating a complete application stack with a single, simple command.
Table of Contents¶
- The Task
- My Step-by-Step Solution
- Why Did I Do This? (The "What & Why")
- Deep Dive: A Line-by-Line Breakdown of My Multi-Service Compose File
- Common Pitfalls
- Exploring the Commands Used
The Task¶
My objective was to deploy a two-container application stack on App Server 3 using a single Docker Compose file located at /opt/finance/docker-compose.yml.
Web Service Requirements:
- Container Name: php_host
- Image: php:apache
- Ports: Map host port 8088 to container port 80.
- Volumes: Map host directory /var/www/html to container directory /var/www/html.
Database Service Requirements:
- Container Name: mysql_host
- Image: mariadb:latest
- Ports: Map host port 3306 to container port 3306.
- Volumes: Map host directory /var/lib/mysql to container directory /var/lib/mysql.
- Environment: Set the database name to database_host and configure a custom user and password.
My Step-by-Step Solution¶
The solution involved preparing the host system with the necessary directories, writing a single YAML file to define the entire application, and then launching it with one command.
Phase 1: Preparing the Host and Writing the Compose File¶
- I connected to App Server 3:
ssh banner@stapp03. - I created all the required directories on the host machine at once. This is a crucial prerequisite for the bind mounts to work correctly.
sudo mkdir -p /opt/finance /var/www/html /var/lib/mysql - I created and edited the main configuration file:
sudo vi /opt/finance/docker-compose.yml. - Inside the editor, I wrote the complete definition for my two-tier application:
version: '3.8' services: web: image: php:apache container_name: php_host ports: - "8088:80" volumes: - /var/www/html:/var/www/html db: image: mariadb:latest container_name: mysql_host ports: - "3306:3306" volumes: - /var/lib/mysql:/var/lib/mysql environment: MYSQL_DATABASE: database_host MYSQL_USER: kodekloud_user MYSQL_PASSWORD: SomeVeryComplexPassword123! MYSQL_ROOT_PASSWORD: AnotherSecurePassword456! - I saved and quit the file.
Phase 2: Launching the Application Stack¶
- I navigated to the directory containing my configuration file. This is essential for the
docker composecommand to find the file automatically.cd /opt/finance - I launched the entire stack with a single command.
sudo docker compose up -d
Phase 3: Verification¶
The final step was to confirm that both containers were running and configured as I had defined them.
1. I ran sudo docker ps.
2. The output showed two running containers, php_host and mysql_host, with their respective port mappings listed correctly. This was the definitive proof that my docker-compose.yml file had successfully orchestrated the deployment of the entire application.
Why Did I Do This? (The "What & Why")¶
- Docker Compose: This is the standard tool for defining and running multi-container applications. This task perfectly illustrated its power. Instead of two long docker run commands and a separate docker network create command, I defined everything in one simple, version-controllable file.
- Two-Tier Architecture: This is a fundamental pattern in web development.
- Tier 1 (Web Service): The php_host container is the presentation layer. It handles incoming HTTP requests from users and runs the PHP application code.
- Tier 2 (DB Service): The mysql_host container is the data layer. It's responsible for storing and retrieving all the application's data. Separating these concerns makes the application more scalable and easier to manage.
- Environment Variables (environment key): This is the correct and secure way to pass configuration into a container. The official mariadb image is specifically designed to look for these environment variables (MYSQL_DATABASE, MYSQL_USER, etc.) on its very first startup. It uses them to automatically initialize the database, create the user, and set the passwords. This is a powerful automation feature that avoids having to manually configure the database after it starts.
Deep Dive: A Line-by-Line Breakdown of My Multi-Service Compose File¶
This docker-compose.yml file defines two distinct but related services. Docker Compose automatically creates a shared network for them so they can communicate.
[Image of a two-tier application stack with a web and DB container]
# Specifies the version of the Docker Compose file format.
version: '3.8'
# The top-level key where all the application's services (containers) are defined.
services:
# This is the definition for my first service, the web server.
# 'web' is the service name. On the internal Docker network, the 'db' service
# could connect to this service using the hostname 'web'.
web:
image: php:apache # The image to use (PHP with Apache built-in).
container_name: php_host # The specific name for the container.
ports:
- "8088:80" # Maps host port 8088 to container port 80.
volumes:
- /var/www/html:/var/www/html # Maps the host's web root to the container's web root.
# This is the definition for my second service, the database.
db:
image: mariadb:latest # The image to use.
container_name: mysql_host # The specific name for the container.
ports:
- "3306:3306" # Maps the standard MySQL port.
volumes:
- /var/lib/mysql:/var/lib/mysql # Persists the database data on the host.
# This block passes environment variables directly into the container.
# The MariaDB image uses these variables to auto-configure itself on first run.
environment:
MYSQL_DATABASE: database_host # Creates the database.
MYSQL_USER: kodekloud_user # Creates the user.
MYSQL_PASSWORD: SomeVeryComplexPassword123! # Sets the user's password.
MYSQL_ROOT_PASSWORD: AnotherSecurePassword456! # Sets the root password.
Common Pitfalls¶
- Forgetting to Create Host Directories: If the host directories for the bind mounts (/var/www/html, /var/lib/mysql) didn't exist, Docker would create them as root. This can lead to permission errors where the application inside the container can't write to the volume. Creating them beforehand is a best practice.
- YAML Indentation Errors: A single incorrect space can invalidate the entire file. All keys under a service (image, ports, etc.) must have the same indentation.
- Using the Wrong Environment Variable Names: The official mariadb image looks for very specific variable names (e.g., MYSQL_ROOT_PASSWORD). Using a different name (e.g., ROOT_PASSWORD) would cause the auto-configuration to fail. Always check the image's documentation on Docker Hub.
- Forgetting -d: Running docker compose up without the -d flag would attach my terminal to the logs of both containers, which can be very noisy and confusing.
Exploring the Commands Used¶
- sudo mkdir -p [paths...]: A single command to create multiple directories and their parent directories if they don't exist. I used this to prepare the host.
- sudo vi /opt/finance/docker-compose.yml: Creates and edits the YAML configuration file with root privileges.
- sudo docker compose up -d: The main command for this task.
- docker compose: The main command for interacting with the Docker Compose tool.
- up: The subcommand to create and start the application stack defined in the .yml file.
- -d: Runs the containers in detached (background) mode.
- sudo docker ps: The standard Docker command to list running containers. I used this to verify that both my php_host and mysql_host containers were successfully launched and running.