WordPress with Docker, Nginx Proxy Manager, and Cloudflare

Deploying WordPress behind Nginx Proxy Manager requires understanding how reverse proxy layers interact with DNS routing and SSL termination. Misconfigured proxy chains produce redirect loops, failed certificate validation, or exposed origin IPs. This guide covers a production Docker Compose stack and routing architecture decisions for self-hosted WordPress.

Prerequisites

The following items are required:

  • Ubuntu server with Docker Engine and Docker Compose installed
  • Ports 80 and 443 open in UFW
  • Domain managed in Cloudflare DNS
  • Basic familiarity with Docker networking and reverse proxies

Routing Architecture Options

External traffic can reach a self-hosted WordPress instance through three primary methods. Each approach has distinct security, infrastructure, and control characteristics.

Direct to Nginx Proxy Manager

Traffic hits the server public IP directly on ports 80 and 443. Nginx Proxy Manager handles Let’s Encrypt certificate issuance via HTTP-01 challenges and routes requests to the WordPress container.

This approach provides complete data sovereignty. No third party has access to decrypted traffic or request metadata. The server IP address is publicly visible through DNS lookups. Volumetric DDoS attacks target the network interface directly without intermediate filtering.

Cloudflare DNS Proxy with Nginx Proxy Manager

Traffic routes through Cloudflare edge nodes before reaching the origin server. Cloudflare proxies requests to the server public IP. Nginx Proxy Manager handles internal container routing and local SSL certificate management.

This configuration provides DDoS mitigation, edge caching, and IP masking without monthly fees. The server must accept connections on ports 80 and 443. Cloudflare SSL mode must be set to Full (strict) to prevent redirect loops. Both Cloudflare and Nginx Proxy Manager terminate SSL, creating two encryption layers.

Cloudflare Tunnel Direct to Container

The cloudflared daemon runs in Docker and establishes an outbound connection to Cloudflare infrastructure. Cloudflare forwards HTTP traffic directly to the WordPress container port 80. No inbound ports are required on the server firewall.

This approach works through CGNAT and strict network environments. It bypasses local reverse proxy routing rules entirely. Certificate management and access logging are handled by Cloudflare rather than local infrastructure.

Recommended Configuration for Dedicated Servers

For a dedicated server with a static public IP, Cloudflare DNS Proxy combined with Nginx Proxy Manager provides the best balance. Cloudflare handles DDoS protection and caching while hiding the origin IP. Nginx Proxy Manager provides local control over SSL certificates, custom headers, rate limiting, and routing for multiple containers.

For home connections behind CGNAT or environments requiring zero open inbound ports, route Cloudflare Tunnel output to Nginx Proxy Manager internally. This preserves local routing control without exposing ports to the public internet.

Deploy Docker Compose Stack

Create a directory for the WordPress deployment:

mkdir -p /opt/containers/wordpress-stack
cd /opt/containers/wordpress-stack

Create a docker-compose.yml file with the following content:

version: '3.8'

networks:
  npm-network:
    driver: bridge

services:
  npm:
    image: 'jc21/nginx-proxy-manager:latest'
    container_name: npm
    restart: always
    ports:
      - '80:80'
      - '81:81'
      - '443:443'
    volumes:
      - ./npm/data:/data
      - ./npm/letsencrypt:/etc/letsencrypt
    networks:
      - npm-network

  mariadb:
    image: mariadb:11
    container_name: wordpress-db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 'ChangeThisSuperSecureRootPassword'
      MYSQL_DATABASE: 'wordpress_db'
      MYSQL_USER: 'wp_user'
      MYSQL_PASSWORD: 'ChangeThisUserPassword'
    volumes:
      - ./db_data:/var/lib/mysql
    networks:
      - npm-network

  wordpress:
    image: wordpress:latest
    container_name: wordpress-app
    restart: always
    depends_on:
      - mariadb
    environment:
      WORDPRESS_DB_HOST: mariadb:3306
      WORDPRESS_DB_USER: wp_user
      WORDPRESS_DB_PASSWORD: 'ChangeThisUserPassword'
      WORDPRESS_DB_NAME: wordpress_db
    volumes:
      - ./wp_data:/var/www/html
    networks:
      - npm-network

This configuration creates three containers on a shared bridge network. Nginx Proxy Manager exposes ports 80, 443, and 81. Port 81 provides access to the NPM admin interface. MariaDB and WordPress communicate over the internal Docker network without exposed ports.

Start the stack:

docker compose up -d

Verify all containers are running:

docker ps

Configure DNS Records

Log into the Cloudflare Dashboard and navigate to DNS Records for the target domain. Add or update an A record:

  • Type: A
  • Name: @ (for root domain) or desired subdomain
  • IPv4 address: Server public IP
  • Proxy status: Proxied (orange cloud) for Cloudflare protection, DNS only (grey cloud) for direct routing
  • TTL: Auto

DNS propagation typically completes within minutes but can take up to 48 hours in some cases.

Configure Nginx Proxy Manager

Access the NPM admin interface at http://SERVER_IP:81. Log in with default credentials:

  • Email: admin@example.com
  • Password: changeme

Update the admin email and password when prompted.

Create Proxy Host

Navigate to Hosts, then Proxy Hosts, then Add Proxy Host. Configure the Details tab:

  • Domain Names: example.com www.example.com
  • Scheme: http
  • Forward Hostname / IP: wordpress-app
  • Forward Port: 80
  • Enable Block Common Exploits
  • Enable Websockets Support if required

The Forward Hostname uses the Docker container name. Docker’s internal DNS resolves container names to their bridge network IP addresses.

Configure the SSL tab:

  • SSL Certificate: Request a new SSL Certificate
  • Enable Force SSL
  • Enable HTTP/2 Support
  • Agree to Let’s Encrypt Terms of Service

Save the configuration. Nginx Proxy Manager will request a Let’s Encrypt certificate using the HTTP-01 challenge method.

Configure Cloudflare SSL Settings

If using Cloudflare DNS Proxy (orange cloud), configure SSL/TLS settings in the Cloudflare Dashboard. Navigate to SSL/TLS, then Overview. Set encryption mode to Full (strict).

This setting ensures end-to-end encryption between the client browser, Cloudflare edge, and origin server. Flexible mode terminates SSL at the Cloudflare edge and uses unencrypted HTTP to the origin. Full mode allows self-signed certificates on the origin. Full (strict) requires valid certificates on both Cloudflare and the origin server.

Complete WordPress Installation

Navigate to the configured domain in a web browser. The WordPress installation screen should appear. Select language, configure site title, and create an administrator account.

If the page does not load, check the following:

  • Verify DNS resolution points to the correct IP address using dig or nslookup
  • Check container logs with docker logs wordpress-app and docker logs npm
  • Confirm UFW allows ports 80 and 443
  • Verify Cloudflare SSL mode is set to Full (strict) if using Cloudflare Proxy

Troubleshooting Common Issues

ERR_TOO_MANY_REDIRECTS

This error occurs when Cloudflare SSL mode is set to Flexible while Nginx Proxy Manager forces HTTPS. Cloudflare receives HTTPS redirects from the origin but sends HTTP requests, creating an infinite loop. Set Cloudflare SSL mode to Full (strict).

Certificate Validation Failures

Let’s Encrypt HTTP-01 challenges require port 80 to be accessible from the public internet. Cloudflare Proxy must allow HTTP traffic to pass through for initial certificate issuance. After the certificate is issued, Force SSL can redirect HTTP to HTTPS.

Container Name Resolution Failures

If Nginx Proxy Manager cannot resolve the wordpress-app hostname, verify both containers are on the same Docker network. Check network membership with docker network inspect wordpress-stack_npm-network.

AI assistance is used on this site for language, formatting, and turning research into a consistent template. It is not used to perform the underlying research or verify technical claims. Every command, configuration, and step in this post is tested by hand before publication.

Leave a Reply

Your email address will not be published. Required fields are marked *

fourteen + 2 =