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
anddb
. - We map port 80 in the
wordpress
container to port 3000 on the host machine (because WordPress inside thewordpress
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”:
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:
- Use our open-source product WebP Server Go.
- Use our SaaS product WebP Cloud.
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