Running Caddy with Wordpress (php-fpm) with docker compose

This is a “clone and go” docker stack where you can setup a Caddy instance and host a Wordpress site in a few minutes.

Docker and compose files are separated from application config. You don’t need to understand the setup to get going, even do I encourage people to actually understand what they are using. :slight_smile:

Config Injection
Configuration is injected into the docker containers as environment variables. You should have as little config as possible injected into each container to lower the attack vector. Therefor it is better to have many small config files than one large and then inject only what is needed for that application to run.
I will store all config files in the conf directory.

Database
Yes, we need a DB and I will use MariaDB. Let’s make a conf/db.env file and a docker-compose.yml.

conf/db.env:

MYSQL_USER=siteuser
MYSQL_PASSWORD=sitepassword
MYSQL_DATABASE=wordpress
MYSQL_ROOT_PASSWORD=rootpassword
TERM=meh

MariaDB and other applications are mapping the Mysql environment variables and even though this is MariaDB, the variables start with MYSQL.
These are start values that will give us a database during startup. The TERM can be anything, as long as it is set it will be accepted.

The docker-compose.yml:

version: "3.9"
services:
  db:
    image: mariadb:11-jammy
    restart: always
    volumes:
      - dbdata:/var/lib/mysql
    env_file:
      - ./conf/db.env
    logging:
      driver: "json-file"
      options:
        max-size: "1M"
        max-file: "10"
volumes:
  dbdata:
    driver: local

Wordpress
Let’s extend the composition with a Wordpress instance.

conf/wordpress.env:

WORDPRESS_DB_USER=siteuser
WORDPRESS_DB_PASSWORD=sitepassword
WORDPRESS_DB_NAME=wordpress
WORDPRESS_DB_HOST=db:3306

PHP_MEMORY_LIMIT=2048M
PHP_ENABLE_XDEBUG=false
DEBUG=false
UPLOAD_MAX_FILESIZE=64M

The bottom part is optional, but gives a bit of control over the container.

Extending the docker-compose.yml:

  wp-fpm:
    image: wordpress:6-fpm
    restart: always
    links:
      - db
    depends_on:
      - db
    volumes:
      - html:/var/www/html
    env_file:
      - ./conf/wordpress.env
    logging:
      driver: "json-file"
      options:
        max-size: "1M"
        max-file: "10"

volumes:
  html:
    driver: local
    driver_opts:
      type: none
      device: $PWD/html 
      o: bind
  dbdata:
    driver: local

Create an html directory where you have the docker-compose.yml. This dir will act as a mounted volume inside the WP container and will give you direct access to all the site files when up and running.

The fun Part, Adding Caddy!
So this will be bit more complex than just adding an image to the compose file. Adding it this way makes it easy to run the same setup locally, as a test server or as a prod setup. The Caddyfile itself will be pretty static and we will inject extra stuff with our config files.

Let’s create a caddy/etc/Caddyfile:

{
    default_sni {$SERVER_NAME}
}

{$SERVER_NAME} {    
    import /etc/{$TLS_MODE} 
    root * /var/www/html
    encode zstd gzip

    @forbidden {
        not path /wp-includes/ms-files.php
        path /wp-admin/includes/*.php
        path /wp-includes/*.php
        path /wp-config.php
        path /wp-content/uploads/*.php
        path /.user.ini
        path /wp-content/debug.log
    }
    respond @forbidden "Access denied" 403

    php_fastcgi wp-fpm:9000
    file_server

    log {
        output file /var/log/caddy.log
    }

    header / {
      X-Frame-Options "SAMEORIGIN"
      X-Content-Type-Options "nosniff"
    }

}

Also, let’s create two more files in the same directory.
caddy/etc/tls_auto:

tls {$TLS_AUTO_EMAIL}

And caddy/etc/tls_selfsigned:

tls internal

And then our conf/caddy.env:

# replace this with your FQDN if this is a public server
SERVER_NAME=localhost
# replace this with tls_auto if this is a public server
TLS_MODE=tls_selfsigned
TLS_AUTO_EMAIL=your@email.com

As you can see above there is a correlation between the env variables, conf files and the Caddyfile. If you set the TLS_MODE to tls_auto, the other file will be imported into Caddyfile and Caddy will try to fetch a certificate based on your server name.

Adding on to the docker-compose.yml file:

...
  caddy:
    image: caddy:2.7-alpine
    restart: always
    volumes:
      - ./html:/var/www/html
      - ./.caddy_data:/data
      - ./.caddy_config:/config
      - ./caddy/etc/Caddyfile:/etc/caddy/Caddyfile
      - ./caddy/etc/tls_auto:/etc/tls_auto
      - ./caddy/etc/tls_selfsigned:/etc/tls_selfsigned
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    env_file:
      - ./conf/caddy.env
    logging:
      driver: "json-file"
      options:
        max-size: "1M"
        max-file: "10"

So all the files we’ve setup for Caddy are added onto the container as volumes. The env file is injecting its content as environment variables.
You can now change the conffiles and restart to get a new behavior without touching the real application config.

With all this created, we have the last step:

docker compose up -d

Open up your favorite browser and go to https://localhost and you should get to the final steps of completing Wordpress.

If you want to try this setup without the manual copy pasting, you can find a complete working example in this branch:

if you want to dig deeper, with support for more features. Check the main branch of the same repository:

3 Likes

3 posts were split to a new topic: Wordpress Docker