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


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.

BASH
# 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

100%
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.

BASH
# 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.

BASH
# 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

  1. Deploy the MySQL container

▶ Example: Starting a MySQL container (Difficulty: ⭐⭐)

BASH
# 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
💡 Tip: Start MySQL first, and wait for it to finish initializing (about 30 seconds) before starting other containers. You can use 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

BASH
# 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.

BASH
# 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: ⭐⭐)

BASH
# 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
📌 Key Point: -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.

BASH
# 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
📌 Key Point: 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: ⭐⭐)

BASH
# 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: ⭐)

BASH
# Test PHP info page
curl http://localhost:8080/info.php | head -5

# Test database connection
curl http://localhost:8080/db-test.php
💻 Output:

TEXT
# curl http://localhost:8080/db-test.php
Connected to MySQL successfully!
MySQL version: 8.0.35
✅ Checkpoint: If you see "Connected to MySQL successfully!", it means the three containers are working together successfully: Nginx → PHP-FPM → MySQL.

(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: ⭐⭐⭐)

BASH
# 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;"
💻 Output:

TEXT
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
|  2 | Bob   |
+----+-------+
✅ Checkpoint: Data remains after the container is deleted and recreated—persistence for the named volume is working.


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)
💡 Tip: This lesson uses a manual startup sequence. In Phase 3, you’ll learn about Docker Compose depends_on to automate dependency management.


11. Complete Example: One-Click Deployment Script for the LEMP Stack

BASH
# ============================================
# 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
💻 Output (excerpt):

TEXT
# 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

Q Why are we starting them separately instead of using Compose?
A This is a comprehensive exercise for Phase 1, designed to help you understand how each container is created and connected independently. In Phase 3, you’ll learn about Docker Compose, which uses a single YAML file to replace these docker run commands. Starting manually before moving to automation helps you gain a deeper understanding.
Q How does Nginx connect to PHP-FPM?
A Through the FastCGI protocol. In the Nginx configuration, 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.
Q Is the data still there after the container restarts?
A Data mounted using a Named Volume (-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.
Q How do I find out which container is causing the error?
A Three-step troubleshooting method: ① 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.
Q How can I view logs from multiple containers at the same time?
A 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


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Deploy the LEMP stack from scratch, then visit http://localhost:8080/index.php to verify that the three containers are working together properly.
  2. 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.
  3. 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; visit http://localhost:8081 and log in with appuser/apppass.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏