DevOps Day 46: Deploying a Two-Tier Application with Docker Compose¶
Today's task was the perfect culmination of my Docker Compose learning. My objective was to define and deploy a classic two-tier application stack—a PHP web server and a MariaDB database—using a single, declarative configuration file. This is a perfect, real-world example of how modern web applications are developed and deployed in a containerized environment.
This exercise was a fantastic demonstration of orchestrating multiple, interconnected services. I learned how to define each service, manage their networking, persist their data with volumes, and securely configure the database on its first startup using environment variables. This document is my very detailed, first-person guide to that entire successful process.
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 I Used
The Task¶
My objective was to deploy a two-container application stack on App Server 1 using a single Docker Compose file located at /opt/itadmin/docker-compose.yml.
Web Service Requirements:
- Container Name: php_apache
- Image: php:apache
- Ports: Map host port 8089 to container port 80.
- Volumes: Map host directory /var/www/html to container directory /var/www/html.
Database Service Requirements:
- Container Name: mysql_apache
- 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_apache 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 1:
ssh tony@stapp01. - I created all the required directories on the host machine at once. This is a crucial prerequisite for the bind mounts to work correctly and have the right permissions from the start.
sudo mkdir -p /opt/itadmin /var/www/html /var/lib/mysql - I created and edited the main configuration file:
sudo vi /opt/itadmin/docker-compose.yml. - Inside the editor, I wrote the complete definition for my two-tier application, paying close attention to the YAML indentation.
version: '3.8' services: web: image: php:apache container_name: php_apache ports: - "8089:80" volumes: - /var/www/html:/var/www/html db: image: mariadb:latest container_name: mysql_apache ports: - "3306:3306" volumes: - /var/lib/mysql:/var/lib/mysql environment: MYSQL_DATABASE: database_apache 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/itadmin - I launched the entire stack with a single, simple 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_apache and mysql_apache, 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.
3. A final test from the jump host (curl http://stapp01:8089/) returned a response, confirming the web service was externally accessible.
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_apache container is the presentation layer. It handles incoming HTTP requests from users and runs the PHP application code.
- Tier 2 (DB Service): The mysql_apache 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.
- Volumes (volumes key): This is how I ensured the application's data would be persistent. By mapping the host's /var/lib/mysql directory into the container, I'm telling Docker to store all the database files on the host machine. This means that even if I remove and recreate the mysql_apache container, the data will still be there.
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 using their service names as hostnames.
[Image of a two-tier application stack with a web and DB container]
# Specifies the version of the Docker Compose file format. '3.8' is a modern, common version.
version: '3.8'
# This is 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_apache # The specific name for the container.
ports:
- "8089:80" # Maps host port 8089 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_apache # 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_apache # Creates the database.
MYSQL_USER: kodekloud_user # Creates the user.
MYSQL_PASSWORD: SomeVeryComplexPassword123! # Sets the user's password.
# This is mandatory for the mariadb image to initialize properly.
MYSQL_ROOT_PASSWORD: AnotherSecurePassword456!
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, and all services (web, db) 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 I 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/itadmin/docker-compose.yml: Creates and edits the YAML configuration file with root privileges.
- cd /opt/itadmin: A critical step to change directory into the location of the compose file before running it.
- 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_apache and mysql_apache containers were successfully launched and running.