Configuring wordpress securely with docker + nginx cache

Note: I’m using this approach in this blog.

The problem

I have been using jekyll for a while. Jekyll is great: integrates with github, allows to statically generate blogs and sites so you just serve static files lightning-fast and you don’t have to be worried about being hacked by bad written php scripts.

The main problem with jekyll is that you have to write markdown manually, and you have to do a lot of things manually. Like copying images, linking to them, choosing the date. And you cannot schedule some posts to be published in the future. So you are kinda limited.

With wordpress you can schedule posts, and it has a wysiwyg editor. Also it has tons of plugins and themes already created for it.

I have tried blogger too, but it is too limited and they do not handle well things like inserting code, which is a thing a do a lot.

So can we have best of both worlds easily in our own server? Yes, but it is not trivial.

The solution

I’m going to explain how to configure everything to have a wordpress working in a password-protected subdomain, and then configure the main domain to cache and serve requests from the uncached domain and to limit access to wordpress admin stuff for security. Everything automatically sandboxed and secured by docker containers and letsencrypt powered SSL.

Links:

With this script you are configuring a docker container + a nginx-letsencrypt companion that will  renew automatically all the required certificates:

DIR=`dirname $0`
docker network create --driver bridge reverse-proxy
docker rm -f nginx
docker run -d -p 80:80 -p 443:443 \
  --name=nginx \
  --restart=always \
  --network=reverse-proxy \
  -v $DIR/certs:/etc/nginx/certs:ro \
  -v $DIR/conf.d:/etc/nginx/conf.d \
  -v $DIR/vhost.d:/etc/nginx/vhost.d \
  -v $DIR/html:/usr/share/nginx/html \
  -v /var/run/docker.sock:/tmp/docker.sock:ro \
  -e NGINX_PROXY_CONTAINER=nginx \
  --label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true \
  jwilder/nginx-proxy

docker rm -f nginx-letsencrypt
docker run -d \
    --name nginx-letsencrypt \
    --restart=always \
    --network=reverse-proxy \
    --volumes-from nginx \
    -v $DIR/certs:/etc/nginx/certs:rw \
    -v /var/run/docker.sock:/var/run/docker.sock:ro \
    jrcs/letsencrypt-nginx-proxy-companion

Then you have to create a folder for your wordpress instance. And place these files there:

Dockerfile (custom wordpress increasing file limits):

# Dockerfile

FROM wordpress

RUN echo "file_uploads = On\n" \
         "memory_limit = 64M\n" \
         "upload_max_filesize = 64M\n" \
         "post_max_size = 64M\n" \
         "max_execution_time = 60\n" \
         > /usr/local/etc/php/conf.d/uploads.ini

docker-compose.yml (you have to replace myblog.com and myemail@myblog.com):

version: '2'

services:
  db-wordpress:
    restart: always
    image: mariadb
    volumes:
      - $PWD/db-wordpress:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=wordpress
      - MYSQL_DATABASE=wordpress
      - MYSQL_USER=wordpress
      - MYSQL_PASSWORD=wordpress
    networks:
      - backend
  wordpress:
    depends_on:
      - db-wordpress
    #image: wordpress
    build:
      context: ./
      dockerfile: Dockerfile
    volumes:
      - $PWD/www:/var/www/html
      - $PWD/pass:/var/www/pass
      #- uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
    expose:
      - 80
    environment:
      - WORDPRESS_DB_HOST=db-wordpress:3306
      - WORDPRESS_DB_PASSWORD=wordpress
      - VIRTUAL_HOST=admin.blog.myblog.com
      - LETSENCRYPT_HOST=admin.blog.myblog.com
      - LETSENCRYPT_EMAIL=myemail@myblog.com
    networks:
      - reverse-proxy
      - backend
    restart: always
  cache:
    image: nginx
    networks:
      - reverse-proxy
    restart: always
    volumes:
      - $PWD/cache:/usr/share/nginx/cache
      - $PWD/conf.d:/etc/nginx/conf.d:ro
    expose:
      - 80
    environment:
      VIRTUAL_HOST: blog.myblog.com
      LETSENCRYPT_HOST: blog.myblog.com
      LETSENCRYPT_EMAIL: myemail@myblog.com
      VIRTUAL_PORT: 80

networks:
  backend:
  reverse-proxy:
    external:
      name: reverse-proxy

conf.d/default.conf (you have to replace myblog and REPLACE_WITH_BASE64_ENCODED_USER:PASSWORD):

proxy_cache_path  /usr/share/nginx/cache  levels=1:2    keys_zone=STATIC:10m inactive=48h  max_size=5g;

server {
    listen       80;
    server_name  localhost;

    location /wp-admin {
        return 500 'Internal Error';
    }

    location ~* \.(php)$ {
        return 500 'Internal Error';
    }

    location / {
        add_header Allow "GET, HEAD" always;
        if ( $request_method !~ ^(GET|HEAD)$ ) {
                return 405;
        }

        proxy_pass https://admin.blog.myblog.com/;
        proxy_set_header Authorization "Basic REPLACE_WITH_BASE64_ENCODED_USER:PASSWORD";

        proxy_cache            STATIC;
        proxy_cache_valid      200 302  10m;
        proxy_cache_valid      404  2m;
        proxy_cache_use_stale  error timeout invalid_header updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        proxy_cache_revalidate on;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

pass/.htpasswd

user:myencryptedpassword

Create missing folders:

# First time you up the docker-compose, these folders will be populated
mkdir db-wordpress
mkdir www

Once you have all this done, you can just:

docker-compose up -d

In order to create a proper password for your .htpasswd for your site, you can use htpasswd tool:

docker-compose exec wordpress bash

# Inside the instance
cd /var/www/pass
htpasswd .htpasswd myuser

wp-config.php:

You will have to set your home/siteurl url for this to work. If you are using sub_filter, you can specify your admin url. If not, you can use relative paths, but that would cause some problems with some plugins.

<?php
//define('WP_HOME','https://admin.blog.myblog.com');
//define('WP_SITEURL','https://admin.blog.myblog.com');
define('WP_HOME','/');
define('WP_SITEURL','/');

sub_filter to the rescue:

proxy_set_header Accept-Encoding ""; // Prevents downloading gzip (and sub_filter would use the gzipped stream)
sub_filter_once off;
sub_filter_types text/html; // If you have generated css/js maybe you want to add them too here
sub_filter 'admin.blog.myblog.com' 'blog.myblog.com';