Introduction
Most compromised WordPress sites aren’t taken down by nation-state actors burning zero-days. They fall to automated mass-scanners exploiting stale plugins, world-writable directories, and databases exposed to the public internet on default ports.
When I architected the infrastructure for securemyass.com, my goal was simple: assume WordPress core or a third-party plugin will eventually execute untrusted code, and design the host environment so that code is contained in a secure sandbox.
In modern infrastructure, containerization is not just a deployment convenience — it is an isolation boundary. This guide breaks down how to run WordPress in production using Docker, MariaDB, and Traefik with true defense-in-depth: isolated bridge networks, immutable volumes, pre-boot request filtering, and zero exposed database ports.
The Threat Model: What We Are Defending Against
Before configuring a single YAML file, you need a clear threat model. For a web application stack, the primary attack vectors include:
- Web Shell Injection via Plugin Exploits: An unauthenticated file upload flaw in a plugin drops a PHP backdoor into
/wp-content/uploads/. - Database Credential Harvesting & Remote Infiltration: An attacker extracts
wp-config.phpvia local file inclusion (LFI) and attempts to connect directly to the database port from the public internet. - Lateral Container Breakout: Malicious code inside the web container attempts to probe other services on the host or pivot across internal subnets.
- Theme / Plugin Editor Exploitation: An administrative session hijack allows an adversary to edit PHP theme files directly from
/wp-admin/theme-editor.php.
Every architectural decision below eliminates or severely constrains one of these vectors.
1. Network Topology: True Port & Bridge Isolation
The first mistake most Docker users make is binding internal services to 0.0.0.0 or using the host network.
In our production stack, only Traefik listens on host ports 80 and 443. The WordPress container and MariaDB database exist exclusively on an internal Docker bridge network (root_default).
Internet (HTTP/S: 80, 443)
│
▼
┌────────────────────────────────────────┐
│ Traefik Ingress Reverse Proxy │ <-- Automated Let's Encrypt TLS
└───────────────────┬────────────────────┘
│ (Internal Bridge Network: root_default)
┌─────────┴─────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ securemyass-wp │ │ securemyass-db │
│ (PHP 8.3 Apache) │ │ (MariaDB 11) │
│ Port 18080 (int) │ │ Port 3306 (NO PUB)│
└──────────────────┘ └──────────────────┘
Why Port Isolation Matters
Notice that securemyass-db has no port mapping in Docker. The MariaDB port 3306 is not bound to 0.0.0.0, nor is it bound to 127.0.0.1. It can only be reached over the Docker bridge by containers explicitly connected to the same virtual network.
Even if an attacker gains the raw database password through an LFI read, they cannot connect to MariaDB from their command line. They are trapped inside the HTTP request execution envelope.
2. Ingress & Reverse Proxy Hardening with Traefik
Traefik manages automated TLS termination via Let’s Encrypt and forwards traffic to the WordPress web container.
A. Strict TLS & Modern Ciphers
Configure Traefik with automated ACME TLS challenges and modern TLS 1.2+ protocols. This guarantees that all communication between visitors and the edge is encrypted with forward secrecy.
B. Header Sanitization & Real IP Attribution
When running behind a reverse proxy, WordPress default functions like $_SERVER['REMOTE_ADDR'] see only the internal Docker gateway IP (e.g. 172.18.0.1). If you ban an IP based on REMOTE_ADDR, you accidentally ban the entire proxy gateway, taking your site offline.
Traefik securely injects:
X-Forwarded-ForX-Real-IPX-Forwarded-Proto
To prevent header spoofing from external clients, your pre-boot engine must validate that X-Forwarded-For is set by the trusted ingress reverse proxy rather than forged by the client.
3. Web Container Hardening: Resource Envelopes & PHP Execution
Inside the web container (securemyass-wp), we enforce strict execution boundaries via custom PHP configuration (/usr/local/etc/php/conf.d/uploads.ini):
upload_max_filesize = 256M
post_max_size = 256M
memory_limit = 512M
max_execution_time = 600
Disabling In-Dashboard Code Editing
One of the easiest ways for an attacker with hijacked admin credentials to gain persistent remote code execution (RCE) is modifying active themes or plugins via the WordPress built-in editor.
In wp-config.php, unconditionally enforce:
define('DISALLOW_FILE_EDIT', true);
This completely disables the file editor submenus in /wp-admin/, preventing single-click web shell injection through the browser interface.
4. Kernel Pre-Boot Directives (.user.ini)
Traditional security plugins wait until WordPress core (wp-settings.php), plugins, and themes load before executing WAF rules. In volumetric bot floods or credential-stuffing campaigns, loading the entire WordPress PHP runtime for every junk request burns hundreds of megabytes of RAM and exhausts database connections.
To achieve zero-latency pre-boot filtering, we utilize PHP’s native .user.ini mechanism:
auto_prepend_file = '/var/www/html/wp-content/plugins/secure-my-ass-wp-plugin-ui/includes/class-firewall-engine.php'
How Pre-Boot Execution Works:
- When Apache / PHP-FPM receives an incoming HTTP request, the PHP engine parses
.user.inibefore executingindex.php. - The Sentinel Firewall inspects the request URI, headers, Shannon entropy, and SQL injection patterns.
- If the request is malicious, it terminates immediately with
HTTP 403 Forbiddenand exits. - WordPress core, database queries, and heavy theme rendering are never initiated — reducing memory overhead by ~92{7ece1cb04018bdb9f0325a99bd1454aa2464db26882f4094be452d1bbfc5ce90} during volumetric attacks.
[!CAUTION]
Always enforce server rules to prevent direct web downloads of
.user.ini. If an attacker can read.user.ini, they can deduce your prepended file paths.
5. Defense-in-Depth Checklist for Production
Here is the operational checklist every administrator should audit when deploying containerized WordPress:
| Security Control | Implementation | Threat Neutralized | | :— | :— | :— | | No Public DB Port | Omit port mapping on DB container | Direct external SQL brute force & access | | File Edit Lockdown | DISALLOW_FILE_EDIT = true | Admin panel web shell drops | | Isolated Docker Bridge | Dedicated network per application | Lateral pivot across host containers | | Pre-Boot Request Filtering | .user.ini auto_prepend_file | Volumetric CPU & RAM exhaustion | | Non-Default DB Prefix | Hardened prefix (wpcma_) | Automated blind SQL injection assumptions | | Restricted Volume Mounts | Bind-mount only /wp-content | Core file modification persistence |
Conclusion
Security is not a product you install; it is an architectural posture. By isolating MariaDB to internal Docker networking, enforcing TLS termination and header routing through Traefik, locking down code modification in wp-config.php, and deploying pre-boot request filtering via .user.ini, you transform WordPress from a fragile target into a hardened, resilient application stack.