Phase 1 Mini Project
Now let’s tie together what we’ve learned in the first five lessons—deploying a complete LEMP stack (Linux + Nginx + MySQL + PHP) from scratch. This is the comprehensive final project for Phase 1.
1. What You'll Learn
- Design of a Multi-Container Collaboration Architecture
- Data Volume Persistence Strategies
- Network Communication Between Containers
- Environment Variable Configuration Management
- Container Troubleshooting and Repair
2. A True Story of a Backend Developer
(1) Pain Point: Setting up a development environment manually takes half a day
Alice needs to set up a development environment for the company’s internal management system: Nginx will serve static content, PHP-FPM will handle dynamic requests, and MySQL will store data. Manual installation requires configuring Nginx virtual hosts, installing PHP-FPM and its extensions, installing and initializing MySQL, and configuring the communication paths between the three components. There are potential pitfalls at every step; it took a colleague an entire day to get everything set up.
(2) Solutions for Multi-Container Deployments in Docker
Alice used Docker to set up a complete LEMP stack in 10 minutes—three containers, one network, and two data volumes.
# Architecture overview: Nginx → PHP-FPM → MySQL
docker network create lemp-net
docker volume create mysql-data
(3) Benefits: 10 minutes vs. half a day
Manual deployment takes half a day (and isn’t reusable), while Docker deployment takes 10 minutes (scripts are reusable and can be shared across the team). Onboarding new team members has been reduced from a full day of environment setup to 15 minutes.
3. LEMP Architecture Design
(1) Architecture Overview
graph TB
USER["User Browser<br/>:8080"] --> NGX["Nginx Container<br/>:80<br/>Static files + PHP proxy"]
NGX -->|"FastCGI :9000"| PHP["PHP-FPM Container<br/>:9000<br/>PHP processing"]
PHP -->|"MySQL :3306"| DB["MySQL Container<br/>:3306<br/>Database"]
DB --> VOL["Named Volume<br/>mysql-data<br/>Persistent storage"]
PHP --> PHPV["Bind Mount<br/>./src → /var/www/html<br/>PHP source code"]
NGX --> NGXV["Bind Mount<br/>./nginx.conf<br/>Nginx config"]
(2) Comparison of Manual Deployment vs. Docker Deployment
| Dimension | Manual Deployment | Docker Deployment |
|---|---|---|
| Time required | 4–8 hours | 10 minutes |
| Reproducibility | Low (depends on operator experience) | High (scripts/commands can be reused) |
| Portability | Low (OS-specific) | High (consistent across platforms) |
| Environment Isolation | None (shared system environment) | Fully isolated |
| Rollback | Difficult (uninstall and reinstall) | Easy (delete container and rebuild) |
| Team Sharing | Write documentation + follow instructions | Share commands/scripts |
4. Preparations: Network and Volumes
(1) Create a Custom Network
Communication between containers requires a custom bridge network—the default bridge network does not support DNS resolution of container names.
# Create a custom bridge network for LEMP stack
docker network create lemp-net
# Verify
docker network ls | grep lemp-net
(2) Create a data volume
MySQL data must be persisted—the data must not be lost when the container is deleted.
# Create a named volume for MySQL data
docker volume create mysql-data
# Verify
docker volume ls | grep mysql-data
(3) Comparison of Volume Mounting Strategies
| Strategy | Syntax | Use Cases | Persistence |
|---|---|---|---|
| Named Volume | -v mysql-data:/var/lib/mysql |
database, persistent storage | ✅ Retained after container deletion |
| Bind Mount | -v ./src:/var/www/html |
Development Environment Code Sync | ✅ Map to Host Directory |
| tmpfs | --tmpfs /tmp |
Temporary data, sensitive information | ❌ Disappears when the container stops |
- Deploy the MySQL container
▶ Example: Starting a MySQL container (Difficulty: ⭐⭐)
# Start MySQL 8.0 with persistent storage
docker run -d \
--name lemp-mysql \
--network lemp-net \
-e MYSQL_ROOT_PASSWORD=rootsecret \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-v mysql-data:/var/lib/mysql \
--restart=unless-stopped \
mysql:8.0
docker logs -f lemp-mysql to monitor the initialization progress; once you see "ready for connections," initialization is complete.
(1) Verify that MySQL is running
# Check MySQL is running
docker ps --filter name=lemp-mysql
# Connect to MySQL to verify
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e "SELECT 1;"
6. Deploying the PHP-FPM Container
(1) Prepare the PHP code
First, create a PHP test file and a database connection test file.
# Create project directory
mkdir -p lemp-project/src
# Create PHP info page
cat > lemp-project/src/info.php << 'EOF'
<?php
phpinfo();
?>
EOF
# Create database connection test
cat > lemp-project/src/db-test.php << 'EOF'
<?php
$host = 'lemp-mysql';
$db = 'appdb';
$user = 'appuser';
$pass = 'apppass';
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "Connected to MySQL successfully!<br>";
echo "MySQL version: " . $pdo->query("SELECT VERSION()")->fetchColumn();
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
?>
EOF
▶ Example: Starting a PHP-FPM container (Difficulty: ⭐⭐)
# Start PHP-FPM container
docker run -d \
--name lemp-php \
--network lemp-net \
-v $(pwd)/lemp-project/src:/var/www/html \
--restart=unless-stopped \
php:8.2-fpm
-v $(pwd)/lemp-project/src:/var/www/html is a bind mount—it maps a local code directory to the container, so changes made to local files take effect immediately within the container, making it suitable for development and debugging.
7. Deploy the Nginx Container
(1) Prepare the Nginx configuration
Nginx needs to be configured as a FastCGI proxy to forward PHP requests to the PHP-FPM container.
# Create Nginx configuration
cat > lemp-project/nginx.conf << 'EOF'
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass lemp-php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
EOF
lemp-php in fastcgi_pass lemp-php:9000 is the PHP-FPM container name. Since they are in the same custom network lemp-net, Docker DNS automatically resolves the container name to the container's IP address.
▶ Example: Starting an Nginx container (Difficulty: ⭐⭐)
# Start Nginx container
docker run -d \
--name lemp-nginx \
--network lemp-net \
-p 8080:80 \
-v $(pwd)/lemp-project/src:/var/www/html \
-v $(pwd)/lemp-project/nginx.conf:/etc/nginx/conf.d/default.conf \
--restart=unless-stopped \
nginx:1.25-alpine
8. Validation and Testing
▶ Example: Accessing a PHP page for verification (Difficulty: ⭐)
# Test PHP info page
curl http://localhost:8080/info.php | head -5
# Test database connection
curl http://localhost:8080/db-test.php
# curl http://localhost:8080/db-test.php
Connected to MySQL successfully!
MySQL version: 8.0.35
(1) Complete Verification Checklist
| Test Item | Command | Expected Result |
|---|---|---|
| All three containers are running | docker ps |
3 containers are in the "Up" state |
| PHP page displays normally | curl localhost:8080/info.php |
Return PHP configuration information |
| Database connection is normal | curl localhost:8080/db-test.php |
"Connected successfully" |
| DNS Resolution Between Containers | docker exec lemp-nginx ping -c 2 lemp-php |
Can be pinged |
| Data volume exists | docker volume ls |
mysql-data is in the list |
9. Data Persistence Validation
▶ Example: Simulating a MySQL container crash (Difficulty: ⭐⭐⭐)
# 1. Create a test table with data
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e \
"CREATE TABLE test_items (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100)); INSERT INTO test_items (name) VALUES ('Alice'), ('Bob');"
# 2. Verify data exists
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e \
"SELECT * FROM test_items;"
# 3. Stop and remove the MySQL container
docker stop lemp-mysql && docker rm lemp-mysql
# 4. Recreate MySQL container with the same volume
docker run -d \
--name lemp-mysql \
--network lemp-net \
-e MYSQL_ROOT_PASSWORD=rootsecret \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-v mysql-data:/var/lib/mysql \
--restart=unless-stopped \
mysql:8.0
# 5. Wait for MySQL to start, then verify data is still there
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e \
"SELECT * FROM test_items;"
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
| 2 | Bob |
+----+-------+
10. Container Ordering and Dependency Policies
| Strategy | Method | Reliability |
|---|---|---|
| Manual Sequential Startup | MySQL → PHP → Nginx | Medium (depends on experience) |
| Health Check Pending | Wait for MySQL to be ready before restarting PHP | High (Automated) |
| Restart Strategy | --restart=on-failure Automatically retry failed services |
Medium (order not guaranteed) |
| Docker Compose | depends_on + Health Checks (Phase 3 Study) |
High (Declarative) |
depends_on to automate dependency management.
11. Complete Example: One-Click Deployment Script for the LEMP Stack
# ============================================
# Complete walkthrough: Deploy LEMP stack from scratch
# Covers: network, volume, MySQL, PHP, Nginx
# ============================================
# --- Setup ---
# Create network and volume
docker network create lemp-net
docker volume create mysql-data
# Create project directory and files
mkdir -p lemp-project/src
cat > lemp-project/src/index.php << 'PHPEOF'
<?php
$host = 'lemp-mysql';
$db = 'appdb';
$user = 'appuser';
$pass = 'apppass';
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
echo "<h1>LEMP Stack is working!</h1>";
echo "<p>MySQL version: " . $pdo->query("SELECT VERSION()")->fetchColumn() . "</p>";
} catch (PDOException $e) {
echo "<h1>Database connection failed</h1><p>" . $e->getMessage() . "</p>";
}
?>
PHPEOF
cat > lemp-project/nginx.conf << 'NGINXEOF'
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass lemp-php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
NGINXEOF
# --- Deploy ---
# 1. Start MySQL (must be first)
docker run -d \
--name lemp-mysql \
--network lemp-net \
-e MYSQL_ROOT_PASSWORD=rootsecret \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-v mysql-data:/var/lib/mysql \
--restart=unless-stopped \
mysql:8.0
# 2. Wait for MySQL to initialize (~30 seconds)
echo "Waiting for MySQL to initialize..."
sleep 30
# 3. Start PHP-FPM
docker run -d \
--name lemp-php \
--network lemp-net \
-v $(pwd)/lemp-project/src:/var/www/html \
--restart=unless-stopped \
php:8.2-fpm
# 4. Start Nginx
docker run -d \
--name lemp-nginx \
--network lemp-net \
-p 8080:80 \
-v $(pwd)/lemp-project/src:/var/www/html \
-v $(pwd)/lemp-project/nginx.conf:/etc/nginx/conf.d/default.conf \
--restart=unless-stopped \
nginx:1.25-alpine
# --- Verify ---
echo "Verifying deployment..."
docker ps
curl -s http://localhost:8080/index.php
# docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
c1d2e3f4 nginx:1.25-alpine Up 5 min 0.0.0.0:8080->80/tcp lemp-nginx
b2c3d4e5 php:8.2-fpm Up 5 min 9000/tcp lemp-php
a1b2c3d4 mysql:8.0 Up 5 min 3306/tcp, 33060/tcp lemp-mysql
# curl http://localhost:8080/index.php
<h1>LEMP Stack is working!</h1><p>MySQL version: 8.0.35</p>
❓ FAQ
docker run commands. Starting manually before moving to automation helps you gain a deeper understanding.fastcgi_pass lemp-php:9000 points to the PHP-FPM container. lemp-php is the container name; within the same custom network, Docker DNS automatically resolves it to the container’s IP address, so there’s no need to hard-code the IP address.-v mysql-data:/var/lib/mysql) remains even after the container is deleted. However, the source code for a Bind Mount (-v ./src:/var/www/html) is mapped to a host directory; as long as the host directory isn’t deleted, the code won’t be lost.docker ps -a Check which container’s status is not “Up”; ② docker logs <container> Check the error logs; ③ docker inspect <container> --format='{{.State.ExitCode}}' Check the exit code. Common issue: PHP connection errors occur when MySQL is not ready; simply wait for MySQL to finish initializing.docker compose logs (Learned in Phase 3) allows you to aggregate and view them. Currently, you can: ① docker logs each container individually; ② docker logs -f open multiple terminal windows to monitor them simultaneously; ③ redirect the logs to a file and merge them for viewing.📖 Summary
- LEMP stack = Linux + Nginx + MySQL + PHP, a classic web application architecture
- Custom Network
lemp-net: Enables communication between containers via DNS resolution using container names - Named Volume for persisting database data; Bind Mount for synchronizing development code
- Startup order: MySQL (initializes first) → PHP-FPM → Nginx (exposes the port last)
fastcgi_pass lemp-php:9000Implementing FastCGI Communication Between Containers- Data persistence verification: After deleting the MySQL container and recreating it, the data in the Named Volume remains intact.
📝 Exercises
- Basic Exercise (Difficulty: ⭐): Deploy the LEMP stack from scratch, then visit
http://localhost:8080/index.phpto verify that the three containers are working together properly. - Advanced Exercise (Difficulty ⭐⭐): Simulate a MySQL container crash (
docker stop lemp-mysql && docker rm lemp-mysql), and after rebuilding it, verify whether the database data has been persisted. - Challenge (Difficulty: ⭐⭐⭐): Add an Adminer (database management tool) container to connect to MySQL, and use the web interface to manage the database. Hint:
docker run -d --name adminer --network lemp-net -p 8081:8080 adminer; visithttp://localhost:8081and log in with appuser/apppass.



