Table of Contents
ToggleIntroduction
Complete Guide: Deploying wordPress with Docker, Nginx & SSL. A professional, production-ready setup for hosting wordPress using containerization and reverse proxy
Highlight: This comprehensive guide walks you through setting up a production-ready wordPress site using Docker containers, Nginx as a reverse proxy, and Let us Encrypt SSL certificates. Perfect for developers looking to deploy scalable, secure wordPress installations.
Modern wordPress deployment has evolved significantly from traditional LAMP stack installations. This guide introduces a containerized approach that leverages Docker isolation capabilities, Nginx high-performance web serving, and automated SSL certificate management. By the end of this tutorial, you will have a fully functional wordPress site that can handle production traffic, scale horizontally, and maintain security best practices.
The architecture we will build separates concerns across multiple layers: a global Nginx instance handles all incoming traffic and SSL termination, Docker containers provide isolated environments for each service, and a private network enables secure communication between components. This separation allows you to run multiple wordPress sites on a single server, each completely isolated from the others, while sharing the same ports 80 and 443 on the host machine.
This approach offers several advantages over traditional installations: easier backup and recovery (entire sites can be exported as Docker images), simplified updates (update containers without touching the host system), better resource utilization (containers share the host kernel), and enhanced security (compromised containers do not affect the host or other containers). Additionally, the configuration-as-code approach using docker-compose.yml makes your entire setup reproducible and version-controllable.
Architecture Overview
This setup creates a robust, containerized wordPress environment with multiple layers of separation. The architecture follows a reverse proxy pattern where requests flow through the global Nginx server before reaching the containerized application.
Understanding the architecture is crucial before diving into implementation. Our design uses a layered approach where each component has a specific responsibility. The outermost layer, the global Nginx server, acts as a traffic director - it receives all incoming HTTP and HTTPS requests on standard ports 80 and 443. This Nginx instance does not serve any content itself; instead, it examines the requested domain name and forwards traffic to the appropriate backend service.
The middle layer consists of Docker containers orchestrated by Docker Compose. Each wordPress site runs in its own isolated container environment with three interconnected services: a MySQL database for data persistence, a wordPress application running PHP-FPM for dynamic content generation, and a containerized Nginx server for efficient static file serving. These containers communicate through a private Docker network that is invisible to the outside world.
The innermost layer is the data persistence layer, managed through Docker volumes. Unlike containers which are ephemeral and can be destroyed and recreated, volumes persist data across container lifecycles. This ensures your wordPress files and database remain safe even if containers are updated or rebuilt.
This architecture particularly powerful due to the port mapping strategy. NOw while the global Nginx occupies ports 80 and 443 on the host machine, each containerized wordPress installation binds to a unique high-numbered port (like 8090, 8091, 8092). The global Nginx then acts as a smart router, forwarding domain-based requests to the correct port. This allows unlimited wordPress sites on a single server, each completely isolated but all accessible through standard HTTP/HTTPS ports.
Request Flow Architecture
┌─────────────────────────────────────────────────────────────────┐
│ USER BROWSER │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ INTERNET (Port 80 HTTP / 443 HTTPS) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GLOBAL NGINX (Reverse Proxy) │
│ Forwards to: 127.0.0.1:8090 │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DOCKER CONTAINER NETWORK │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ CONTAINER NGINX (Port 8090→80) │ │
│ │ • Serves static files (CSS, JS, images) │ │
│ │ • Forwards PHP requests to WordPress │ │
│ └─────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ WORDPRESS PHP-FPM (Port 9000) │ │
│ │ • Processes PHP code │ │
│ │ • Generates dynamic content │ │
│ └─────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ MYSQL DATABASE (Port 3306) │ │
│ │ • Stores WordPress data │ │
│ │ • Posts, pages, users, settings │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
How it works:
- User Browser: Sends HTTP/HTTPS request to your domain. The browser performs a DNS lookup to resolve your domain name to your server IP address, then initiates a TCP connection.
- Internet (Port 80/443): Standard web ports receive the incoming traffic. Port 80 handles unencrypted HTTP traffic, while port 443 handles encrypted HTTPS traffic with TLS/SSL.
- Global Nginx: Acts as reverse proxy and examines the Host header to determine which backend service should handle the request. It then forwards requests to port 8090 on localhost, adding important headers that preserve client information.
- Container Nginx: Receives traffic on host port 8090 (mapped to container port 80). It serves static files directly from the shared wordPress volume for optimal performance, and forwards PHP requests via FastCGI protocol to the wordPress container on port 9000.
- PHP-FPM: The PHP FastCGI Process Manager executes wordPress PHP code, generates dynamic HTML content, processes form submissions, handles user authentication, and communicates with the database for data operations.
- MySQL Database: Stores all wordPress persistent data including posts, pages, comments, user accounts, site settings, plugin configurations, and theme options. All data is stored in a Docker volume for persistence across container restarts.
Step 1: System Preparation
Before installing any services or applications, it is critical to ensure your Ubuntu server is up-to-date and properly configured. System preparation involves updating existing packages to patch security vulnerabilities, upgrading the kernel and core utilities, and establishing a stable baseline for your production environment. This step, while often overlooked, can prevent compatibility issues and security exploits down the line.
Many production environments prefer to disable automatic updates to maintain predictable system states. Now, while automatic updates help keep systems secure, they can also introduce unexpected changes that break production applications. By disabling automatic updates and implementing a manual update schedule, you maintain full control over when changes occur, allowing you to test updates in a staging environment first.
Update and Upgrade the System
Start with a fresh system update to ensure all packages are current:
sudo apt update
sudo apt upgrade
About these commands:
sudo apt updateDownloads the latest package information from all configured repositories. This refreshes the local package index but does not install anything. Think of it as refreshing a catalog - you are getting the latest list of available software and their versions.sudo apt upgradeInstalls the newest versions of all packages currently installed on the system based on the updated package index. This applies security patches, bug fixes, and feature updates while respecting dependency relationships. The upgrade process is conservative and will not remove packages or install new ones unless explicitly required for dependencies.
During the upgrade process, you may be prompted about configuration file changes. If a package maintainer has updated a config file that you have also modified, apt will ask whether to keep your version, install the new version, or view the differences. For production systems, carefully review these prompts and consider the implications of each choice.
Disable Automatic Updates
For production stability, disable automatic updates to prevent unexpected changes:
sudo vi /etc/apt/apt.conf.d/20auto-upgrades
Set all values to 0:
APT::Periodic::Update-Package-Lists "0"
APT::Periodic::Download-Upgradeable-Packages "0"
APT::Periodic::AutocleanInterval "0"
APT::Periodic::Unattended-Upgrade "0"
Understading the settings:
APT::Periodic::Update-Package-Lists "0"Prevents automatic updating of the package list. Value 0 means never update automatically.APT::Periodic::Download-Upgradeable-Packages "0"Prevents automatic downloading of package upgrades. Stops the system from pre-downloading updates in the background.APT::Periodic::AutocleanInterval "0"Prevents automatic cleaning of the package cache. If enabled, this removes obsolete package files.APT::Periodic::Unattended-Upgrade "0"Disables automatic installation of updates. This is the main setting that prevents surprise system changes.
Important: As this prevents automatic updates, remember to manually update your system regularly for security patches. Plan a maintenance schedule for system updates.
Step 2: Install Core Components
Now, with a clean and updated system, we can now install the foundational components that power our wordPress deployment. This step installs two critical pieces of infrastructure: Nginx, which will handle all web traffic both at the global level and within containers, and Docker, which provides the containerization platform for isolating and managing our wordPress stack.
Nginx is chosen over alternatives like Apache for several reasons: it excels at handling concurrent connections with minimal memory overhead, serves static files extremely efficiently, and performs reverse proxy duties with excellent performance characteristics. Docker, on the other hand, revolutionizes application deployment by packaging applications with their dependencies into portable containers that run consistently across different environments.
The installation process for Docker is more involved than typical apt packages because Docker requires cryptographic verification of packages and must be installed from Docker official repositories rather than Ubuntu default repositories. This ensures you receive the latest stable version directly from Docker, with all the newest features and security patches.
Install Nginx
Nginx will serve as both the global reverse proxy and the container webserver:
sudo apt install nginx
nginx --v
About these commands:
sudo apt install nginxInstalls the Nginx web server package. Nginx is a high-performance HTTP server and reverse proxy. It will handle incoming web requests and forward them to appropriate services.nginx --vDisplays the installed Nginx version. This confirms the installation was successful.
So, why Nginx? Nginx excels at handling multiple concurrent connections with low memory footprint. It efficiently serves static content and acts as a reverse proxy to forward dynamic requests to backend services.
Install Docker & Docker Compose
Docker enables containerization, providing isolation and portability for your wordPress stack:
# Add Docker's official GPG key
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Security Setup - what these commands do:
sudo apt-get install ca-certificates curlInstalls CA certificates for secure HTTPS connections and curl for downloading files.sudo install -m 0755 -d /etc/apt/keyringsCreates a directory to store GPG keys with specific permissions (755 = owner can read/write/execute, others can read/execute).sudo curl -fsSL ... -o /etc/apt/keyrings/docker.ascDownloads Docker official GPG key. This key verifies that packages are genuinely from Docker. The flags: -f (fail silently), -s (silent mode), -S (show errors), -L (follow redirects).sudo chmod a+r /etc/apt/keyrings/docker.ascMakes the GPG key readable by all users (a+r = all users can read).
# Add repository to Apt sources
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
Repository Configuration:
$(dpkg --print-architecture)Detects your system architecture (amd64, arm64, etc.) to download the correct packages.signed-by=/etc/apt/keyrings/docker.ascTells apt to verify packages using Docker GPG key for security.$(. /etc/os-release && echo "$VERSION_CODENAME")Automatically detects your Ubuntu version name (e.g., jammy, focal) to use the correct repository.sudo tee /etc/apt/sources.list.d/docker.listwrites the repository configuration to apt sources list so it knows where to find Docker packages.sudo apt-get updateUpdates the package index with Docker repository information.
# Install Docker packages
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo apt install docker-compose
# Verify installation
docker --version
docker-compose --version
Package Installation:
docker-ceDocker Community Edition, the core Docker engine that runs containers.docker-ce-cliCommand-line interface tools for interacting with Docker (docker run, docker build, etc.).containerd.ioContainer runtime that manages the container lifecycle (start, stop, pause).docker-buildx-pluginExtended build capabilities with BuildKit, supports multi-platform builds.docker-compose-pluginPlugin version of Docker Compose for managing multi-container applications.docker-composeStandalone Docker Compose tool (installs both plugin and standalone versions for compatibility).docker --versionanddocker-compose --versionVerify successful installation by displaying version information.
Step 3: Configure Docker Compose
Docker Compose is a tool for defining and running multi-container Docker applications. Instead of manually running multiple docker run commands with various flags and configurations, Docker Compose allows you to declare your entire application stack in a single YAML file. This configuration-as-code approach makes your infrastructure reproducible, version-controllable, and easy to share with team members.
Our wordPress stack requires three interconnected services: a MySQL database, the wordPress application itself, and an Nginx web server. Each service runs in its own container, but they need to communicate with each other and share data. Docker Compose handles this complexity by creating networks for container communication and volumes for data persistence, all defined declaratively in the docker-compose.yml file.
The beauty of this approach is that you can destroy and recreate your entire wordPress installation with a single command, and all your data remains intact in Docker volumes. This makes backups, migrations, and disaster recovery significantly simpler than traditional installations where configuration files and data are scattered across the filesystem.
Create Project Directory Structure
Organization is key when managing multiple sites. And we create a dedicated directory for each wordPress installation, named after the domain for easy identification. This directory will contain all configuration files needed to define and run the containers.
mkdir service.analyticalman.com
cd service.analyticalman.com
Check what these commands do:
mkdir service.analyticalman.comCreates a new directory with your domain name. This directory will contain all configuration files for this wordPress installation. Using the domain name helps organize multiple sites.cd service.analyticalman.comChanges the current directory to the newly created folder where we will place all Docker configuration files.
Create docker-compose.yml
This file orchestrates three services: MySQL database, wordPress application, and Nginx webserver:
version: '3'
services:
db:
image: mysql:8.3
container_name: sam_db
restart: unless-stopped
env_file: .env
environment:
- MYSQL_DATABASE=wordpress
volumes:
- dbdata:/var/lib/mysql
command: '--default-authentication-plugin=mysql_native_password'
networks:
- app-network
wordpress:
depends_on:
- db
image: wordpress:php8.3-fpm-alpine
container_name: sam_wordpress
restart: unless-stopped
env_file: .env
environment:
- WORDPRESS_DB_HOST=db:3306
- WORDPRESS_DB_USER=$MYSQL_USER
- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD
- WORDPRESS_DB_NAME=wordpress
volumes:
- wordpress:/var/www/html
networks:
- app-network
webserver:
depends_on:
- wordpress
image: nginx:1.25.4-alpine
container_name: service.analyticalman.com
restart: unless-stopped
ports:
- "8090:80"
volumes:
- wordpress:/var/www/html
- ./nginx-conf:/etc/nginx/conf.d
networks:
- app-network
volumes:
wordpress:
dbdata:
networks:
app-network:
driver: bridge
Database Service (db) Configuration:
image: mysql:8.3Uses MySQL version 8.3 official image from Docker Hub.container_name: sam_dbNames the container sam_db for easy identification.restart: unless-stoppedAutomatically restarts the container if it crashes, unless you manually stop it.env_file: .envLoads environment variables (passwords, usernames) from the .env file for security.environment: MYSQL_DATABASE=wordpressCreates a database named wordpress on first run.volumes: dbdata:/var/lib/mysqlPersists database data in a Docker volume named dbdata. without this, all data would be lost when container restarts.command: '--default-authentication-plugin=mysql_native_password'Uses native password authentication for compatibility with wordPress.networks: app-networkConnects to the app-network so it can communicate with other containers.
Service Configuration of wordPress:
depends_on: dbEnsures database container starts before wordPress container.image: wordpress:php8.3-fpm-alpineUses wordPress with PHP 8.3 FPM (FastCGI Process Manager) on Alpine Linux for small image size. FPM handles PHP processing efficiently.container_name: sam_wordpressNames the container sam_wordpress.WORDPRESS_DB_HOST=db:3306Points wordPress to the database service. db is the service name (Docker internal DNS), port 3306 is MySQL default port.WORDPRESS_DB_USER=$MYSQL_USERReads database username from .env file variables.WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORDReads database password from .env file.WORDPRESS_DB_NAME=wordpressConnects to the wordpress database we created in the db service.volumes: wordpress:/var/www/htmlStores wordPress files in a shared volume. This volume is also mounted in the webserver container.
Service Configuration of webserver (Nginx):
depends_on: wordpressEnsures wordPress starts before the webserver.image: nginx:1.25.4-alpineUses Nginx version 1.25.4 on Alpine Linux for lightweight deployment.container_name: service.analyticalman.comNames container with your domain name.ports: "8090:80"Critical mapping: Maps container internal port 80 to host machine port 8090. The global Nginx will forward requests to this port. Format is HOST_PORT:CONTAINER_PORT.volumes: wordpress:/var/www/htmlMounts the same wordPress volume to serve files. This shared volume allows Nginx to access wordPress files.volumes: ./nginx-conf:/etc/nginx/conf.dMounts local nginx-conf directory into container config directory. This loads our custom Nginx configuration.
Volumes Section:
wordpress:Declares a named volume for wordPress files. Persists across container restarts.dbdata:Declares a named volume for MySQL database files. Ensures data is not lost.
Networks Section:
app-network:Creates an isolated network for these containers.driver: bridgeUses bridge networking, allowing containers to communicate using service names (db, wordpress, webserver) as hostnames.
Key Architecture Points: The wordPress and Nginx containers share the wordpress volume, allowing Nginx to serve static files directly while forwarding PHP requests to wordPress via FastCGI on port 9000. All three containers communicate through the app-network using their service names as DNS hostnames.
Create Environment Variables File
Store sensitive database credentials in a separate .env file:
nano .env
MYSQL_ROOT_PASSWORD=your_secure_root_password
MYSQL_USER=your_wordpress_user
MYSQL_PASSWORD=your_secure_password
Understanding each variable:
MYSQL_ROOT_PASSWORDSets the password for MySQL root user (database administrator). Root has full control over all databases. Choose a very strong password.MYSQL_USERCreates a MySQL user specifically for wordPress. This user has limited permissions (only access to the wordpress database), following security best practices of least privilege.MYSQL_PASSWORDSets the password for the wordPress database user. wordPress will use this to connect to the database.
How it works: Docker Compose reads this .env file and substitutes these variables into the docker-compose.yml configuration. The MySQL container uses them to initialize users and passwords, while the wordPress container uses them to connect to the database.
Security warning: Replace these placeholder values with strong, unique passwords (minimum 16 characters, mix of letters, numbers, symbols). Never commit the .env file to version control systems like Git. Add .env to your .gitignore file.
Configure Container Nginx
Create the Nginx configuration for the wordPress container:
mkdir nginx-conf
cd nginx-conf
nano nginx.conf
Setup commands:
mkdir nginx-confCreates directory for Nginx configuration files.cd nginx-confEnters the nginx-conf directory.nano nginx.confOpens nano text editor to create the configuration file.
server {
listen 80;
listen [::]:80;
server_name service.analyticalman.com;
index index.php index.html index.htm;
root /var/www/html;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
log_not_found off;
access_log off;
allow all;
}
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
expires max;
log_not_found off;
}
}
Configuration Breakdown:
Server Block Basics:
listen 80;Listens on IPv4 port 80 (HTTP) inside the container.listen [::]:80;Listens on IPv6 port 80 for broader compatibility.server_name service.analyticalman.com;Defines the domain name this server block responds to. Replace with your actual domain.index index.php index.html index.htm;Order of files to serve when directory is requested. Checks for index.php first (wordPress uses this).root /var/www/html;Document root where wordPress files are located (the shared volume).
SSL Certificate Challenge Location:
location ~ /.well-known/acme-challengeSpecial path for Let's Encrypt SSL certificate verification.allow all;Permits public access to this path so Let's Encrypt can verify domain ownership.- This enables automated SSL certificate renewal without manual intervention.
Main Location Block (/):
try_files $uri $uri/ /index.php$is_args$args;wordPress permalink handler.- First tries to serve the file as-is ($uri).
- If not found, tries as directory ($uri/).
- If still not found, passes to index.php with original query string ($is_args$args).
- This enables clean URLs like yoursite.com/about instead of yoursite.com/index.php?page=about.
PHP Processing Block:
location ~ \.php$Matches all files ending in .php using regex (~).try_files $uri =404;Returns 404 if the PHP file does not exist, prevents security issues.fastcgi_split_path_info ^(.+\.php)(/.+)$;Splits the URL into script name and path info for proper PHP routing.fastcgi_pass wordpress:9000;Critical: Forwards PHP requests to the wordPress container on port 9000. wordpress is the service name from docker-compose.yml, Docker DNS resolves it to the container IP.fastcgi_index index.php;Default file when directory with PHP is requested.include fastcgi_params;Includes standard FastCGI parameters.fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;Tells PHP-FPM the full path to the script to execute.fastcgi_param PATH_INFO $fastcgi_path_info;Passes URL path information to PHP.
Security Block:
location ~ /\.htMatches files starting with .ht (like .htaccess, .htpasswd).deny all;Blocks access to these sensitive configuration files for security.
Performance Optimization Blocks:
location = /favicon.icoandlocation = /robots.txtExact match for these common files.log_not_found off;Does not log 404 errors for these files (reduces log noise).access_log off;Does not log access to these files (improves performance).location ~* \.(css|gif|ico|jpeg|jpg|js|png)$Matches static files (case-insensitive with ~*).expires max;Sets far-future expiration headers (browsers cache these files for maximum time).- This significantly improves page load times for returning visitors.
How it works together: The time when a request comes in, Nginx first checks if it is a static file (images, CSS, JS) and serves it directly. For PHP files, it forwards the request to the wordPress container PHP-FPM process on port 9000. PHP-FPM processes the PHP code, queries the database if needed, and returns the generated HTML to Nginx, which sends it to the user.
Launch Docker Containers
sudo docker compose up -d
sudo docker ps
Understanding the commands:
sudo docker compose up -dStarts all services defined in docker-compose.yml.upCreates and starts containers.-dRuns in detached mode (background), freeing up your terminal.- Docker Compose will: pull images if not present, create networks, create volumes, start containers in dependency order (db → wordpress → webserver).
sudo docker psLists all running containers with their status, ports, and names.- You should see three containers: sam_db, sam_wordpress, and service.analyticalman.com all with Up status.
Success! Your wordPress containers are now running. Verify all three containers (db, wordpress, webserver) show Up status in the output. The webserver should show port mapping 0.0.0.0:8090->80/tcp.
Step 4: Configure Global Nginx Reverse Proxy
Now that our containerized wordPress stack is running and listening on port 8090, we need to make it accessible from the internet. This is where the global Nginx configuration comes into play. The global Nginx server acts as the entry point for all web traffic, listening on the standard HTTP port 80 (and later HTTPS port 443 after SSL installation).
The reverse proxy pattern is essential for modern web infrastructure. Rather than exposing containers directly to the internet, we place a powerful, security-hardened web server between the internet and our applications. This proxy examines incoming requests, determines which backend service should handle them based on the domain name, and forwards the requests accordingly while preserving important information about the original client.
This architecture provides several benefits: centralized SSL/TLS termination (one place to manage certificates for all sites), load balancing capabilities (distribute traffic across multiple backend containers), additional security layers (the proxy can filter malicious requests), and simplified firewall rules (only ports 80 and 443 need to be open to the internet).
Nginx configuration follows a hierarchical structure with server blocks (similar to Apache virtual hosts) that define how to handle requests for specific domains. Now, within each server block, location directives specify how to process different URL patterns. For a reverse proxy, the key directive is proxy_pass, which forwards requests to the backend service.
Create Site Configuration
Configure the global Nginx to forward requests from port 80 to your container port 8090. This configuration file tells Nginx: Now, when someone requests service.analyticalman.com, forward that request to the Docker container listening on port 8090.
cd /etc/nginx/sites-available
sudo nano service.analyticalman.com
Directory explanation:
/etc/nginx/sites-availableDirectory containing all available Nginx site configurations (enabled or not).- Then we create a file named after our domain for easy identification.
server {
server_name service.analyticalman.com;
listen [::]:80;
listen 80;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:8090$request_uri;
}
}
Reverse Proxy Configuration Breakdown:
Server Block:
server_name service.analyticalman.com;Domain name this configuration responds to. Replace with your domain.listen [::]:80;Listens on IPv6 port 80 (standard HTTP port).listen 80;Listens on IPv4 port 80 (standard HTTP port).- Port 80 is where all HTTP traffic from the internet arrives.
Location Block - Proxy Headers:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;Appends the client real IP address to the X-Forwarded-For header. And without this, wordPress would only see requests coming from localhost (127.0.0.1).proxy_set_header Host $host;Passes the original Host header (your domain name) to the backend. The wordPress needs this to generate correct URLs.proxy_set_header X-Real-IP $remote_addr;Sends the client IP address. Important for wordPress security plugins, analytics, and logging.proxy_set_header X-Forwarded-Proto $scheme;Tells wordPress whether the original request was HTTP or HTTPS. Critical for SSL/HTTPS detection after Certbot installs certificates.proxy_http_version 1.1;Uses HTTP/1.1 protocol for proxy connections (required for webSockets and modern features).proxy_set_header Upgrade $http_upgrade;Passes webSocket upgrade requests (needed for real-time features).proxy_set_header Connection "upgrade";Maintains connection for webSocket upgrades.
Proxy Pass:
proxy_pass http://127.0.0.1:8090$request_uri;The heart of reverse proxy:http://127.0.0.1:8090Forwards requests to localhost port 8090 (where our Docker container Nginx listens).$request_uriAppends the full original request URI (path + query string), preserving the complete URL.- This creates the bridge: Internet (port 80) → Global Nginx → Container Nginx (port 8090).
The reason of this Architecture: The reverse proxy pattern allows you to run multiple websites on the same server. Each site gets its own Docker containers on unique ports (8090, 8091, 8092, etc.), while all share ports 80/443 on the host. The global Nginx routes traffic based on domain names.
Enable the Site
Create a symbolic link to enable the site configuration:
cd /etc/nginx/sites-enabled
sudo ln -s /etc/nginx/sites-available/service.analyticalman.com .
Understanding the commands:
cd /etc/nginx/sites-enabledChanges to the sites-enabled directory. Nginx only loads configurations from this directory.sudo ln -s /etc/nginx/sites-available/service.analyticalman.com .Creates a symbolic link (symlink).ln -sCreates a symbolic link (like a shortcut)./etc/nginx/sites-available/service.analyticalman.comSource file (the configuration we created)..Destination (current directory: sites-enabled).
Need of symbolic links This Debian/Ubuntu convention separates available configurations from enabled ones. You can quickly enable/disable sites by creating or removing symlinks without deleting the actual configuration files. To disable a site later, just delete the symlink in sites-enabled.
Test and Restart Nginx
sudo nginx -t
sudo systemctl restart nginx
Understanding the commands:
sudo nginx -tTests the Nginx configuration for syntax errors.- The
-tflag runs a configuration test without actually restarting the service. - If successful, outputs: syntax is ok and test is successful.
- If errors exist, shows the specific file and line number with the problem.
- Always run this before restarting to prevent breaking a working Nginx installation.
sudo systemctl restart nginxRestarts the Nginx service to load the new configuration.systemctlis the system service manager on Ubuntu.restartstops and starts the service, applying all configuration changes.
Pro Tip: Always test your Nginx configuration before restarting. The nginx -t command checks for syntax errors and prevents service failures. If the test fails, fix the errors shown before attempting to restart. You can also use sudo systemctl reload nginx for a graceful reload that does not drop existing connections.
Step 5: Secure with SSL/TLS
In today web landscape, HTTPS is no longer optional - it is an absolute requirement. Modern browsers mark HTTP sites as Not Secure, search engines penalize sites without HTTPS, and users have come to expect the green padlock symbol that indicates their connection is encrypted. Beyond these practical considerations, HTTPS protects your users data from eavesdropping, man-in-the-middle attacks, and tampering.
SSL/TLS (Secure Sockets Layer / Transport Layer Security) certificates enable HTTPS by providing two critical functions: encryption of data in transit between the browser and server, and authentication that proves your server is actually who it claims to be. Traditionally, obtaining SSL certificates was expensive and complicated, requiring annual fees and manual renewal processes. Let us Encrypt revolutionized this by providing free, automated certificates that are trusted by all major browsers.
Certbot is the official client software for Let's Encrypt, developed by the Electronic Frontier Foundation (EFF). It automates the entire certificate lifecycle: obtaining certificates by proving domain ownership, installing them in your web server configuration, and setting up automatic renewal before certificates expire. Let's Encrypt certificates are valid for 90 days, and Certbot automatically renews them when they have 30 days or less remaining.
The certificate issuance process uses the ACME (Automatic Certificate Management Environment) protocol. Certbot proves you control the domain by placing a special file at a specific URL path on your server (/.well-known/acme-challenge/). Let's Encrypt validation servers attempt to retrieve this file - if successful, they issue a certificate. This is why we configured that specific location block in the container Nginx configuration earlier.
Install Certbot
Certbot automates the process of obtaining and renewing Let's Encrypt SSL certificates. Installing it via Snap ensures you always have the latest version with all security updates.
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Understanding the commands:
sudo snap install --classic certbotInstalls Certbot using Snap package manager.snapis Ubuntu modern package manager that provides isolated, always-updated applications.--classicflag gives Certbot necessary system access to modify Nginx configurations and manage certificates.- Certbot is the official Let's Encrypt client recommended by the Electronic Frontier Foundation (EFF).
sudo ln -s /snap/bin/certbot /usr/bin/certbotCreates a symbolic link to make Certbot accessible from anywhere.- This allows you to run
certbotcommand without specifying the full path.
About Certbot Let's Encrypt is a free, automated certificate authority that provides SSL/TLS certificates. These certificates encrypt data between your users and your server, enable HTTPS, and are trusted by all major browsers.
Obtain SSL Certificate
sudo certbot --nginx
Understanding the commands:
sudo certbot --nginxRuns Certbot with Nginx plugin for automatic configuration.--nginxflag tells Certbot to automatically detect Nginx configurations and modify them for HTTPS.
Certbot Interactive Process:
If you run the above command, Certbot will guide you through several steps:
- Email Address: Prompts for your email (used for urgent renewal and security notices).
- Terms of Service: Asks you to agree to Let's Encrypt Terms of Service.
- Domain Selection: Automatically detects domains from your Nginx configurations and presents a list.
- Select your domain (service.analyticalman.com) from the list.
- HTTP to HTTPS Redirect: Asks if you want to redirect all HTTP traffic to HTTPS (recommended - choose yes).
Automation with Certbot:
- Domain Verification: Certbot uses ACME protocol to verify you control the domain by placing a temporary file at /.well-known/acme-challenge/ (this is why we configured that location in Nginx).
- Certificate Generation: Requests and downloads SSL certificates from Let's Encrypt.
- Certificate Installation: Installs certificates in /etc/letsencrypt/live/your-domain/.
- Nginx Configuration: Automatically modifies your Nginx configuration file to:
- Add HTTPS listener on port 443
- Configure SSL certificate paths
- Add SSL security settings (protocols, ciphers)
- Set up HTTP to HTTPS redirect (if selected)
- Auto-Renewal Setup: Creates a systemd timer that automatically renews certificates every 60 days (certificates expire after 90 days).
After Certificate Installation:
Your Nginx configuration will be updated to include:
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/service.analyticalman.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/service.analyticalman.com/privkey.pem;
# ... SSL settings and your existing configuration ...
}
Certificate Files:
fullchain.pemYour certificate plus intermediate certificates (what Nginx presents to browsers).privkey.pemYour private key (keep this secure, never share it).cert.pemYour certificate only.chain.pemIntermediate certificates.
Congratulations! Your wordPress site is now live with HTTPS encryption. The green padlock will appear in browsers. Visit https://your-domain.com to complete the wordPress installation. Certbot will automatically renew certificates before they expire. Therefore, no manual intervention is needed.
Key Benefits of This Setup
The architecture we have built offers numerous advantages over traditional wordPress installations. These benefits extend beyond simple technical improvements - they fundamentally change how you manage, scale, and secure your wordPress sites. Let us explore each advantage in detail to understand why this containerized approach represents best practices for modern wordPress deployment.
🔒 Enhanced Security
Security is built into every layer of this architecture. Containerized services provide strong isolation - if a wordPress plugin vulnerability is exploited, the attacker is trapped inside the wordPress container and cannot access the host system or other containers. The principle of least privilege is enforced through Docker user namespacing and Linux capabilities restrictions.
SSL/TLS encryption protects all data in transit between users and your server, preventing eavesdropping and man-in-the-middle attacks. Environment variables stored in .env files keep sensitive credentials out of configuration files and version control systems. The reverse proxy adds an additional security layer, hiding your backend infrastructure and providing a single point for implementing security policies like rate limiting and IP blocking.
Each container runs with minimal privileges, and the MySQL database is completely inaccessible from the internet - it only accepts connections from the wordPress container through the private Docker network. This defense-in-depth approach ensures that even if one security layer fails, others remain to protect your system.
📈 Scalability
The modular architecture enables both vertical and horizontal scaling. Need more resources? Simply adjust Docker container resource limits. Need to handle more traffic? Deploy multiple wordPress containers behind a load balancer. The reverse proxy pattern makes this trivial - add more backend services, update the proxy configuration, and traffic is automatically distributed.
Running multiple wordPress sites on the same server becomes straightforward. Each site gets its own isolated container environment with dedicated resources, yet they all share the efficient host kernel. Add a new site by copying the configuration directory, changing the domain name and port number, and running docker-compose up. The global Nginx handles routing to the correct site based on the domain name.
Database scaling is simplified through Docker volumes and standard MySQL replication techniques. Need to migrate a site to a more powerful server? Export the Docker images and volumes, transfer them to the new server, and restart. The entire site moves as a unit, with all configurations intact.
🔧 Easy Maintenance
Docker containers revolutionize wordPress maintenance. Updates become simple and safe - pull the new wordPress image, stop the old container, start a new one. If something breaks, roll back to the previous image in seconds. The entire update process can be tested in a development environment using identical containers before applying to production.
Backups are comprehensive and straightforward. Docker volumes can be backed up using simple file system snapshots, or you can export entire container images including all configurations. The entire wordPress stack - application, configuration, and data - can be captured in a single backup operation. Restoration is equally simple: import the volumes and images, run docker-compose up, and the site is live again.
Configuration management benefits from the infrastructure-as-code approach. The docker-compose.yml file and Nginx configurations can be version controlled with Git, providing full history of all changes, the ability to roll back to previous configurations, and easy collaboration with team members. Documentation is built-in - the configuration files serve as executable documentation of exactly how the system is set up.
Troubleshooting is enhanced through Docker logging capabilities. Each container writes logs to stdout/stderr, which Docker captures and makes available through docker logs commands. You can inspect container states, view real-time logs, execute commands inside running containers for debugging, and analyze resource usage - all through standardized Docker tooling.
⚡ Performance
Performance is optimized at every layer. Nginx serves as an efficient reverse proxy with minimal overhead, using an event-driven architecture that handles thousands of concurrent connections with low memory usage. Static assets (images, CSS, JavaScript) are served directly from disk by the container Nginx, bypassing PHP entirely for maximum speed.
PHP-FPM (FastCGI Process Manager) handles PHP processing efficiently through a pool of persistent PHP processes. Unlike traditional CGI where each request spawns a new PHP process, FPM maintains ready processes that can immediately handle requests. This eliminates process startup overhead and enables efficient resource utilization through process pooling and connection reuse.
The container architecture enables optimal resource allocation. Docker cgroup (control group) integration allows fine-grained control over CPU, memory, and I/O resources for each container. You can ensure wordPress has sufficient resources during traffic spikes while preventing it from consuming excessive resources that impact other services.
Caching strategies integrate seamlessly with this architecture. Add Redis or Memcached containers to the Docker Compose stack, configure wordPress to use them, and benefit from object caching without modifying the host system. The shared network allows these services to communicate at near-native speeds through the Docker bridge network.
The architecture also supports CDN integration naturally. Configure your CDN to pull static assets from your origin server, and the container Nginx efficiently serves these files with proper cache headers. For dynamic content, the reverse proxy can implement intelligent caching rules, reducing load on the wordPress application.
Troubleshooting Tips
Even with careful configuration, issues can arise during deployment or operation. Understanding how to diagnose and resolve common problems is essential for maintaining a healthy wordPress installation. This section covers the most frequent issues and provides systematic approaches to troubleshooting.
Container Debugging
Container Issues: Use sudo docker logs [container_name] to view container logs and diagnose problems. For real-time log monitoring, add the -f flag: sudo docker logs -f sam_wordpress.
Now, when containers fail to start or behave unexpectedly, logs are your first diagnostic tool. Each container writes application output to stdout and stderr, which Docker captures. Common container issues include:
- Container exits immediately: Check logs for PHP errors, database connection failures, or missing environment variables. The wordPress container requires valid database credentials to start.
- Database connection errors: Verify the MySQL container is running (
sudo docker ps), check .env file variables match docker-compose.yml expectations, and ensure the containers are on the same network. - Permission issues: wordPress needs write access to certain directories. Check volume permissions with
sudo docker exec sam_wordpress ls -la /var/www/html. - Container not visible in docker ps: Use
sudo docker ps -ato see stopped containers, then check logs to determine why the container exited.
For deeper investigation, you can execute commands inside running containers: sudo docker exec -it sam_wordpress /bin/sh opens a shell session inside the wordPress container, allowing you to inspect files, test network connectivity, and manually run commands.
Nginx Configuration Issues
Nginx Errors: Check /var/log/nginx/error.log for detailed error messages. For real-time monitoring: sudo tail -f /var/log/nginx/error.log. The access log at /var/log/nginx/access.log shows all incoming requests.
Nginx problems typically manifest as 502 Bad Gateway errors, 404 errors, or connection refused messages. Common Nginx issues include:
- 502 Bad Gateway: The proxy cannot reach the backend. Verify the backend container is running and listening on the expected port. Test with
curl http://localhost:8090from the host. Check proxy_pass configuration points to the correct port. - Configuration syntax errors: Always test configuration before restarting:
sudo nginx -t. This catches typos, missing semicolons, and invalid directives before they break your running server. - Permission denied errors: Nginx must read configuration files and access log directories. Check file permissions and SELinux settings if applicable.
- Site not loading: Verify DNS is correctly pointing to your server. Check firewall rules allow traffic on ports 80 and 443. Ensure the correct server_name is configured in your site configuration.
Port Conflicts and Network Issues
Port Conflicts: Ensure port 8090 (or your chosen port) is not already in use with sudo netstat -tulpn | grep 8090. If the port is occupied, either stop the conflicting service or choose a different port in your docker-compose.yml.
Port conflicts prevent containers from binding to expected ports, causing startup failures. Additional network troubleshooting commands:
sudo ss -tlnpShows all listening TCP ports and the processes using them, helpful for identifying conflicts.sudo docker network lsLists all Docker networks. Verify your app-network exists.sudo docker network inspect app-networkShows which containers are connected to the network and their IP addresses.sudo iptables -L -nDisplays firewall rules that might be blocking traffic.
Issues regaridng wordPress
Problems of wordPress often relate to permissions, database connectivity, or plugin conflicts:
- white screen of death: Enable wordPress debugging by adding
define('WP_DEBUG', true);to wp-config.php. View errors in debug.log. - Database connection errors: Verify environment variables in .env match wordPress configuration. Test database connectivity from within the wordPress container:
sudo docker exec sam_wordpress mysql -h db -u $MYSQL_USER -p$MYSQL_PASSWORD wordpress -e "SELECT 1;" - Upload issues: wordPress needs write permissions to wp-content/uploads/. Check permissions:
sudo docker exec sam_wordpress ls -la /var/www/html/wp-content/ - Permalink issues: After enabling SSL, you may need to update the wordPress site URL. Use wordPress CLI within the container:
sudo docker exec sam_wordpress wp option update home 'https://your-domain.com' --allow-root
SSL/Certificate Problems
Certificate issues typically prevent HTTPS from working correctly:
- Certificate validation errors: Ensure your domain DNS is correctly pointing to your server. Let's Encrypt cannot issue certificates for domains it cannot reach.
- Renewal failures: Check certbot renewal logs at
/var/log/letsencrypt/. Verify the /.well-known/acme-challenge/ location is accessible. - Mixed content warnings: After enabling HTTPS, wordPress must be configured to use HTTPS URLs. Install the Really Simple SSL plugin or manually update URLs in the database.
- Testing renewal: Simulate renewal without actually renewing:
sudo certbot renew --dry-run. This verifies the renewal process works without affecting your actual certificates.
Next Steps
Now, with your wordPress site deployed, secured, and operational, you have established a solid foundation. However, a production wordPress site requires ongoing maintenance, optimization, and enhancement. This section outlines critical next steps to harden security, improve performance, ensure business continuity, and optimize the user experience.
Implement Comprehensive Backup Strategy
Data loss can be catastrophic for any business. Implement a robust backup strategy that covers all critical components:
- Automated Docker Volume Backups: Create scripts that export wordPress and database volumes regularly. Use
docker run --rm --volumes-from sam_wordpress -v $(pwd):/backup ubuntu tar czf /backup/wordpress-$(date +%Y%m%d).tar.gz /var/www/htmlto create timestamped backups. - Database Dumps: Export MySQL databases separately for faster restoration:
docker exec sam_db mysqldump -u root -p$MYSQL_ROOT_PASSWORD wordpress > backup-$(date +%Y%m%d).sql - Off-site Storage: Store backups in multiple locations - cloud storage (Aws S3, Google Cloud Storage), another server, or offline media. Never rely solely on local backups.
- Backup Testing: Regularly test restoration procedures. A backup you cannot restore is worthless. Schedule quarterly restoration drills to verify backup integrity.
- Backup Automation: Use cron jobs to automate backup creation, rotation (delete old backups), and verification. Consider backup management tools like Restic or Duplicity for encrypted, deduplicated backups.
- Plugins of wordPress: Install backup plugins like UpdraftPlus or BackwPup for application-level backups that can be managed from the wordPress admin interface.
Deploy Monitoring and Alerting
Proactive monitoring catches problems before they impact users:
- Uptime Monitoring: Use services like UptimeRobot, Pingdom, or StatusCake to monitor your site availability from multiple locations worldwide. Configure alerts for downtime or slow response times.
- Server Monitoring: Deploy monitoring solutions like Prometheus with Grafana, Netdata, or Datadog to track server resources (CPU, memory, disk, network), container health, and application metrics.
- Log Aggregation: Centralize logs from all containers using ELK Stack (Elasticsearch, Logstash, Kibana) or Loki. This enables searching across all logs, identifying patterns, and troubleshooting issues quickly.
- Performance Monitoring: Install wordPress performance plugins like Query Monitor to identify slow database queries, memory usage issues, and performance bottlenecks.
- Security Monitoring: Implement intrusion detection with tools like Fail2Ban to automatically block suspicious IP addresses. Monitor wordPress login attempts and file changes.
- SSL Certificate Expiry Monitoring: Now, while Certbot auto-renews, monitor certificate expiry dates to catch renewal failures before they cause outages.
Optimize Performance with CDN
Content Delivery Networks dramatically improve global performance:
- CDN Selection: Choose a CDN provider like Cloudflare (free tier available), Amazon CloudFront, or Fastly. Cloudflare offers additional DDoS protection and free SSL.
- CDN Configuration: Configure your CDN to cache static assets (images, CSS, JavaScript) at edge locations worldwide. This reduces latency for global visitors and offloads traffic from your server.
- Cache Rules: Set appropriate cache TTLs (Time To Live) for different content types. Static assets can cache for days or weeks, while dynamic content needs shorter TTLs.
- Image Optimization: Use CDN features or wordPress plugins to automatically optimize images (compression, format conversion to webP, lazy loading).
- DNS Management: Many CDNs offer DNS management with features like load balancing, geographic routing, and automatic failover.
Harden wordPress Security
Additional security measures protect against evolving threats:
- Security Plugins: Install wordfence, Sucuri, or iThemes Security for features like firewall protection, malware scanning, two-factor authentication, and security hardening.
- Fail2Ban Integration: Configure Fail2Ban on the host to automatically ban IPs with repeated failed login attempts. This prevents brute force attacks.
- Disable XML-RPC: If not needed, disable XML-RPC to prevent a common attack vector. Add this to .htaccess or use a plugin.
- Limit Login Attempts: Implement login throttling to prevent brute force attacks. Use plugins like Login LockDown or limit-login-attempts-reloaded.
- Regular Security Scans: Schedule regular malware scans, check for vulnerable plugins, and review user permissions. Keep wordPress core, themes, and plugins updated.
- web Application Firewall: Implement a firewall either through your CDN provider or using ModSecurity on the Nginx layer to filter malicious requests.
- Database Security: Use strong passwords, limit MySQL network access (already handled by Docker networking), and regularly audit database users.
Performance Optimization
Fine-tune your wordPress installation for maximum speed:
- Caching Plugins: Install wpRocket, w3TotalCache, or wp-SuperCache to implement page caching, browser caching, and database query caching.
- Object Caching: Add Redis or Memcached containers to your Docker Compose stack. Install corresponding wordPress plugins (Redis Object Cache or Memcached Object Cache) for persistent object caching.
- Database Optimization: Regularly optimize database tables, remove post revisions, clean spam comments, and optimize database queries. Use plugins like wp-Optimize.
- PHP OpCache: Enable PHP OpCache in your wordPress container to cache compiled PHP code, reducing CPU usage and improving response times.
- HTTP/2 Configuration: Enable HTTP/2 in your Nginx configuration for improved page load times through multiplexing and server push.
- Lazy Loading: Implement lazy loading for images and videos to defer loading off-screen content until needed, improving initial page load times.
- Minification: Minify CSS, JavaScript, and HTML using plugins or build tools to reduce file sizes and bandwidth usage.
Establish Maintenance Schedule
Regular maintenance prevents problems and ensures optimal operation:
- Tasks per week: Review error logs, check backup success, monitor disk space usage, review security scan results.
- Monthly Tasks: Update wordPress core and plugins, review user accounts and permissions, test backup restoration, analyze performance metrics.
- Quarterly Tasks: Security audit, performance optimization review, disaster recovery drill, SSL certificate check (even with auto-renewal).
- Annual Tasks: Architecture review, capacity planning, major version upgrades, comprehensive security assessment.
You now have a professional, production-ready wordPress installation that is secure, scalable, and maintainable. By implementing these next steps, you will ensure your site remains performant, secure, and reliable for years to come. The containerized architecture provides a solid foundation that can grow with your needs - from a simple blog to a high-traffic enterprise website.
💬 Feedback & Support
Loved the discussion? Have suggestions? Found a bug?
- Blog: analyticalman.com
- Issues: Open a GitHub issue
- Contact: analyticalman.com
Acknowledgments
- Legacy design: @Pangolier
- The open-source community for amazing codes



