Dockerized Wordpress + Discourse + Caddy


(Andrew) #1

Continuing the discussion from Running Discourse with Caddy Server:

I’m coming over from a misbehaving nginx + wp + discourse scenario. Since Nginx reverse proxy was throwing weird errors on the Discourse side, I was lured here by rumors of easy configuration files. Buuuut, I guess easy is relative.

Caddy: :heavy_check_mark: (h/t @matt & @abiosoft)
Discourse: :heavy_check_mark: on forums.example.com (h/t @Falco)
WordPress: 502 bad gateway on wp.example.com via Cloudflare
Portainer: 502 bad gateway on port.example.com via Cloudflare

Caddy / Docker

I suspect, due to Cloudflare (among other things), that I may need a custom build to include some plugins. Following the abiosoft/caddy description, I was looking at something like this:

docker build --build-arg \
    plugins=realip,git,cloudflare,upload \
    github.com/abiosoft/caddy-docker.git

In fact, I did that build once, but quit when I realized PHP wasn’t in the mix. I guess I also need the :php flag on the image. For what’s working now, I used Falco’s docker run command, but with the :php flag (and proper email).

  1. Is there a way to build from abiosoft’s image to get PHP + the plugins?
  2. Do I need realip and cloudflare to use Cloudflare?

Ports

I grabbed a screenshot from Portainer, mainly because it’s more compact than my terminal:

Portainer

Caddyfile

forums.example.com {
 #Discourse
 proxy / unix:/sock/nginx.http.sock {
        transparent
    }
}

port.example.com {
 #Portainer
    proxy / 9000:80 {
        transparent
    }
}

wp.example.com {
  #Wordpress
    proxy / 80:80 {
        transparent
    }
}

Since I suspect my Caddyfile is wrong AND I need plugins, getting help on either half of the equation would allow me to dig into the remaining variables. The Caddyfile WP example leads me to believe I still have some work to do beyond just getting the proxy bit right.

WordPress

Whoa, so many options between alpine, fpm, and the various combinations. Is there one image I should be flagging with a better chance it will play nicely with everything else?

I should also say that Portainer isn’t really crucial to this endeavor. I installed it to see if it might help troubleshoot after the other stuff wasn’t working. It seems like that subdomain is suffering from the same problem as WP, so I’d guess the solution for both of them would be similar.

[Update: Soved]

This was basically solved by @Whitestrake in post 2. The files in the “Solved” post below are 100% functional in terms of the goals of the topic. Wordpress (Apache) + Discourse + Caddy + Cloudflare work in that configuration. It is easy to spin up multiple Wordpress sites with that same configuration simply by repeating the Wordpress and MariaDB section in docker-compose.yml.

[Addendum]

The ultimate goal of having this work with a slim Wordpress Docker image based on Alpine and PHP-FPM is currently not 100%. The discussion below the “Solved” post relates to attempts to get that working.


(Matthew Fay) #2

You might be overcomplicating things a fair bit. For example, wordpress:latest has Apache and PHP, so you don’t need PHP in your Caddy container, you don’t even need fastcgi, just proxy everything to the WordPress container.

Here’s my boilerplate configuration for WordPress on Docker served by Caddy.

docker-compose.yml
version: '3'

services:
  caddy:
    image: abiosoft/caddy:latest
    command: ["-log", "stdout",
      "-email", "letsencrypt@example.com",
      "-conf", "/etc/Caddyfile"]
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./Caddyfile:/etc/Caddyfile
      - ./certificates:/root/.caddy
#    restart: unless-stopped

  wordpress:
    image: wordpress:latest
    depends_on:
      - wordpress-db
    environment:
      WORDPRESS_DB_PASSWORD: [snip]
      WORDPRESS_DB_HOST: wordpress-db
    volumes:
      - ./wordpress:/var/www/html
#    restart: unless-stopped

  wordpress-db:
    image: mariadb:latest
    environment:
      MYSQL_ROOT_PASSWORD: [snip]
    volumes:
      - ./wordpress-db:/var/lib/mysql
#    restart: unless-stopped
Caddyfile
example.com {
  proxy / wordpress {
    transparent
  }
}

Portainer would be very similar and easy to add, another Compose service and a proxy.

I like how you’ve got Discourse on an unix socket!

This format won’t work. You need a valid host and port to forward to. If you know the IP of the host, you can use that, but I prefer to use Composed networks wherever possible and refer to services by their container name (as shown in the boilerplate example). This way I don’t need to publish any ports other than Caddy’s.

The Caddyfile WP example is for if Caddy were serving WordPress files itself. This would be the case if you downloaded a distribution of WP and unzipped it to your web root and ran Caddy with PHP on-host; you’re running it off the official Docker image instead, which is a prepackaged deal.

Yep, use the PHP Dockerfile instead of the base repository Dockerfile by using the raw URL instead of the git URL. Plugins build arg still applies.

docker build --build-arg \
  plugins=filemanager,git,linode \
  https://raw.githubusercontent.com/abiosoft/caddy-docker/master/php/Dockerfile

Need? No, to both of the above. They both have a purpose, though, and could be quite helpful. realip can help for instances where you have orange-clouded your domain in Cloudflare as it will translate all instances of the remote IP (which will be Cloudflare itself 99% of the time) to the real remote client (given by Cloudflare as X-Forwarded-For among other headers), including in your server logs.

cloudflare is a DNS plugin used for DNS validation and might be necessary if you orange-cloud your A records.


(Andrew) #3

Okay, cool. This gives me a lot to go on.

It sounds like, for testing purposes, gray-clouding A records, replacing the port:mess with hostnames in Caddyfile, and using wordpress:latest would get me most of the way there. I’ll try that first, then try a plugin build of abiosoft to go orange-cloud if that works.


(Matthew Fay) #4

I’m not a fan of orange-cloud in front of Caddy. It just complicates things too much since you lose control of TLS termination. Not to mention, Cloudflare doesn’t proxy non-HTTP(S) protocols or ports.

It’s simpler if you think of Caddy for your requirements as a single-host load balancer with automatic HTTPS certificates. You could put wordpress:latest directly on port 80 on the host, and access it just fine without Caddy there; Caddy just gives you turn-key HTTPS. In a way it operates in much the same role as Cloudflare does with orange-cloud records.


(Andrew) #5

Only loosely related, but I just stumbled across this post by @matt while doing some research on Cloudflare Warp for K8s ingress.

Not to threadjack my own thread or anything. :innocent:


(Matthew Fay) #6

Aye, pretty keen to see more about that coming out!


(Andrew) #7

Got the crucial bits working (WordPress, Docker and Caddy). Haven’t quite tackled Cloudflare and misc plugins yet.

Caddyfile
forums.example.com {
  proxy / unix:/sock/nginx.http.sock {
    transparent
  }
}

blog.example.com {
  proxy / wordpress {
    transparent
  }
}

There are some extra database details in here that are probably redundant, and I’d still like to get it on a slimmed down wordpress:alpine, but it’s working for now. Actually, I might mess around with a WP-CLI image before trying to figure out alpine. I usually do multisite WP installs, and CLI is helpful for that.

docker-compose.yml
version: '3.2'

services:
  wordpress:
    image: wordpress
    depends_on:
      - wordpress-db
    environment:
      WORDPRESS_DB_PASSWORD: noneshallpass
      WORDPRESS_DB_HOST: wordpress-db
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - /var/www/html:/var/www/html
    restart: unless-stopped

  wordpress-db:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: noneshallpass
      MYSQL_USER: wp
      MYSQL_PASSWORD: noneshallpass
      MYSQL_DATABASE: wordpress
    volumes:
      - /var/caddy/wordpress-db:/var/lib/mysql
    restart: unless-stopped

  caddy:
    image: abiosoft/caddy:0.10.12
    command: ["-email", "letsencrypt@example.com",
      "-conf", "/etc/Caddyfile",
      "-agree"]
    ports:
      - 80:80
      - 80:80/udp
      - 443:443
      - 443:443/udp
    volumes:
      - /var/caddy/Caddyfile:/etc/Caddyfile
      - /var/caddy:/root/.caddy
      - /var/discourse/shared/standalone:/sock
    restart: unless-stopped
#    entrypoint: /usr/bin/caddy

TL:DR; Wordpress + Discourse + Caddy

  1. Follow @Falco’s Discourse with Caddy tutorial up to the app.yml tweaks and rebuild (don’t apply the docker run... part).
  2. Tweak the Caddyfile above to suit your needs
  3. Apply the above docker-compose.yml*
  4. Enjoy a life of trying to figure out if what you have to say is important enough to elevate from a forum topic to a blog post.

*In addition to this being untested and rough, it would be a good idea to change around the passwords and stuff.

Thank you for getting me headed in the right direction as well as nudging me toward docker-compose, Matthew. Although I feel like I wasted half a day before figuring out whatever dev os I was on (CoreOS?) was only on v1.8.0, I needed to figure it out sooner than later. Ouch!

Not quite ready for Discourse Deployment on k8s with a side order of Caddy on Kubernetes yet, but who knows what the world will hold after a few hours of sleep.


(Matthew Fay) #8

You’re welcome! If you find a neat WP-CLI solution for WordPress on Docker, let me know, it’s such a neat tool for some situations (especially database editing, which is inconvenient enough on shared hosts that have phpMyAdmin).

And you’ve got at least one interested party for a blog post, especially if you proceed with the k8s stuff.


(Andrew) #9

Hm. Trying this on a new server with the same version of Ubuntu (16.04), Docker (17.12.0-ce) and docker-compose (1.18.0), but something is amiss.

Old/Working

{expand}
# docker-compose up
Creating caddy_wordpress-db_1 ... done
Creating caddy_wordpress-db_1 ...
Creating caddy_caddy_1        ... done
Creating caddy_wordpress_1    ... done
Attaching to caddy_wordpress-db_1, caddy_caddy_1, caddy_wordpress_1
caddy_1         | Activating privacy features... done.
caddy_1         | https://forum.example.com
caddy_1         | https://blog.example.com
caddy_1         | http://forum.example.com
caddy_1         | http://blog.example.com
wordpress-db_1  | ...

New/Fail

# docker-compose up
Creating caddy_wordpress-db_1 ... done
Creating caddy_wordpress-db_1 ...
Creating caddy_caddy_1        ... done
Creating caddy_wordpress_1    ... done
Attaching to caddy_wordpress-db_1, caddy_caddy_1, caddy_wordpress_1
caddy_1         | Activating privacy features...
caddy_1         |
caddy_1         | Your sites will be served over HTTPS automatically using Let's Encrypt.
caddy_1         | By continuing, you agree to the Let's Encrypt Subscriber Agreement at:
caddy_1         |   https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf
caddy_1         | Do you agree to the terms? (y/n): 2018/04/15 01:48:12 user must agree to CA terms (use -agree flag)
caddy_1         | exit status 1

Same Caddyfile and docker-compose.yml.

Ideas?

EDIT: Setting the tag to 0.10.11 resolved the issue. I’ll try to track down the fix for 0.10.12, and update this thread if I figure it out before someone else does.

EDIT [Fix applied to docker-compose.yml above]: Added -agree flag. Tagged the 0.10.12 image so the next upgrade doesn’t break the script.


(Matthew Fay) #10

Aye, we went to ACMEv2 in the latest version, 0.10.12, and LE changed their terms of service and require a new explicit agreement to that. The -agree flag is the correct answer as you’ve found.


(Andrew) #11

I recalled mention of Acme v2 in the 0.10.12 release blog post. What threw me was that it was the “Do you agree to the terms? (y/n)” prompt. It allowed entering “y”, but would fail anyway. Oh well, it didn’t take me tooooooo long to figure it out. :smile:


(Andrew) #12

Any guidance on the proper Caddyfile syntax for the fpm-alpine variants of the Wordpress Docker images?

I started with the Wordpress Caddyfile example and tried a few variations which all yield 502s.

I read through 10 or so forum posts that kinda sorta deal with similar issues, but wasn’t able to piece it together.


(Matthew Fay) #13

Add errors stdout to your Caddyfile and let us know what comes up when you get a 502.


(Andrew) #14

I tore this down for now. Is there a working Caddyfile example somewhere that uses Docker (abiosoft/caddy) and fpm? I’m happy to log errors when I rebuild, but a known-to-be-working example would help too.


(Matthew Fay) #15

This is known good:

docker-compose.yml
version: '3'

services:
  caddy:
    image: abiosoft/caddy:latest
    command: ["-log", "stdout", "-agree",
      "-email", "letsencrypt@whitestrake.net",
      "-conf", "/etc/Caddyfiles/Caddyfile"]
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./certificates:/root/.caddy
      - ./caddyfiles:/etc/Caddyfiles
      - ./keys:/root/keys
      - ./sites:/srv
    restart: always

  php-fpm:
    build: ./php-dockerfile
    volumes:
      - ./sites:/srv
    restart: always
php-dockerfile/Dockerfile
FROM php:fpm-alpine
RUN apk add --no-cache libpng-dev
RUN docker-php-ext-install gd zip
caddyfiles/Caddyfile
http://www.whitestrake.net, https://www.whitestrake.net {
  redir https://whitestrake.net{uri}
}

whitestrake.net {
  root /srv/whitestrake
  git {
    repo git@gitlab.com:Whitestrake/whitestrake-home.git
    key /root/keys/id_rsa
    hook /c3b9edf5e8609ca3419c4a007448cd8d
  }
  gzip
  push
  fastcgi / php-fpm:9000 php
}

(Andrew) #16

I made some headway on this. I think the 502 problem was that I was targeting the external directory of the bind-mount. Those errors are gone, but it’s not exactly working.

The Good

wp-main_1          | [23-Apr-2018 00:02:50] NOTICE: fpm is running, pid 1
wp-main_1          | [23-Apr-2018 00:02:50] NOTICE: ready to handle connections

The Bad

When accessing https://blog.example.com I get Access denied. in the browser, and this in terminal:

403 security.limit_extension errors
wp-main_1          | 172.xx.xx.xx -  23/Apr/2018:00:14:00 +0000 "GET /" 403
wp-main_1          | [23-Apr-2018 00:14:00] WARNING: [pool www] child 57 said into stderr: "NOTICE: Access to the script '/var/www/html' has been denied (see security.limit_extensions)"
caddy_1            | 23/Apr/2018:00:14:00 +0000 [ERROR 0 /] Access to the script '/var/www/html' has been denied (see security.limit_extensions)

Then if I manually enter https://blog.example.com/wp-admin/install.php, I get an html-only page. There’s the expected content, but no .css or .js niceness. This is accompanied by this output:

CSS and JS security.limit_extensions errors
wp-main_1          | 172.xx.xx.xx -  23/Apr/2018:00:16:00 +0000 "GET /" 403
caddy_1            | 23/Apr/2018:00:16:00 +0000 [ERROR 0 /wp-includes/css/dashicons.min.css] Access to the script '/var/www/html/wp-includes/css/dashicons.min.css' has been denied (see security.limit_extensions)
caddy_1            | 23/Apr/2018:00:16:00 +0000 [ERROR 0 /wp-includes/js/jquery/jquery-migrate.min.js] Access to the script '/var/www/html/wp-includes/js/jquery/jquery-migrate.min.js' has been denied (see security.limit_extensions)
wp-main_1          | 172.xx.xx.xx -  23/Apr/2018:00:16:00 +0000 "GET /" 403
wp-main_1          | [23-Apr-2018 00:16:00] WARNING: [pool www] child 57 said into stderr: "NOTICE: Access to the script '/var/www/html/wp-admin/js/language-chooser.min.js' has been denied (see security.limit_extensions)"
caddy_1            | 23/Apr/2018:00:16:00 +0000 [ERROR 0 /wp-admin/js/language-chooser.min.js] Access to the script '/var/www/html/wp-admin/js/language-chooser.min.js' has been denied (see security.limit_extensions)

The Ugly

docker-compose.yml
version: '3.2'

services:
  wp-main:
    image: registry.gitlab.com/jetatomic/wordpress
    depends_on:
      - main-db
    environment:
      WORDPRESS_DB_PASSWORD: n0ne5hallp@55
      WORDPRESS_DB_HOST: main-db
      WORDPRESS_DB_USER: wpdrone
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - /var/www/main.wp:/var/www/html
    restart: always

  main-db:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: n0ne5hallp@55
      MYSQL_USER: wpdrone
      MYSQL_PASSWORD: n0ne5hallp@55
      MYSQL_DATABASE: wordpress
    volumes:
      - /var/farm/mysql/main-db:/var/lib/mysql
    restart: always

    caddy:
      image: abiosoft/caddy:0.10.12
      command: ["-email", "letsencrypt@example.com",
        "-conf", "/etc/Caddyfile",
        "-agree"]
      ports:
        - 80:80
        - 80:80/udp
        - 443:443
        - 443:443/udp
      volumes:
        - /var/farm/caddy/Caddyfile:/etc/Caddyfile
        - /var/farm/caddy:/root/.caddy
        - /var/discourse/shared/standalone:/sock
      restart: always
Caddyfile
blog.example.com {
  root /var/www/html
  gzip
  push
  errors stdout
  fastcgi / wp-main:9000
}
Dockerfile
FROM wordpress:4.9.5-php7.2-fpm-alpine

RUN apk add --no-cache \
  libpng-dev \
	wget \
	unzip \
	nano \
	sudo \
	&& docker-php-ext-configure gd --with-png-dir=/usr \
  && docker-php-ext-install gd zip 

  # Then more custom config junk

Since the security.limit_extensions is a PHP-FPM thing, I suspect there’s a package or config missing in the Dockerfile. Rewrite? Preliminary googling shows methods for tweaking that extension, but everything so far looks sketchy security-wise.

So I’ll keep researching, and update if I get a solution. Until then, I’m open to suggestions.

Edit: Related thread, but no clear solution.


(Matthew Fay) #17

Change this line from your Caddyfile:

fastcgi / wp-main:9000

to:

fastcgi / wp-main:9000 php

otherwise, all requests will be proxied to the PHP-PFM listener (not just .php files).

I believe the security limit_extensions errors are appearing when requests for non-PHP indexes or static files are received, which should be handled directly by Caddy and not by the PHP-FPM listener.


(Andrew) #18

Yes, I had it that way before and after posting that Caddyfile example. I guess I just copy/pasted it between tests. The behavior above happens with php added to the line.

I understand what those words mean, but I don’t know how to effect that in Caddyspeak (or Nginx, for that matter, though there are a bunch of examples of Nginx rewrites for WP).


(Matthew Fay) #19

The PHP preset should handle that - only PHP scripts should be sent when the preset is added. Here’s what php does:

ext   .php         # specifies extensions which will get proxied
split .php         # tells Caddy that anything after this goes in PATH_INFO
index index.php    # specifies a default file to try for an index request

So you can see with it enabled (or these settings manually specified - php is just shorthand), no non-PHP file should ever be sent via FastCGI.

Can you double check it’s present in the Caddyfile, restart Caddy, and let me know if the CSS and JS requests still get sent to PHP?


(Andrew) #20

Added this from this write-up:

  rewrite {
      if {path} not_match ^\/wp-admin
      to {path} {path}/ /index.php?_url={uri}
  }

Doesn’t resolve the security error, but it helps with the redirect from https://blog.example.com