How this site is built: WordPress and Elementor in Docker behind a Cloudflare Tunnel

How this site is built: WordPress and Elementor in Docker behind a Cloudflare Tunnel

It felt wrong to run a homelab blog on someone else’s hosting, so this site is self-hosted. But I didn’t want to open ports on my router or put my home IP address in public DNS either. Here’s how it all fits together.

The big picture

Visitor ──► Cloudflare ──► Cloudflare Tunnel ──► cloudflared ─┬─► WordPress (main site)
                                                               └─► Flarum (forum)
                                        WordPress + Flarum ──► MySQL

Everything runs as Docker containers on one VM at home:

  • WordPress with the Astra theme and Elementor for page design
  • Flarum for the community forum
  • MySQL 8 shared by both (separate databases and users)
  • cloudflared, the Cloudflare Tunnel connector

Why a Cloudflare Tunnel?

A tunnel flips the usual model around. Instead of the internet connecting in to my server, the cloudflared container makes an outbound connection to Cloudflare and keeps it open. Visitors hit Cloudflare, and Cloudflare sends the traffic down the tunnel.

That means:

  • No open ports on the router.
  • My home IP never appears in DNS. The site’s records point at Cloudflare.
  • Free HTTPS at Cloudflare’s edge, plus its caching and firewall.

Setting one up is mostly clicking: Cloudflare Zero Trust → Networks → Tunnels → Create, copy the token into the container, then add a route for each hostname pointing at a container name, e.g. http://wordpress:80. Because cloudflared sits on the same Docker network, it reaches the other containers by name.

The compose file (simplified)

services:
  db:
    image: mysql:8
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - ./data/mysql:/var/lib/mysql

  wordpress:
    image: wordpress:latest
    restart: unless-stopped
    depends_on: [db]
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
      WORDPRESS_CONFIG_EXTRA: |
        if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
          $$_SERVER['HTTPS'] = 'on';
        }
    volumes:
      - ./data/wordpress:/var/www/html
      - ./wordpress/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro

  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel run
    environment:
      TUNNEL_TOKEN: ${TUNNEL_TOKEN}

Passwords and the tunnel token live in a .env file, never in the compose file itself.

Two gotchas worth knowing

1. “Too many redirects”. Cloudflare talks HTTPS to visitors but plain HTTP down the tunnel. WordPress sees HTTP, decides it should redirect to HTTPS, and you get an endless loop. The WORDPRESS_CONFIG_EXTRA block above tells WordPress to trust Cloudflare’s X-Forwarded-Proto header. (Note the doubled $$, which stops Docker Compose treating them as variables.)

2. Elementor needs more memory. The default PHP limits are too small for Elementor, and the editor just spins forever. A small uploads.ini mounted into the container fixes it:

memory_limit = 512M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300

Why I switched from Ghost to WordPress

I actually started this site on Ghost. Ghost is lovely: fast, clean and great for writing, but I had used WordPress before, so felt right at home straight away.

Because everything was in Docker, the switch was painless: stop the Ghost container, create a WordPress database, start WordPress and repoint the tunnel. The forum didn’t even notice.

Keeping it secure

Self-hosting a public website means taking security seriously. The checklist I work from:

  • Don’t use admin as the admin username, and keep the password long and in a password manager.
  • Rate-limit login attempts with a plugin.
  • Block /xmlrpc.php with a Cloudflare WAF rule. It’s a favourite target for brute-force attacks.
  • Keep plugins to a minimum. Every plugin is something else to keep updated.
  • Run nightly backups of both databases and the site files to a different machine.

Keeping it updated

  • WordPress core, themes and plugins: update from the dashboard roughly weekly, and turn on auto-updates for plugins you trust.
  • Containers, monthly: back up first, then docker compose pull && docker compose up -d.

What’s next

Next up: getting the Live page hooked up properly for when I’m back streaming on Twitch, and getting the forum going. Come and say hello!

Leave a Comment

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