A Complete Step-by-Step Deep Dive MVP Tutorial
Welcome to the most comprehensive Docker tutorial for creating a complete web infrastructure! This tutorial follows the Inception project specifications and will teach you everything from basic Docker concepts to advanced containerization techniques.
π― What You'll Build
By the end of this tutorial, you'll have:
- β NGINX reverse proxy with TLS encryption
- β WordPress with PHP-FPM
- β MariaDB database
- β Docker networking and volumes
- β Complete MVP infrastructure
Part 1: Understanding the Fundamentals
What is Docker? π³
Think of Docker as a shipping container for your applications. Just like how physical containers revolutionized shipping by making it possible to transport goods consistently across different ships, trucks, and trains, Docker containers package your application with everything it needs to run consistently across different environments.
π€ Reflection Question: If you had to explain Docker to a non-technical friend in one sentence, what would you say?
Docker vs Virtual Machines
Aspect | Docker Containers | Virtual Machines |
---|---|---|
Resource Usage | Share host OS kernel | Each VM has full OS |
Startup Time | Seconds | Minutes |
Memory | MB | GB |
Portability | High | Medium |
π Think About It: Why would you choose containers over VMs for a web application?
Part 2: Setting Up Your Environment
Prerequisites
- Virtual Machine (Ubuntu 22.04 or 24.04 recommended)
- Root or sudo access
- Basic command line knowledge
Installing Docker
# Update package index
sudo apt update
# Install required packages
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release
# Add Docker's GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Add your user to docker group
sudo usermod -aG docker $USER
# Log out and back in, then test
docker --version
π§ͺ Experiment: Run docker run hello-world
. What do you think this command does?
Part 3: Docker Basics - Building Blocks
Understanding Key Concepts
Images vs Containers
# List images
docker images
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
π€ Question: What's the difference between an image and a container? Think of it like the difference between a recipe and a cake!
Your First Container
# Run a simple nginx container
docker run -d -p 8080:80 --name my-nginx nginx
π§ͺ What do you think will happen if you run this command?
- What does
-d
do? - What does
-p 8080:80
mean? - What does
--name my-nginx
do?
Visit http://localhost:8080
to see the result!
Part 4: Creating Your First Dockerfile
What is a Dockerfile?
A Dockerfile is like a recipe that tells Docker how to build your image.
# Simple nginx Dockerfile
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
EXPOSE 80
π Breaking it down:
-
FROM
: What base image to start with -
COPY
: Copy files from host to container (same folder that the Dockerfile) -
EXPOSE
: Document which port the container uses
Building Your Image
# Build the image
docker build -t my-custom-nginx .
# Run your custom image
docker run -d -p 8080:80 my-custom-nginx
π§ͺ Experiment: Create an index.html
file with your name and build the image. What happens?
Part 5: The Inception Project Architecture
Before we dive into building, let's understand what we're creating:
Project Structure
inception/
βββ Makefile
βββ secrets/
β βββ db_password.txt
β βββ db_root_password.txt
β βββ credentials.txt
βββ srcs/
βββ docker-compose.yml
βββ .env
βββ requirements/
βββ mariadb/
β βββ Dockerfile
β βββ conf/
β βββ tools/
βββ nginx/
β βββ Dockerfile
β βββ conf/
β βββ tools/
βββ wordpress/
βββ Dockerfile
βββ conf/
βββ tools/
π€ Reflection: Why do you think we separate each service into its own directory?
Part 6: Setting Up Docker Networks
Understanding Docker Networking
# Create a custom network
docker network create inception-network
# List networks
docker network ls
# Inspect the network
docker network inspect inception-network
π Think: Why can't we just use the default network?
The default bridge network has limitations:
- Containers can only communicate by IP address
- No automatic DNS resolution
- Less secure
π§ͺ Experiment: Create two containers on the same custom network and try to ping one from the other using container names.
# Connect a running container
docker network connect inception-network <container_id_or_name>
# Create a httpd apache server connected to the network
docker run --rm --name client --network inception-network -d httpd
# To enter shell
docker exec -it client sh
# If ping is missing
apk add iputils # For Alpine
apt-get update && apt-get install iputils-ping # For Debian/Ubuntu
# To get an IP address
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <container_id_or_name>
Part 7: MariaDB Container - The Database Layer
Creating the MariaDB Dockerfile
FROM debian:bullseye
# Install MariaDB
RUN apt-get update && apt-get install -y \
mariadb-server \
&& rm -rf /var/lib/apt/lists/*
# Create directories
RUN mkdir -p /var/run/mysqld \
&& chown -R mysql:mysql /var/run/mysqld \
&& chmod 755 /var/run/mysqld
# Copy configuration
COPY conf/50-server.cnf /etc/mysql/mariadb.conf.d/
COPY tools/init_db.sh /usr/local/bin/
# Set permissions
RUN chmod +x /usr/local/bin/init_db.sh
# Environment variables (can be overridden at runtime)
ENV MYSQL_ROOT_PASSWORD=root \
MYSQL_DATABASE=app_db \
MYSQL_USER=app_user \
MYSQL_PASSWORD=app_pass
EXPOSE 3306
ENTRYPOINT ["init_db.sh"]
π€ Questions to ponder:
- Why are we creating
/var/run/mysqld
? - What does
chown
do? - Why do we need an initialization script?
MariaDB Configuration File
Create conf/50-server.cnf
:
[mysqld]
bind-address = 0.0.0.0
port = 3306
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
log-error = /var/log/mysql/error.log
pid-file = /var/run/mysqld/mysqld.pid
π Critical thinking: Why is bind-address = 0.0.0.0
important here?
Database Initialization Script
Create tools/init_db.sh
:
#!/bin/bash
set -e
echo "π§ Starting MariaDB initialization..."
# Initialize MySQL data directory if it doesn't exist
if [ ! -d "/var/lib/mysql/mysql" ]; then
echo "π Initializing data directory..."
mysql_install_db --user=mysql --datadir=/var/lib/mysql > /dev/null
fi
# Start the server (no networking for setup)
echo "π Starting temporary MariaDB server for setup..."
mysqld --skip-networking --socket=/run/mysqld/mysqld.sock --user=mysql &
pid="$!"
# Wait for MariaDB to be ready
echo "β³ Waiting for MariaDB to be ready..."
until mysqladmin --socket=/run/mysqld/mysqld.sock ping >/dev/null 2>&1; do
sleep 1
done
echo "β
MariaDB is ready!"
# Run setup SQL: create database and users
echo "βοΈ Running setup SQL..."
mysql --socket=/run/mysqld/mysqld.sock -u root << EOF
ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}';
CREATE DATABASE IF NOT EXISTS ${MYSQL_DATABASE};
CREATE USER IF NOT EXISTS '${MYSQL_USER}'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}';
GRANT ALL PRIVILEGES ON ${MYSQL_DATABASE}.* TO '${MYSQL_USER}'@'%';
FLUSH PRIVILEGES;
EOF
# Shut down temporary server
echo "π Shutting down temporary MariaDB..."
mysqladmin --socket=/run/mysqld/mysqld.sock -u root -p"${MYSQL_ROOT_PASSWORD}" shutdown
# Wait for shutdown
wait "$pid" || true
# Start MariaDB normally (with networking)
echo "β
Initialization complete. Starting MariaDB..."
exec mysqld --user=mysql --datadir=/var/lib/mysql --socket=/run/mysqld/mysqld.sock
π§ͺ Experiment Question: What would happen if we didn't wait for MySQL to start before creating the database?
# To check if socket file exist on container
docker exec -it <container_id_or_name> ls -l /run/mysqld/mysqld.sock
# To check maria db
docker exec -it <container_id_or_name> mysqladmin ping -u root -p
Part 8: WordPress with PHP-FPM Container
Understanding PHP-FPM
PHP-FPM (FastCGI Process Manager) is a PHP implementation that's perfect for serving high-traffic sites. Unlike running PHP as an Apache module, PHP-FPM runs as a separate process.
π€ Why use PHP-FPM instead of Apache with mod_php?
- Better performance under high load
- Separate process isolation
- Better resource management
WordPress Dockerfile
FROM debian:bookworm
# Install PHP-FPM and required extensions
RUN apt-get update && apt-get install -y \
php8.2-fpm \
php8.2-mysql \
php8.2-curl \
php8.2-gd \
php8.2-intl \
php8.2-mbstring \
php8.2-soap \
php8.2-xml \
php8.2-zip \
wget \
curl \
&& ln -s /usr/sbin/php-fpm8.2 /usr/local/bin/php-fpm \
&& rm -rf /var/lib/apt/lists/*
# Create WordPress directory
WORKDIR /var/www/html
# Copy PHP-FPM configuration (correct version)
COPY conf/www.conf /etc/php/8.2/fpm/pool.d/
# Copy and set permissions for setup script
COPY tools/setup_wordpress.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/setup_wordpress.sh
EXPOSE 9000
ENTRYPOINT ["/usr/local/bin/setup_wordpress.sh"]
π Reflection: Why do we need so many PHP extensions?
PHP-FPM Configuration
Create conf/www.conf
:
[www]
user = www-data
group = www-data
listen = 9000
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
π€ Challenge Question: What would happen if you set pm.max_children = 1
on a busy website?
WordPress Setup Script
Create tools/setup_wordpress.sh
:
Note: If the variable is only used by the script itself, no need for export
. If it must be seen by another process (e.g., PHP, nginx, Python, etc.), then you should export
it.
#!/bin/bash
set -e
WP_PATH="/var/www/html"
# Read password from secret file
if [ -n "$WORDPRESS_DB_PASSWORD_FILE" ] && [ -f "$WORDPRESS_DB_PASSWORD_FILE" ]; then
WORDPRESS_DB_PASSWORD=$(cat "$WORDPRESS_DB_PASSWORD_FILE")
export WORDPRESS_DB_PASSWORD
fi
echo "π¦ Setting up WordPress..."
# Download and configure WordPress if not present
if [ ! -f "$WP_PATH/wp-config.php" ]; then
echo "β¬οΈ Downloading WordPress..."
wget -q https://wordpress.org/latest.tar.gz -O /tmp/wordpress.tar.gz
tar -xzf /tmp/wordpress.tar.gz -C /tmp
rm /tmp/wordpress.tar.gz
# Copy only missing files (avoid overwriting existing content)
cp -rn /tmp/wordpress/* "$WP_PATH" || true
rm -rf /tmp/wordpress
# Fetch security salts from WordPress API
WP_SALTS=$(wget -qO- https://api.wordpress.org/secret-key/1.1/salt/)
# Create wp-config.php
cat > "$WP_PATH/wp-config.php" << EOF
<?php
define('DB_NAME', '${WORDPRESS_DB_NAME}');
define('DB_USER', '${WORDPRESS_DB_USER}');
define('DB_PASSWORD', '${WORDPRESS_DB_PASSWORD}');
define('DB_HOST', '${WORDPRESS_DB_HOST}');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
\$table_prefix = '${WORDPRESS_TABLE_PREFIX:-wp_}';
${WP_SALTS}
define('WP_DEBUG', false);
if ( !defined('ABSPATH') )
define('ABSPATH', __DIR__ . '/');
require_once ABSPATH . 'wp-settings.php';
EOF
# Set secure permissions
find "$WP_PATH" -type d -exec chmod 750 {} \;
find "$WP_PATH" -type f -exec chmod 640 {} \;
chown -R www-data:www-data "$WP_PATH"
echo "β
WordPress setup complete."
else
echo "βοΈ WordPress already initialized, skipping setup."
fi
echo "π Starting PHP-FPM..."
exec php-fpm8.2 -F
π§ͺ Experiment: What do you think happens if WordPress files already exist?
# If we want to test wp container we can use this:
docker run -d \
-e WORDPRESS_DB_HOST=dummyhost \
-e WORDPRESS_DB_NAME=dummydb \
-e WORDPRESS_DB_USER=dummyuser \
-e WORDPRESS_DB_PASSWORD=dummypassword \
mywp
# Then
docker exec -it docker_id_name cat /var/www/html/wp-config.php
# And then check with:
cat /var/www/html/wp-config.php
Part 9: NGINX with TLS - The Front Door
Understanding NGINX's Role
NGINX acts as a reverse proxy, meaning it:
- Receives requests from users
- Forwards them to PHP-FPM
- Returns the response to users
π€ Why not access PHP-FPM directly?
- PHP-FPM doesn't handle HTTP directly
- NGINX handles static files efficiently
- SSL termination
NGINX Dockerfile
FROM debian:bullseye
# Install nginx and openssl
RUN apt-get update && apt-get install -y \
nginx \
openssl \
&& rm -rf /var/lib/apt/lists/*
# Ensure nginx runs as www-data with correct UID
RUN usermod -u 33 www-data && groupmod -g 33 www-data
# Create SSL directory
RUN mkdir -p /etc/nginx/ssl
# Copy configuration files
COPY conf/nginx.conf /etc/nginx/nginx.conf
COPY tools/generate_ssl.sh /usr/local/bin/
# Set permissions
RUN chmod +x /usr/local/bin/*.sh
EXPOSE 443
ENTRYPOINT ["/usr/local/bin/generate_ssl.sh"]
SSL Certificate Generation Script
Create tools/generate_ssl.sh
:
#!/bin/bash
set -e
# Ensure SSL directory exists
mkdir -p /etc/nginx/ssl
# Default domain if not provided
: "${DOMAIN_NAME:=localhost}"
# Generate SSL certificate if it doesn't exist
if [ ! -f /etc/nginx/ssl/nginx.crt ]; then
echo "π Generating self-signed SSL certificate for ${DOMAIN_NAME}..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/nginx.key \
-out /etc/nginx/ssl/nginx.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=${DOMAIN_NAME}"
chmod 600 /etc/nginx/ssl/nginx.key
chmod 644 /etc/nginx/ssl/nginx.crt
echo "β
SSL certificate generated at /etc/nginx/ssl/"
else
echo "βΉοΈ SSL certificate already exists. Skipping generation."
fi
# Test nginx configuration before starting
echo "π§ͺ Testing nginx configuration..."
nginx -t
echo "β
Nginx configuration test passed."
# Start nginx in foreground (PID 1)
echo "π Starting Nginx..."
exec nginx -g "daemon off;"
π Security Question: Why do we set different permissions for the key and certificate files?
NGINX Configuration
Create conf/nginx.conf
:
user www-data;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name ${DOMAIN_NAME};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name ${DOMAIN_NAME};
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
root /var/www/html;
index index.php index.html;
client_max_body_size 64M;
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.ht {
deny all;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1M;
access_log off;
add_header Cache-Control "public";
}
}
}
π§ͺ Critical Analysis:
- Why does
fastcgi_pass
usewordpress:9000
instead of an IP address? - What does
try_files
do?
# Check certificate
docker exec -it docker_id_name ls /etc/nginx/ssl/
Part 10: Docker Volumes - Data Persistence
Understanding Docker Volumes
π€ What happens to data when a container is deleted?
Without volumes, all data is lost! Volumes provide persistent storage that survives container restarts and deletions.
# Create volumes
docker volume create mariadb_data
docker volume create wordpress_data
# Inspect a volume
docker volume inspect mariadb_data
π Think: Where does Docker store volume data on the host?
Volume Types Comparison
Type | Use Case | Performance | Portability |
---|---|---|---|
Named Volumes | Databases, app data | High | High |
Bind Mounts | Development, logs | Medium | Low |
tmpfs Mounts | Temporary data | Highest | N/A |
Part 11: Environment Variables and Secrets
The Security Challenge
β NEVER do this in production:
ENV MYSQL_ROOT_PASSWORD=supersecret
β Instead, use environment variables:
Create .env
file:
DOMAIN_NAME=login.42.fr
MYSQL_ROOT_PASSWORD=secure_root_password
MYSQL_DATABASE=wordpress_db
MYSQL_USER=wp_user
MYSQL_PASSWORD=secure_user_password
# WordPress settings
WORDPRESS_DB_NAME=wordpress_db
WORDPRESS_DB_USER=wp_user
WORDPRESS_DB_PASSWORD=secure_user_password
WORDPRESS_DB_HOST=mariadb
WORDPRESS_TABLE_PREFIX=wp_
π Security Best Practice: Store passwords in separate files:
# Create secrets directory
mkdir -p secrets
echo "root" > secrets/db_root_password.txt
echo "pass" > secrets/db_password.txt
# Set strict permissions
chmod 600 secrets/*
π€ Security Question: Why are environment variables not ideal for secrets?
Part 12: Docker Compose - Orchestrating Everything
What is Docker Compose?
Docker Compose allows you to define and run multi-container applications using a YAML file.
Create srcs/docker-compose.yml
:
services:
mariadb:
build:
context: requirements/mariadb
image: mariadb:inception
container_name: mariadb
networks:
- inception-network
volumes:
- mariadb_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_root_password
- db_password
restart: always
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 10s
retries: 5
wordpress:
build:
context: requirements/wordpress
image: wordpress:inception
container_name: wordpress
depends_on:
mariadb:
condition: service_healthy
networks:
- inception-network
volumes:
- wordpress_data:/var/www/html
environment:
WORDPRESS_DB_HOST: mariadb
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
WORDPRESS_TABLE_PREFIX: wp_
secrets:
- db_password
restart: always
expose:
- "9000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000"]
interval: 10s
timeout: 5s
retries: 5
nginx:
build:
context: requirements/nginx
image: nginx:inception
container_name: nginx
depends_on:
wordpress:
condition: service_healthy
ports:
- "443:443"
networks:
- inception-network
volumes:
- wordpress_data:/var/www/html
environment:
DOMAIN_NAME: ${DOMAIN_NAME}
restart: always
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 10s
retries: 3
networks:
inception-network:
driver: bridge
volumes:
mariadb_data:
driver: local
driver_opts:
type: none
o: bind
device: /home/${USER}/data/mariadb
wordpress_data:
driver: local
driver_opts:
type: none
o: bind
device: /home/${USER}/data/wordpress
secrets:
db_root_password:
file: ../secrets/db_root_password.txt
db_password:
file: ../secrets/db_password.txt
π§ͺ Analysis Questions:
- Why does
wordpress
depend onmariadb
? - What does
restart: always
do? - Why are volumes mapped to host directories?
# For testing we need to create data folders
mkdir -p /home/user/data/mariadb
mkdir -p /home/user/data/wordpress
Part 13: The Makefile - Automation
Why Use a Makefile?
A Makefile provides simple commands to manage your project:
# Variables
COMPOSE_FILE = srcs/docker-compose.yml
DATA_DIR = /home/$(USER)/data
.PHONY: all build up down clean fclean re
all: build up
# Create data directories
$(DATA_DIR)/mariadb:
mkdir -p $(DATA_DIR)/mariadb
$(DATA_DIR)/wordpress:
mkdir -p $(DATA_DIR)/wordpress
# Build images
build: $(DATA_DIR)/mariadb $(DATA_DIR)/wordpress
docker compose -f $(COMPOSE_FILE) build
# Start services
up:
docker compose -f $(COMPOSE_FILE) up -d
# Stop services
down:
docker compose -f $(COMPOSE_FILE) down
# Clean containers and images
clean:
docker compose -f $(COMPOSE_FILE) down
docker system prune -af
# Full clean including volumes
fclean: clean
docker volume prune -f
sudo rm -rf $(DATA_DIR)
# Rebuild everything
re: fclean all
π€ Understanding Make:
- What does
.PHONY
do? - Why do we create directories first?
Part 14: Testing and Debugging
Step-by-Step Testing
- Build and start services:
make all
- Check container status:
docker ps
π§ͺ Debugging Exercise: If a container is not running, how would you investigate?
# Check container logs
docker logs mariadb
docker logs wordpress
docker logs nginx
# Access container shell
docker exec -it mariadb bash
- Test database connection:
docker exec -it mariadb mysql -u root -p
- Test WordPress:
Visit https://login.42.fr
(or your domain)
π Troubleshooting Questions:
- What if you get "connection refused"?
- What if you see nginx but not WordPress?
- What if the database connection fails?
Common Issues and Solutions
Issue | Symptom | Solution |
---|---|---|
Port conflict | bind: address already in use |
Stop conflicting services |
Permission denied | mkdir: cannot create directory |
Check file permissions |
Network issues | could not connect to mariadb |
Verify network configuration |
SSL warnings | Browser security warning | Expected with self-signed certs |
Part 15: Advanced Concepts and Best Practices
Docker Security Best Practices
- Use non-root users:
RUN groupadd -r wordpress && useradd -r -g wordpress wordpress
USER wordpress
- Minimize image layers:
# Good
RUN apt-get update && apt-get install -y \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*
# Bad
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
- Use specific image tags:
# Good
FROM nginx:1.21-alpine
# Bad
FROM nginx:latest
Understanding PID 1
π€ Why is PID 1 special in containers?
In Unix systems, PID 1 is the init process that:
- Manages child processes
- Handles system signals
- Cleans up zombie processes
π Critical Thinking: What happens if your container process doesn't handle SIGTERM?
# Good - process runs as PID 1
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# Bad - shell runs as PID 1
ENTRYPOINT nginx -g "daemon off;"
Performance Optimization
- Use multi-stage builds:
# Build stage
FROM node:16 AS builder
COPY package*.json ./
RUN npm ci --only=production
# Runtime stage
FROM node:16-alpine
COPY --from=builder /app/node_modules ./node_modules
- Optimize layer caching:
# Copy dependencies first
COPY package*.json ./
RUN npm install
# Copy source code last
COPY . .
Part 16: Monitoring and Maintenance
Health Checks
Add health checks to your services:
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD mysqladmin ping -h localhost || exit 1
# In docker-compose.yml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/wp-admin/install.php"]
interval: 30s
timeout: 10s
retries: 3
Logging Strategy
services:
nginx:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Backup Strategy
# Backup database
docker exec mariadb mysqldump -u root -p${MYSQL_ROOT_PASSWORD} ${MYSQL_DATABASE} > backup.sql
# Backup volumes
tar -czf wordpress_backup.tar.gz /home/${USER}/data/wordpress
π§ͺ Practical Exercise: Write a script that creates automated backups every day at 2 AM.
Part 17: Scaling and Production Considerations
Horizontal Scaling
π€ Question: How would you run multiple WordPress containers?
wordpress:
scale: 3 # Run 3 WordPress containers
Load Balancing with NGINX
upstream wordpress_backend {
server wordpress1:9000;
server wordpress2:9000;
server wordpress3:9000;
}
location ~ \.php$ {
fastcgi_pass wordpress_backend;
}
Database Clustering
π Advanced Challenge: How would you set up MariaDB master-slave replication?
Secret Management at Scale
For production, consider:
- Docker Swarm secrets
- Kubernetes secrets
- External secret managers (HashiCorp Vault, AWS Secrets Manager)
Part 18: Final Project Assembly
Complete Directory Structure
inception/
βββ Makefile
βββ secrets/
β βββ db_password.txt
β βββ db_root_password.txt
β βββ credentials.txt
βββ srcs/
βββ docker-compose.yml
βββ .env
βββ requirements/
βββ mariadb/
β βββ Dockerfile
β βββ conf/
β β βββ 50-server.cnf
β βββ tools/
β βββ init_db.sh
βββ nginx/
β βββ Dockerfile
β βββ conf/
β β βββ nginx.conf
β βββ tools/
β βββ generate_ssl.sh
β βββ start_nginx.sh
βββ wordpress/
βββ Dockerfile
βββ conf/
β βββ www.conf
βββ tools/
βββ setup_wordpress.sh
Final Checklist
β Infrastructure Requirements:
- [ ] NGINX with TLSv1.2 or TLSv1.3 only
- [ ] WordPress with php-fpm (no nginx)
- [ ] MariaDB (no nginx)
- [ ] Two volumes: database and WordPress files
- [ ] Docker network connecting containers
- [ ] Containers restart on crash
β Security Requirements:
- [ ] No passwords in Dockerfiles
- [ ] Environment variables used
- [ ] Secrets properly managed
- [ ] No prohibited commands (tail -f, sleep infinity, etc.)
β Architecture Requirements:
- [ ] Each service in dedicated container
- [ ] Custom Dockerfiles (no pulling ready-made images)
- [ ] NGINX as sole entry point on port 443
- [ ] Proper domain name configuration
Launch Commands
# Build everything
make all
# Check status
docker ps
# View logs
docker logs nginx
docker logs wordpress
docker logs mariadb
# Test the website
curl -k https://login.42.fr
# Stop everything
make down
# Clean everything
make fclean
Part 19: Troubleshooting Guide
Common Scenarios
Scenario 1: Container won't start
# Check the logs
docker logs <container_name>
# Check the Dockerfile
# Common issues: wrong base image, missing packages, permission issues
π§ͺ Debug Challenge: If MariaDB container exits immediately, what are the first three things you'd check?
Scenario 2: Cannot connect to database
# Test network connectivity
docker exec wordpress ping mariadb
# Check database is listening
docker exec mariadb netstat -tuln | grep 3306
# Test database connection
docker exec mariadb mysql -u ${MYSQL_USER} -p${MYSQL_PASSWORD} -e "SHOW DATABASES;"
Scenario 3: NGINX returns 502 Bad Gateway
π Analysis: What does 502 mean, and what would you check?
# Check if WordPress container is running
docker ps | grep wordpress
# Check PHP-FPM is listening
docker exec wordpress netstat -tuln | grep 9000
# Check NGINX configuration
docker exec nginx nginx -t
Performance Issues
π€ Investigation Questions:
- Is the issue CPU, memory, or I/O related?
- Are there any bottlenecks in the database queries?
- Is the network connectivity optimal?
# Monitor resource usage
docker stats
# Check disk usage
df -h
du -sh /home/${USER}/data/*
# Monitor network
docker exec nginx ss -tuln
Part 20: Beyond the Basics - Advanced Topics
Container Orchestration
π Next Level: After mastering Docker Compose, consider:
- Kubernetes: For large-scale orchestration
- Docker Swarm: For simpler clustering
- Nomad: HashiCorp's orchestrator
CI/CD Integration
# Example GitHub Actions workflow
name: Deploy Inception
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build and deploy
run: |
make build
make up
Security Scanning
# Scan images for vulnerabilities
docker scan nginx:inception
docker scan wordpress:inception
docker scan mariadb:inception
Multi-architecture Builds
# Build for multiple architectures
FROM --platform=$BUILDPLATFORM nginx:alpine
Next Steps π
- Experiment with different base images: Try Alpine vs Ubuntu vs Debian
- Add monitoring: Alertmanager, Integrate Prometheus and Grafana
- Implement logging: Use ELK stack (Elasticsearch, Logstash, Kibana)
- Learn Kubernetes: The next level of container orchestration
- Explore cloud services: Deploy your containers to AWS, GCP, or Azure
Docker containers basic commands
# To build
docker build -t <container_image_name> .
# To run
docker run -d <container_image_name>
# To run with name
docker run -d --name my_app image
# To check all docker containers
docker ps -a
# To stop containers and remove images
docker rm -f $(docker ps -aq) && docker rmi -f $(docker images -aq)
# To run a compose file
docker compose up
# Force rebuild in background
docker compose up --build -d
# Down composer
docker compose down
# To delete content from data volumes
sudo rm -rf /home/user/data/wordpress/
sudo rm -rf /home/user/data/mariadb/
# To create again
mkdir -p /home/user/data/wordpress/
mkdir -p /home/user/data/mariadb/
# To check db connection manually (from wp contaier)
docker exec -it wordpress bash
apt-get update && apt-get install -y mariadb-client
mysql -h mariadb -u"$WORDPRESS_DB_USER" -p
# To acces mariadb user table from root (inside mariadb contaier)
docker exec -it mariadb mysql -u root -p
# And from wp_user (inside mariadb contaier)
docker exec -it mariadb mysql -u"$WORDPRESS_DB_USER" -p
# To check env values
docker exec -it mariadb env
docker exec -it wordpress env
Remember
Every expert was once a beginner. The questions you asked yourself throughout this tutorial - "Why does this work?", "What would happen if...?", "How can I improve this?" - these are the questions that make great engineers.
Keep questioning, keep experimenting, and keep building! π³
Resources for Continued Learning
- Docker Documentation: https://docs.docker.com/
- Docker Best Practices: https://docs.docker.com/develop/best-practices/
- Kubernetes Learning: https://kubernetes.io/docs/tutorials/
- Container Security: https://www.nist.gov/publications/application-container-security-guide
Happy containerizing! π
Top comments (0)