WebP Cloud Services Blog

Quickly Deploying WordPress with Docker-Compose in 2024 and Hiding it Behind Cloudflare, Including WebP/AVIF Image Conversion

Everyone can see that our title is quite long and divided into the following sections:

  • In 2024
  • Quickly Deploy WordPress Using Docker-Compose
  • Hide WordPress Behind the Cloudflare Network
  • Add WebP/AVIF Image Conversion

This article aims to achieve the above three goals, so let’s get started!

Docker and Docker Compose

What Are They

In case some of you are not familiar with Docker and Docker Compose, we asked ChatGPT to generate a brief introduction:

Docker is an open-source containerization platform that allows developers to automate the deployment, scaling, and management of applications.

Docker-Compose is a tool for defining and running multi-container Docker applications. It is mainly used to manage multiple interdependent containers on a single host.

In simple terms, once you have Docker installed, you can run some containers on your machine. For example, you can run a WebP Server Go container and map the /path/to/pics directory on your machine to the /opt/pics directory inside the container:

docker run -d -p 3333:3333 -v /path/to/pics:/opt/pics --name webp-server --restart always webpsh/webp-server-go 

However, managing containers this way can be cumbersome. Each time you start or restart, you need to use commands like docker start webp-server and docker stop webp-server. It is also not easy to communicate with other stateful containers. Therefore, we usually use Docker Compose to manage containers. For example, to accomplish the same task, we can write a docker-compose.yml file with the following content:

version: '3'

services:
  webp-server:
    image: webpsh/webp-server-go
    restart: always
    volumes:
      - /path/to/pics:/opt/pics
    ports:
      - 3333:3333

This way, we can clearly see what the container is, what directories are mapped, and what ports are exposed. Additionally, in the directory containing this docker-compose.yml file, we can start the service with docker-compose up -d and stop it with docker-compose down. Isn’t that convenient?

How to Install

To install Docker and Docker Compose on a machine that hasn’t had them installed before, you can use the following commands (if you are using an AMD64 architecture processor):

curl -fsSL https://get.docker.com -o install-docker.sh && bash install-docker.sh
wget https://github.com/docker/compose/releases/download/v2.31.0/docker-compose-linux-x86_64 -O /usr/bin/docker-compose
chmod +x /usr/bin/docker-compose

If you are using an ARM64 architecture machine (like the Hetzner ARM64 we are using), you can refer to the following commands:

curl -fsSL https://get.docker.com -o install-docker.sh && bash install-docker.sh
wget https://github.com/docker/compose/releases/download/v2.31.0/docker-compose-linux-aarch64 -O /usr/bin/docker-compose
chmod +x /usr/bin/docker-compose

Yes, this is how we install Docker and Docker Compose on WebP Cloud’s internal machines.

Quickly Deploy WordPress Using Docker Compose

Those of you who have been online for a while might remember how to install older versions of WordPress. At that time, the installation process looked like this:

  • Purchase a CPanel hosting
  • Download the WordPress zip package and upload it to the host using Filezilla
  • Unzip the zip package in the CPanel file manager
  • Access your IP/domain in a browser and start configuring WordPress
  • Configure CDN/domain, etc.

In the “VPS” era, the installation process might have looked like this, following various online tutorials:

  • Buy a VPS host
  • Install Nginx/PHP-FPM on the VPS and configure Nginx/PHP-FPM/MySQL
  • Download the WordPress zip package and unzip it to the /var/www directory
  • Continue configuring Nginx/PHP-FPM/MySQL, and after multiple errors and restarts, the site finally runs
  • Configure CDN/domain, etc.

But this is still very tiring as we spend a lot of time configuring the WordPress runtime environment, and almost everyone uses the same configuration.

Let’s think about what WordPress needs:

  • PHP runtime environment
  • Nginx/Apache as the web server
  • MySQL as the database

After understanding this point, we can directly use containers to replace the cumbersome steps mentioned above. Fortunately, WordPress has an official image on DockerHub, which includes the PHP runtime environment and Nginx/Apache as the web server. Then, we just need to find a MySQL image. So, we have the following quick-to-use Compose file for WordPress. You can create an empty directory and create a file named docker-compose.yml in it:

version: '3'

services:
  wordpress:
    image: wordpress:latest
    restart: always
    volumes:
       - ./wordpress:/var/www/html
    ports:
       - 3000:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: password
      WORDPRESS_DB_NAME: db

  db:
    image: ubuntu/mysql:8.0-22.04_beta
    restart: always
    environment:
      MYSQL_DATABASE: 'db'
      MYSQL_ROOT_PASSWORD: 'password'
    volumes:
      - ./db_data:/var/lib/mysql

Let’s explain what we are doing here:

  • We declare that we need to create two containers, named wordpress and db.
  • We map port 80 in the wordpress container to port 3000 on the host machine (because WordPress inside the wordpress container runs on port 80).
  • The WordPress data will be stored in the ./wordpress directory, and the MySQL data will be stored in the ./db_data directory.

At this point, you can start WordPress and MySQL by running docker-compose up -d in this directory, and you can access your WordPress at http://localhost:3000. We have achieved our first small goal: “Quickly deploy WordPress using Docker Compose”!

  • If you need to stop these two containers, just run docker-compose down in this directory.
  • If you need to migrate WordPress, simply stop the containers, move the entire directory, and then start them with docker-compose up -d. There are no external file dependencies.

Hide WordPress Behind the Cloudflare Network

Accessing your WordPress at http://localhost:3000 won’t work for everyone; we need to make our WordPress site accessible to everyone. Many tutorials would teach you to “configure Nginx as a reverse proxy” after setting up WordPress. After a lot of configuration and DNS resolution, you could finally access your site at http://xxx.xxx.xxx.xxx. To add https:// to your site, you might follow the common advice to “use Cloudflare CDN,” create a DNS record pointing to your IP, and enable the “Proxy” option. Now, you can access your site at https://your-domain.tld, and since your-domain.tld uses Cloudflare’s IP, attackers can’t directly see your server’s IP.

However, there’s still a problem. With the current deployment, your server’s ports 80/443 are exposed to the public internet (so Cloudflare can correctly forward traffic to your server). In theory, someone could use IP scanning to find your server’s IP and directly access your site via the IP address. If a malicious actor wants to DDoS your site, it could go down again.

Some people will suggest configuring Nginx to only allow Cloudflare IPs to access your site, as described in How do I deny all requests not from cloudflare? - Server Fault. After another round of configuration, you finally solve this problem.

Is there a simpler way?

Yes, use Cloudflare Tunnel (cloudflared). This software runs on the server and establishes an outbound connection to the Cloudflare network (so your server only needs to expose the SSH port, not any other ports).

You can learn more about Cloudflare Tunnel in the article “Use Cloudflare Argo Tunnel (cloudflared) to accelerate and protect your website.”.

Let’s give it a try!

Create a Cloudflare Tunnel

In the Cloudflare Zero Trust dashboard, select “Network” under “Tunnels” and choose “Cloudflared.”

Give this tunnel a name (you can change it later).

In this panel, you can see the configuration token (the eyJhxxx part).

Since we want to deploy conveniently and consider portability, we will also run Cloudflare Tunnel in a container. The configuration is as follows:

version: '3'

services:
  cloudflared:
    image: cloudflare/cloudflared
    restart: always
    command: --no-autoupdate tunnel run
    environment:
      - TUNNEL_TOKEN=eyJhIjoi.....JMSJ9

Integrate with WordPress

Cloudflare Tunnel can help us expose local services. We have a WordPress instance, so we need to integrate cloudflared with the already installed WordPress. The configuration is as follows:

version: '3'

services:
  wordpress:
    image: wordpress:latest
    restart: always
    volumes:
       - ./wordpress:/var/www/html
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: password
      WORDPRESS_DB_NAME: db

  db:
    image: ubuntu/mysql:8.0-22.04_beta
    restart: always
    environment:
      MYSQL_DATABASE: 'db'
      MYSQL_ROOT_PASSWORD: 'password'
    volumes:
      - ./db_data:/var/lib/mysql

  cloudflared:
    image: cloudflare/cloudflared
    restart: always
    command: --no-autoupdate tunnel run
    environment:
      - TUNNEL_TOKEN=eyJhIjoi.....JMSJ9

Note: I removed the exposed port from the wordpress service because we no longer need to expose the WordPress container’s port to the host machine. It only needs to be accessible within this Compose setup.

If your machine uses a firewall like ufw instead of the provider’s firewall, Docker’s 3000:80 mapping can expose your 3000 port to the public internet due to Docker’s iptables rules, increasing the attack surface.

Docker Compose Networking

Here’s a small piece of knowledge: in the above Docker Compose configuration, wordpress, db, and cloudflared will be in a separate network created for them, and they can communicate with each other using their service names. For example, I can ping the cloudflared container from the wordpress container:

root@4fdc0a781999:/opt# ping cloudflared
PING cloudflared (192.168.160.4) 56(84) bytes of data.
64 bytes from wordpresswebpsh-cloudflared-1.wordpresswebpsh_default (192.168.160.4): icmp_seq=1 ttl=64 time=0.178 ms
64 bytes from wordpresswebpsh-cloudflared-1.wordpresswebpsh_default (192.168.160.4): icmp_seq=2 ttl=64 time=0.069 ms
64 bytes from wordpresswebpsh-cloudflared-1.wordpresswebpsh_default (192.168.160.4): icmp_seq=3 ttl=64 time=0.110 ms
^C
--- cloudflared ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2020ms
rtt min/avg/max/mdev = 0.069/0.119/0.178/0.044 ms

Configure Cloudflared Forwarding

With the above knowledge, we know how to configure Cloudflared. We know the WordPress address is wordpress (within the container) and the port is 80. So we can configure our rules on Cloudflare as follows:

This means:

  • If the incoming request is to https://wordpress.webp.sh/*
  • Then forward the traffic to the http://wordpress service (default port 80)

Our demo site is now live here: https://wordpress.webp.sh/!

The second goal, “Hide WordPress Behind the Cloudflare Network,” is complete. Let’s move on to the final goal!

Why Add WebP/AVIF Image Conversion?

For different users, the original images can be varied, such as JPG/PNG, etc. However, these original formats can be quite large in size. If not optimized, they can slow down user load times and negatively impact PageSpeed scores. Therefore, we need methods to compress or convert images into more efficient formats.

Let’s compare the suggestions provided by Google PageSpeed Insights:

Before converting images to WebP/AVIF, PageSpeed suggests “Serve images in next-gen formats”:

PageSpeed Suggestion Before Conversion

After converting images to WebP/AVIF, this suggestion disappears, and the overall PageSpeed score improves significantly.

What Are We Aiming For?

Let’s outline our requirements:

  • Support for different original image formats (at least JPG/PNG/GIF/HEIC).
  • Ability to convert images to modern, more efficient formats like WebP/AVIF/JPEG XL.
  • Seamless integration into WordPress.
  • Serve different image formats based on browser compatibility (e.g., not serving AVIF to a browser that doesn’t support it, as users wouldn’t be able to see the images).
  • Ideally, serve different image sizes based on screen resolution (though WordPress already indirectly achieves this by saving different sizes of the original image).

At WebP Cloud Services, we specialize in meeting these needs. Here are two options:

Since this article mainly discusses self-hosted solutions, we’ll focus on using WebP Server Go.

WebP Server Go is an open-source product we developed in 2021. The GitHub repository is https://github.com/webp-sh/webp_server_go (give us a star?). It aims to provide a tool that seamlessly converts images from formats like JPG/PNG to WebP/AVIF while keeping the URL unchanged.

For example, if your image is located at /path/to/image.png on your server and accessible via https://your-server.tld/to/image.png, after starting WebP Server Go, you can continue to access the image at https://your-server.tld/to/image.png and receive a smaller WebP/AVIF image. Additionally, older browsers (or cURL) will still display the original image to ensure compatibility.

We know that WordPress stores user-uploaded files in the /wp-content/uploads/ directory. For example, I uploaded an image:

https://wordpress.webp.sh/wp-content/uploads/2024/11/ccc.jpg

WebP Server Go Kicks in!

We just need to mount the ./wordpress directory to the WebP Server. Let’s edit the docker-compose.yml file to add the WebP Server part. Now the file looks like this:

version: '3'

services:
  wordpress:
    image: wordpress:latest
    restart: always
    volumes:
       - ./wordpress:/var/www/html
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: password
      WORDPRESS_DB_NAME: db

  db:
    image: ubuntu/mysql:8.0-22.04_beta
    restart: always
    environment:
      MYSQL_DATABASE: 'db'
      MYSQL_ROOT_PASSWORD: 'password'
    volumes:
      - ./db_data:/var/lib/mysql

  cloudflared:
    image: cloudflare/cloudflared
    restart: always
    command: --no-autoupdate tunnel run
    environment:
      - TUNNEL_TOKEN=eyJhIjoi.....JMSJ9

  webpserver:
    image: ghcr.io/webp-sh/webp_server_go
    restart: always
    volumes:
      - ./wordpress:/opt/pics

After starting with docker-compose up -d, WebP Server will listen on port 3333 inside the container by default. For more parameters and configuration information, you can refer to our documentation: https://docs.webp.sh/usage/configuration/

Next, we need to forward image requests under /wp-content/uploads/ to the webpserver so that WebP Server can handle the image requests. Let’s configure Cloudflare Tunnel:

Forward Image Requests to WebP Server

Remember to adjust the priority to ensure image requests are matched to WebP Server first (put this rule at the top):

Now, when we access the image, we will see that it is output in WebP format, and its size has been reduced to 82% of the original!

Oh, by the way, WebP Server supports HEIC as the original image format. Let’s give it a try:

We uploaded a HEIC image, and the address is https://wordpress.webp.sh/wp-content/uploads/2024/11/sample3.heic. After uploading, WordPress automatically converted the image to JPG, and the given address is: https://wordpress.webp.sh/wp-content/uploads/2024/11/sample3.jpg

The reason for converting HEIC to JPG can be found in the article “WWordPress 6.7 has finally added support for HEIC images? Let’s talk about WebP Cloud’s support for HEIC.

When accessing the given address, we can see the optimized image (from JPG to WebP) by WebP Server:

https://wordpress.webp.sh/wp-content/uploads/2024/11/sample3.jpg

Or if you want to directly access the HEIC image, you can:

https://wordpress.webp.sh/wp-content/uploads/2024/11/sample3.heic

However, there is currently a small issue: after converting the HEIC image to WebP, the WebP image size is larger than the original HEIC image, so WebP Server chooses to render the HEIC image. If your browser doesn’t support rendering HEIC, you won’t see the image. We will fix this issue in the next version. You can follow our PR: https://github.com/webp-sh/webp_server_go/pull/367

Disabling Caching for Image Requests

There is a bit of a tradeoff here. When you use Cloudflare, by default, Cloudflare will cache your images (only at the datacenter where the visitor accesses, and the cache duration depends on how frequently the resource is accessed). This feature is very useful for simple static images. For more details about Cloudflare caching, you can refer to our article “WebP Cloud compared with Cloudflare Polish”.

However, if you use WebP Server, we will output different formats of images based on the browser’s support (for example, we will serve WebP images to browsers that support WebP and serve the original images to browsers that do not support WebP, which are rare in 2024). Cloudflare’s caching mechanism will randomly cache one of the formats:

For example, if the first visitor receives a WebP image and it is cached by Cloudflare, the second visitor accessing the same datacenter will receive the cached WebP image. If the second visitor’s browser does not support WebP, the image will not be displayed.

In this case, we need to disable caching for image requests. This is straightforward; you just need to configure a “Cache Rule” as shown below:

Finally, let’s request the image to confirm that it is indeed bypassing the cache. Here, we use cURL to simulate the request:

curl -H "Accept: image/webp,image/avif" https://wordpress.webp.sh/wp-content/uploads/2024/11/ccc.jpg -I
HTTP/2 200 
...
content-type: image/webp
content-length: 115038
...
x-compression-rate: 0.82
cf-cache-status: DYNAMIC
....

From the cf-cache-status: DYNAMIC, we can see that this request is not being cached by Cloudflare.

Congratulations, we have achieved our final goal!


The WebP Cloud Services team is a small team of three individuals from Shanghai and Helsingborg. Since we are not funded and have no profit pressure, we remain committed to doing what we believe is right. We strive to do our best within the scope of our resources and capabilities. We also engage in various activities without affecting the services we provide to the public, and we continuously explore novel ideas in our products.

If you find this service interesting, feel free to log in to the WebP Cloud Dashboard to experience it. If you’re curious about other magical features it offers, take a look at our WebP Cloud Services Docs. We hope everyone enjoys using it!


Discuss on Hacker News