DEV Community

alejiri
alejiri

Posted on

Docker NGINX + WordPress + MariaDB Tutorial - Inception42

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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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
Enter fullscreen mode Exit fullscreen mode

πŸ€” 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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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
Enter fullscreen mode Exit fullscreen mode

πŸ’­ 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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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/
Enter fullscreen mode Exit fullscreen mode

πŸ€” 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
Enter fullscreen mode Exit fullscreen mode

πŸ’­ 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
Enter fullscreen mode Exit fullscreen mode
# 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>
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

πŸ€” 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
Enter fullscreen mode Exit fullscreen mode

πŸ’­ 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

Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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

Enter fullscreen mode Exit fullscreen mode

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"]

Enter fullscreen mode Exit fullscreen mode

πŸ’­ 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
Enter fullscreen mode Exit fullscreen mode

πŸ€” 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


Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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

Enter fullscreen mode Exit fullscreen mode

Part 9: NGINX with TLS - The Front Door

Understanding NGINX's Role

NGINX acts as a reverse proxy, meaning it:

  1. Receives requests from users
  2. Forwards them to PHP-FPM
  3. 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"]
Enter fullscreen mode Exit fullscreen mode

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;"
Enter fullscreen mode Exit fullscreen mode

πŸ’­ 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";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Critical Analysis:

  • Why does fastcgi_pass use wordpress:9000 instead of an IP address?
  • What does try_files do?
# Check certificate
docker exec -it docker_id_name ls /etc/nginx/ssl/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ’­ 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
Enter fullscreen mode Exit fullscreen mode

βœ… 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_
Enter fullscreen mode Exit fullscreen mode

πŸ”’ 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/*
Enter fullscreen mode Exit fullscreen mode

πŸ€” 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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Analysis Questions:

  • Why does wordpress depend on mariadb?
  • 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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ€” Understanding Make:

  • What does .PHONY do?
  • Why do we create directories first?

Part 14: Testing and Debugging

Step-by-Step Testing

  1. Build and start services:
make all
Enter fullscreen mode Exit fullscreen mode
  1. Check container status:
docker ps
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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
Enter fullscreen mode Exit fullscreen mode
  1. Test database connection:
docker exec -it mariadb mysql -u root -p
Enter fullscreen mode Exit fullscreen mode
  1. 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

  1. Use non-root users:
RUN groupadd -r wordpress && useradd -r -g wordpress wordpress
USER wordpress
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. Use specific image tags:
# Good
FROM nginx:1.21-alpine

# Bad
FROM nginx:latest
Enter fullscreen mode Exit fullscreen mode

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;"
Enter fullscreen mode Exit fullscreen mode

Performance Optimization

  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. Optimize layer caching:
# Copy dependencies first
COPY package*.json ./
RUN npm install

# Copy source code last
COPY . .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# In docker-compose.yml
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost/wp-admin/install.php"]
  interval: 30s
  timeout: 10s
  retries: 3
Enter fullscreen mode Exit fullscreen mode

Logging Strategy

services:
  nginx:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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
Enter fullscreen mode Exit fullscreen mode

Load Balancing with NGINX

upstream wordpress_backend {
    server wordpress1:9000;
    server wordpress2:9000;
    server wordpress3:9000;
}

location ~ \.php$ {
    fastcgi_pass wordpress_backend;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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;"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Security Scanning

# Scan images for vulnerabilities
docker scan nginx:inception
docker scan wordpress:inception
docker scan mariadb:inception
Enter fullscreen mode Exit fullscreen mode

Multi-architecture Builds

# Build for multiple architectures
FROM --platform=$BUILDPLATFORM nginx:alpine
Enter fullscreen mode Exit fullscreen mode

Next Steps πŸš€

  1. Experiment with different base images: Try Alpine vs Ubuntu vs Debian
  2. Add monitoring: Alertmanager, Integrate Prometheus and Grafana
  3. Implement logging: Use ELK stack (Elasticsearch, Logstash, Kibana)
  4. Learn Kubernetes: The next level of container orchestration
  5. 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

Enter fullscreen mode Exit fullscreen mode

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

Happy containerizing! πŸŽ‰

Top comments (0)