├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── VERSION ├── ansible ├── deploy-site.yml └── press-site.yml ├── docker-compose.override.yml ├── docker-compose.yml ├── docs ├── CHANGELOG.md ├── Cookbook.md ├── TODO.md └── env-sample ├── hooks └── build ├── misc-configs ├── apt-preferences ├── nginx-forbidden.conf ├── nginx-noproxy.conf ├── ssh_config ├── supervisord.conf ├── wordpress.conf └── wp-config-sample.php ├── nginx-configs ├── acme.challenge.le.conf ├── le.ini ├── nginx.conf ├── restrictions.conf ├── security_headers.conf ├── sites-available │ └── nginx-site.conf └── ssl.conf ├── schedulers-configs ├── 02periodic ├── 50unattended-upgrades └── wordpress.cron ├── scripts ├── bootstrap_container ├── install_wordpress └── setup_web_cert └── website ├── README.md └── make_env /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | .env 4 | .env.bak 5 | scratchpad 6 | ansible-digital_ocean.cache 7 | hosts 8 | digital_ocean.py 9 | website/wordpress 10 | website/wordpress.sql 11 | website/VERSION 12 | *.retry 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | services: docker 3 | 4 | before_install: 5 | - docker network create -d bridge rijanet 6 | - docker run -d --name rijamysql --net=rijanet --env MYSQL_ROOT_PASSWORD=changemeroot --env MYSQL_DATABASE=wordpress --env MYSQL_USER=wordpress --env MYSQL_PASSWORD=changemeuser mysql:5.5.45 7 | - RIJAMYSQL=$(docker inspect -f '{{.Config.Hostname }}' rijamysql) 8 | - echo "$RIJAMYSQL" 9 | 10 | script: 11 | - git clone https://github.com/WordPress/WordPress.git website/wordpress 12 | - docker build -t rija/docker-nginx-fpm-caches-wordpress --build-arg VCS_REF=`git rev-parse — short HEAD` --build-arg BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”` --build-arg VERSION=`cat VERSION` . 13 | - docker run --privileged -d --name rijaweb --net=rijanet --env SERVER_NAME=dockerfiletest.cinecinetique.com --env DB_HOSTNAME=$RIJAMYSQL --env DB_USER=wordpress --env DB_PASSWORD=changemeuser --env DB_DATABASE=wordpress -v /etc/letsencrypt:/etc/letsencrypt -p 443:443 -p 80:80 rija/docker-nginx-fpm-caches-wordpress 14 | - docker exec -it rijaweb bash -c "letsencrypt certonly --help" 15 | - sleep 15 16 | - docker exec -it rijaweb bash -c "test -f /tmp/last_bootstrap" 17 | - docker exec -it rijaweb bash -c "cat /etc/hosts" 18 | - docker exec -it rijaweb bash -c "cat /etc/letsencrypt/cli.ini" 19 | - sleep 10 20 | - docker exec -it rijaweb bash -c "ps -e" 21 | - docker exec -it rijaweb bash -c "ps -e" | grep "supervisor" 22 | - docker exec -it rijaweb bash -c "ps -e" | grep "nginx" 23 | - docker exec -it rijaweb bash -c "ps -e" | grep "php-fpm7.1" 24 | - docker exec -it rijaweb bash -c "ps -e" | grep "cron" 25 | - docker exec -it rijaweb bash -c "curl --head http://dockerfiletest.cinecinetique.com" 26 | - docker exec -it rijaweb bash -c "curl --head http://dockerfiletest.cinecinetique.com" | grep 301 27 | - sleep 30 28 | - docker exec -it rijaweb bash -c "cat /usr/share/nginx/www/wp-config.php" 29 | - docker exec -it rijaweb bash -c "ls -alrt /usr/share/nginx/www/" | grep "wp-content" 30 | 31 | after_failure: 32 | - docker exec -it rijaweb bash -c "supervisorctl tail bootstrap" 33 | - docker exec -it rijaweb bash -c "supervisorctl tail install_wordpress" 34 | - docker logs rijaweb 35 | - docker exec -it rijaweb bash -c "cat /etc/hosts" 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitnami/minideb:jessie 2 | MAINTAINER Rija Menage 3 | 4 | EXPOSE 80 5 | EXPOSE 443 6 | 7 | ENTRYPOINT ["/usr/bin/supervisord"] 8 | CMD ["-c", "/etc/supervisor/supervisord.conf"] 9 | 10 | # Enabling https download of packages 11 | RUN install_packages apt-transport-https ca-certificates \ 12 | 13 | # Basic Dependencies 14 | 15 | && install_packages \ 16 | # basic process management 17 | procps \ 18 | # used to download sources for nginx and gosu, as well as gpg signature and keys 19 | curl \ 20 | # used for installing the Wordpress web application from online git repositories 21 | git \ 22 | # installed for the ip utility used in bootstrap_container for finding the container's external ip address 23 | # also used as an action for fail2ban 24 | iproute2 \ 25 | # used to run cert auto-renewal, database backup and Wordpress scheduled tasks 26 | cron \ 27 | # manage all processes in the container, act as init script, has PID 1 and handles POSIX signals 28 | supervisor \ 29 | # for automated security updates 30 | unattended-upgrades \ 31 | # tool to manage malicious connections to the web application through IP addresses black-listing 32 | fail2ban \ 33 | # used by the automated backup script 34 | mysql-client \ 35 | # firewall, used in conjunction with fail2ban 36 | ufw \ 37 | # syslog daemon needed by fail2ban's Wordpress plugin 38 | rsyslog \ 39 | # install the tool to rotate logs 40 | logrotate 41 | 42 | ENV GOSU_VERSION 1.7 43 | ENV NGINX_VERSION 1.13.0 44 | ENV PHP_VERSION 7.1 45 | 46 | # php installation 47 | 48 | RUN curl -o /etc/apt/trusted.gpg.d/php.gpg -fsSL https://packages.sury.org/php/apt.gpg \ 49 | && echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/php.list \ 50 | && install_packages php$PHP_VERSION \ 51 | php$PHP_VERSION-fpm \ 52 | php$PHP_VERSION-cli \ 53 | php$PHP_VERSION-mysql \ 54 | php$PHP_VERSION-gd \ 55 | php$PHP_VERSION-intl \ 56 | php$PHP_VERSION-imagick \ 57 | php$PHP_VERSION-imap \ 58 | php$PHP_VERSION-mcrypt \ 59 | php$PHP_VERSION-pspell \ 60 | php$PHP_VERSION-recode \ 61 | php$PHP_VERSION-tidy \ 62 | php$PHP_VERSION-xml \ 63 | php$PHP_VERSION-json \ 64 | php$PHP_VERSION-opcache \ 65 | php$PHP_VERSION-mbstring 66 | 67 | 68 | 69 | 70 | # Download Nginx and Ngx cache purge source code 71 | 72 | RUN install_packages build-essential zlib1g-dev libpcre3-dev libssl-dev libgeoip-dev nginx-common \ 73 | && GPG_KEYS=B0F4253373F8F6F510D42178520A9993A1C052F8 \ 74 | && cd /tmp \ 75 | && curl -O -fsSL https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz \ 76 | && curl -O -fsSL https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz.asc \ 77 | && export GNUPGHOME="$(mktemp -d)" \ 78 | && found=''; \ 79 | for server in \ 80 | ha.pool.sks-keyservers.net \ 81 | hkp://keyserver.ubuntu.com:80 \ 82 | hkp://p80.pool.sks-keyservers.net:80 \ 83 | pgp.mit.edu \ 84 | ; do \ 85 | echo "Fetching GPG key $GPG_KEYS from $server"; \ 86 | gpg --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$GPG_KEYS" && found=yes && break; \ 87 | done; \ 88 | test -z "$found" && echo >&2 "error: failed to fetch GPG key $GPG_KEYS" && exit 1; \ 89 | gpg --batch --verify nginx-$NGINX_VERSION.tar.gz.asc nginx-$NGINX_VERSION.tar.gz \ 90 | && rm -r "$GNUPGHOME" nginx-$NGINX_VERSION.tar.gz.asc \ 91 | && tar xzvf nginx-$NGINX_VERSION.tar.gz \ 92 | && curl -o ngx_cache_purge-2.3.tar.gz -fsSL https://github.com/FRiCKLE/ngx_cache_purge/archive/2.3.tar.gz \ 93 | && tar xzvf ngx_cache_purge-2.3.tar.gz \ 94 | 95 | # Compile nginx from source with ngx_http_v2_module, ngx_http_realip_module and ngx_cache_purge 96 | 97 | && cd /tmp/nginx-$NGINX_VERSION \ 98 | && ./configure --prefix=/usr/share/nginx \ 99 | --with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat \ 100 | -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2' \ 101 | --with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now' \ 102 | --conf-path=/etc/nginx/nginx.conf \ 103 | --http-log-path=/var/log/nginx/access.log \ 104 | --error-log-path=/var/log/nginx/error.log \ 105 | --lock-path=/var/lock/nginx.lock \ 106 | --pid-path=/run/nginx.pid \ 107 | --http-client-body-temp-path=/var/lib/nginx/body \ 108 | --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \ 109 | --http-proxy-temp-path=/var/lib/nginx/proxy \ 110 | # --with-debug \ 111 | --with-pcre-jit \ 112 | --with-ipv6 \ 113 | --with-http_ssl_module \ 114 | # --with-http_stub_status_module \ 115 | --with-http_realip_module \ 116 | --with-http_auth_request_module \ 117 | # --with-http_addition_module \ 118 | --with-http_geoip_module \ 119 | --with-http_gunzip_module \ 120 | --with-http_gzip_static_module \ 121 | --with-http_v2_module \ 122 | # --with-http_sub_module \ 123 | --with-stream \ 124 | --with-stream_ssl_module \ 125 | --with-threads \ 126 | --add-module=/tmp/ngx_cache_purge-2.3 \ 127 | && make && make install \ 128 | && ln -fs /usr/share/nginx/sbin/nginx /usr/sbin/nginx \ 129 | && rm -r /tmp/nginx-$NGINX_VERSION \ 130 | && rm -r /tmp/ngx_cache_purge-2.3 \ 131 | && adduser --system --no-create-home --shell /bin/false --group --disabled-login www-front \ 132 | && openssl dhparam -out /etc/nginx/dhparam.pem 2048 \ 133 | 134 | # Removing devel dependencies 135 | && dpkg --remove build-essential zlib1g-dev libpcre3-dev libssl-dev libgeoip-dev \ 136 | 137 | # Install LE's ACME client for domain validation and certificate generation and renewal 138 | 139 | && echo "deb http://ftp.debian.org/debian jessie-backports main" | tee /etc/apt/sources.list.d/php.list \ 140 | && apt-get update && apt-get -t jessie-backports install -y certbot \ 141 | && mkdir -p /tmp/le \ 142 | && rm -rf /var/lib/apt/lists/* \ 143 | 144 | 145 | 146 | # grab gosu for easy step-down from root 147 | && GPG_KEYS=B42F6819007F00F88E364FD4036A9C25BF357DD4 \ 148 | && curl -o /usr/local/bin/gosu -fsSL "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ 149 | && curl -o /usr/local/bin/gosu.asc -fsSL "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ 150 | && export GNUPGHOME="$(mktemp -d)" \ 151 | && found=''; \ 152 | for server in \ 153 | ha.pool.sks-keyservers.net \ 154 | hkp://keyserver.ubuntu.com:80 \ 155 | hkp://p80.pool.sks-keyservers.net:80 \ 156 | pgp.mit.edu \ 157 | ; do \ 158 | echo "Fetching GPG key $GPG_KEYS from $server"; \ 159 | gpg --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$GPG_KEYS" && found=yes && break; \ 160 | done; \ 161 | test -z "$found" && echo >&2 "error: failed to fetch GPG key $GPG_KEYS" && exit 1; \ 162 | gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ 163 | && rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc \ 164 | && chmod +x /usr/local/bin/gosu \ 165 | && gosu nobody true 166 | 167 | # copy nginx config 168 | COPY nginx-configs/* /etc/nginx/ 169 | COPY nginx-configs/sites-available/nginx-site.conf /etc/nginx/sites-available/default 170 | 171 | # copy configs (supervisord, ssh config, wordpress config sample and scheduling configs) 172 | COPY misc-configs/* schedulers-configs/* /etc/ 173 | 174 | # copy bootstrapping scripts 175 | COPY scripts/* / 176 | 177 | # copy web site content into the container image 178 | COPY website/wordpress /usr/share/nginx/wordpress/ 179 | COPY website/*.* /usr/share/nginx/ 180 | 181 | 182 | # php-fpm config: Opcode cache config 183 | RUN sed -i -e"s/^;opcache.enable=0/opcache.enable=1/" /etc/php/$PHP_VERSION/fpm/php.ini \ 184 | && sed -i -e"s/^;opcache.max_accelerated_files=2000/opcache.max_accelerated_files=4000/" /etc/php/$PHP_VERSION/fpm/php.ini \ 185 | 186 | # php-fpm config 187 | && sed -i -e "s/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g" /etc/php/$PHP_VERSION/fpm/php.ini \ 188 | && sed -i -e "s/expose_php = On/expose_php = Off/g" /etc/php/$PHP_VERSION/fpm/php.ini \ 189 | && sed -i -e "s/upload_max_filesize\s*=\s*2M/upload_max_filesize = 100M/g" /etc/php/$PHP_VERSION/fpm/php.ini \ 190 | && sed -i -e "s/;session.cookie_secure\s*=\s*/session.cookie_secure = True/g" /etc/php/$PHP_VERSION/fpm/php.ini \ 191 | && sed -i -e "s/session.cookie_httponly\s*=\s*/session.cookie_httponly = True/g" /etc/php/$PHP_VERSION/fpm/php.ini \ 192 | && sed -i -e "s/post_max_size\s*=\s*8M/post_max_size = 100M/g" /etc/php/$PHP_VERSION/fpm/php.ini \ 193 | && sed -i -e "s/;daemonize\s*=\s*yes/daemonize = no/g" /etc/php/$PHP_VERSION/fpm/php-fpm.conf \ 194 | && sed -i -e "s/;catch_workers_output\s*=\s*yes/catch_workers_output = yes/g" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf \ 195 | && sed -i -e "s/listen\s*=\s*\/run\/php\/php$PHP_VERSION-fpm.sock/listen = 127.0.0.1:9000/g" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf \ 196 | && sed -i -e "s/;listen.allowed_clients\s*=\s*127.0.0.1/listen.allowed_clients = 127.0.0.1/g" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf \ 197 | && sed -i -e "s/;access.log\s*=\s*log\/\$pool.access.log/access.log = \/var\/log\/\$pool.access.log/g" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf \ 198 | 199 | # create the pid and sock file for php-fpm 200 | && mkdir -p /var/run/php && chown www-data:www-data /var/run/php \ 201 | && touch /run/php/php$PHP_VERSION-fpm.pid && chown www-data:www-data /run/php/php$PHP_VERSION-fpm.pid \ 202 | && touch /var/log/php$PHP_VERSION-fpm.log && chown www-data:www-data /var/log/php$PHP_VERSION-fpm.log \ 203 | 204 | # configuring nginx 205 | && mkdir -p /var/log/nginx \ 206 | && chown -R www-front:www-front /var/log/nginx \ 207 | && touch /var/log/nginx/error.log && chown www-front:www-front /var/log/nginx/error.log \ 208 | && touch /var/log/nginx/access.log && chown www-front:www-front /var/log/nginx/access.log \ 209 | 210 | # configuring supervisor 211 | && mv /etc/supervisord.conf /etc/supervisor/supervisord.conf \ 212 | && /usr/bin/easy_install supervisor-stdout \ 213 | && mkdir -p /var/log/supervisor \ 214 | && mkdir -p /var/run/supervisor \ 215 | && chmod 700 /etc/supervisor/supervisord.conf \ 216 | 217 | # ssh config for git authentication 218 | && mkdir -p /root/.ssh \ 219 | && mv /etc/ssh_config /root/.ssh/config \ 220 | && chmod 700 /root/.ssh/config \ 221 | 222 | # Setting up cronjobs 223 | && crontab /etc/wordpress.cron \ 224 | && touch /var/log/certs.log \ 225 | && touch /var/log/db-backup.log \ 226 | && touch /var/log/wp-cron.log \ 227 | 228 | # apt upgrade configuration 229 | && mv /etc/02periodic /etc/apt/apt.conf.d/02periodic \ 230 | && mv /etc/50unattended-upgrades /etc/apt/apt.conf.d/50unattended-upgrades \ 231 | && mv /etc/apt-preferences /etc/apt/preferences.d/my_preferences \ 232 | 233 | # fail2ban configuration 234 | && touch /var/log/auth.log \ 235 | && cp /etc/wordpress.conf /etc/fail2ban/jail.d/wordpress.conf \ 236 | && cp /etc/nginx-forbidden.conf /etc/fail2ban/filter.d/nginx-forbidden.conf \ 237 | && cp /etc/nginx-noproxy.conf /etc/fail2ban/filter.d/nginx-noproxy.conf \ 238 | && mkdir -p /var/run/fail2ban \ 239 | 240 | 241 | # tightening up permissions on bootstrapping scripts 242 | && chmod 700 /bootstrap_container \ 243 | && chmod 700 /install_wordpress \ 244 | && chmod 700 /setup_web_cert 245 | 246 | 247 | # Build-time metadata as defined at http://label-schema.org 248 | ARG BUILD_DATE 249 | ARG VCS_REF 250 | ARG VERSION 251 | LABEL org.label-schema.build-date=$BUILD_DATE \ 252 | org.label-schema.name="Wordpress (Nginx/php-fpm) Docker Container" \ 253 | org.label-schema.description="Wordpress container running PHP $PHP_VERSION served by Nginx/php-fpm with caching, TLS encryption, HTTP/2" \ 254 | org.label-schema.url="https://github.com/rija/docker-nginx-fpm-caches-wordpress" \ 255 | org.label-schema.vcs-ref=$VCS_REF \ 256 | org.label-schema.vcs-url="https://github.com/rija/docker-nginx-fpm-caches-wordpress" \ 257 | org.label-schema.vendor="Rija Menage" \ 258 | org.label-schema.version=$VERSION \ 259 | org.label-schema.schema-version="1.0" 260 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Rija Ménagé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -- 24 | 25 | Some portions of the start.sh, supervisord.conf and Dockerfile files were copied as is from Eugene Ware's project: 26 | 27 | https://github.com/eugeneware/docker-wordpress-nginx/blob/master/LICENSE 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-nginx-fpm-caches-wordpress 2 | 3 | [![](https://images.microbadger.com/badges/image/rija/docker-nginx-fpm-caches-wordpress.svg)](https://microbadger.com/images/rija/docker-nginx-fpm-caches-wordpress "Get your own image badge on microbadger.com") 4 | [![Build Status](https://img.shields.io/badge/docker%20hub-automated%20build-ff69b4.svg)](https://hub.docker.com/r/rija/docker-nginx-fpm-caches-wordpress/) 5 | [![Build Status](https://travis-ci.org/rija/docker-nginx-fpm-caches-wordpress.svg?branch=master)](https://travis-ci.org/rija/docker-nginx-fpm-caches-wordpress) 6 | 7 | 8 | ### Maintainer 9 | 10 | Rija Ménagé 11 | 12 | ### Description 13 | 14 | Dockerfile to create a container with Nginx and php-fpm running a Wordpress web application. 15 | TLS encryption is provided (and automatically renewed) using free certificates provided by Let's Encrypt. 16 | Page caching (using Nginx's FastCGI cache) and Opcode caching with Zend Opcache are enabled and configured. 17 | 18 | The container can be deployed with either a vanilla installation of Wordpress or an existing Wordpress-based codebase. 19 | 20 | The container doesn't have a database server, but the supplied docker compose file allow instantiating a MariaDB 10.2 database server on the same network as the Wordpress container. 21 | 22 | When choosing to use an existing Wordpress-based web site, the codebase is baked into the image when the image is built, so that the deployed container is immutable, making it play nicely in a versioned deployment pipelines. 23 | 24 | When choosing to use a vanilla installation of Wordpress, the latest wordpress software is baked at build time, and additional security and caching related plugins are installed during deployment of the container. 25 | 26 | 27 | **Headline features:** 28 | * Nginx 1.13.0 29 | * HTTP/2 and TLS encryption configured 30 | * TLS configured using Mozilla Server-side TLS Intermediate profile + TLSv1.3 31 | * PHP 7.1 installed with CLI, PHP-FPM and bare essential extensions 32 | * FastCGI Caching+Cache Purge and Zend Opcode enabled 33 | * RealIP Nginx module installed for when running behind a reverse-proxy 34 | * Latest version of Wordpress is installed at container startup 35 | * Can clone a Wordpress-based site from GIT repositories 36 | * WP-CLI to manage a Wordpress install from command line 37 | * OS-level security updates performed automatically 38 | * TLS certificate automatically renewed 39 | * Daily backup of database to volume sharable with Docker host 40 | * Supervisord 3.0 as init script to manage processes' life-cycle 41 | * Small-footprint Docker image using Bitnami/minideb as BASE image 42 | 43 | 44 | 45 | *Available Docker Hub tags:* **v1, v2, latest** 46 | 47 | ### How to run 48 | 49 | 50 | #### with docker run: 51 | 52 | ```bash 53 | 54 | $ docker run --name a-mariadb-server \ 55 | -e MYSQL_ROOT_PASSWORD=my-secret-pw \ 56 | -e MYSQL_USER=wp_user \ 57 | -e MYSQL_PASSWORD=wp_password \ 58 | -e MYSQL_DATABASE=wp_database \ 59 | -d mariadb:10.2 60 | 61 | $ docker run -d \ 62 | --link a-mariadb-server:dbserver \ 63 | --name a-wordpress-container \ 64 | -e SERVER_NAME=example.com \ 65 | -e ADMIN_EMAIL=helloworld@example.com \ 66 | -e ADMIN_PASSWORD=changemenow \ 67 | -e DB_HOSTNAME=dbserver \ 68 | -e DB_USER=wp_user \ 69 | -e DB_PASSWORD=wp_password \ 70 | -e DB_DATABASE=wp_database \ 71 | -v /etc/letsencrypt:/etc/letsencrypt \ 72 | -v /${HOME}:/root/sql \ 73 | -p 443:443 -p 80:80 \ 74 | rija/docker-nginx-fpm-caches-wordpress 75 | 76 | ``` 77 | 78 | 79 | **Notes:** 80 | The ``ADMIN_EMAIL`` variable is used by WP-CLI for the initial setup of the Wordpress install and by Let's Encrypt's Certbot for managing TLS certificates renewal. It is also supplied alongside ``ADMIN_PASSWORD`` to the Wordpress install associated with the admin user. 81 | 82 | #### with Docker compose: 83 | 84 | ```bash 85 | $ cd docker-nginx-fpm-caches-wordpress 86 | $ ./make_env 87 | $ docker-compose up -d 88 | ``` 89 | 90 | One can adjust the values in the **.env** file updated (and created if non-existent) by ``./make_env`` 91 | 92 | #### with Ansible playbook: 93 | 94 | ###### - New image based on vanilla Wordpress pushed to a private registry 95 | 96 | ```bash 97 | $ ansible-playbook --extra-vars="registry_url=registry.gitlab.com registry_user=foobar force_build=yes download_wp=yes" ansible/press-site.yml 98 | ``` 99 | 100 | One can adjust the values in the **.env** file updated (and created if non-existent) by ``./make_env`` 101 | 102 | 103 | ###### - Deploy the previously baked image to a Digital Ocean droplet 104 | 105 | ```bash 106 | $ ansible-playbook -i digital_ocean.py --extra-vars="registry_url=registry.gitlab.com registry_user=foobar docker_host_user=docker" ansible/deploy-site.yml 107 | ``` 108 | 109 | where digital_ocean.py is downloaded from https://github.com/ansible/ansible/blob/devel/contrib/inventory/digital_ocean.py 110 | 111 | ```bash 112 | $ curl -O https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/digital_ocean.py 113 | $ chmod u+x digital_ocean.py 114 | ``` 115 | 116 | if you don't deploy on Digital Ocean, you can find the relevant dynamic inventory for your cloud service on https://github.com/ansible/ansible/tree/devel/contrib/inventory 117 | 118 | 119 | ###### - Workflow for an existing web site 120 | 121 | make sure you have the web site in a directory called ``wordpress`` inside the ``website`` directory. Then ensure the database dump to be imported is there as well under the name ``wordpress.sql``: 122 | 123 | ``` 124 | website/ 125 | ├── README.md 126 | ├── wordpress 127 | ├── VERSION 128 | └── wordpress.sql 129 | ``` 130 | 131 | Then ensure you have an ``.env`` file with [appropriate variables](docs/env-sample): 132 | 133 | ```bash 134 | $ ./make_env 135 | 136 | ``` 137 | 138 | The script above will also keep the build specific variables up-to-date. 139 | 140 | Now, you can bake an image and upload it to a registry 141 | 142 | ```bash 143 | $ ansible-playbook --extra-vars="registry_url=registry.gitlab.com registry_user=foobar force_build=yes" ansible/press-site.yml 144 | ``` 145 | 146 | 147 | to deploy, use: 148 | 149 | ```bash 150 | $ ansible-playbook -vvv -i digital_ocean.py --extra-vars="registry_url=registry.gitlab.com registry_user=foobar docker_host_user=someuser update_image=yes" ansible/deploy-site.yml 151 | ``` 152 | 153 | *Note:* 154 | There is a known issue, such that sometimes the install script will fails to load the database dump and the docker logs will show the following error: 155 | ``` 156 | install_wordpress stdout | this is an existing Wordpress web site, loading the database dump if not loaded already ... 157 | install_wordpress stderr | ERROR install_wordpress stderr | 2003 (HY000) install_wordpress stderr | : Can't connect to MySQL server on 'dbs' (111) 158 | 2018-04-27 16:18:14,558 INFO exited: install_wordpress (exit status 1; not expected) 159 | ``` 160 | 161 | When that happens, simply re-run the installation script ``/install_wordpress`` and the database will be loaded correctly. 162 | There is an issue raised for this: [#11](https://github.com/rija/docker-nginx-fpm-caches-wordpress/issues/11) 163 | 164 | ### How to enable Encryption (TLS) 165 | 166 | **This step is not necessary if you used the ansible playbook above.** 167 | 168 | It is advised to have read Lets Encrypt's [FAQ](https://community.letsencrypt.org/c/docs/) and [user guide](https://letsencrypt.readthedocs.org/en/latest/index.html) beforehand. 169 | 170 | after the Wordpress container has been started, run the following command on the host and follow the on-screen instructions: 171 | 172 | ```bash 173 | $ docker exec -it a-wordpress-container bash -c "/setup_web_cert" 174 | ``` 175 | 176 | After the command as returned with a successful message regarding acquisition of certificate, nginx will be reloaded with encryption enabled and configured. 177 | 178 | **Notes:** 179 | * There is no change needed to nginx configuration for standard use cases 180 | * Navigating to the web site will throw a connection error until that step has been performed as encryption is enabled across the board and http connections are redirected to https. You should update nginx configuration files as needed to match your use case if that behaviour is not desirable. 181 | * Lets Encrypt's' Certbot client configuration file is deployed to ``/etc/letsencrypt/cli.ini``. Review and amend that file according to needs. 182 | * the generated certificate is valid for domain.tld and www.domain.tld (SAN) 183 | * **The certificate files are accessible on the Docker host server** in ``/etc/letsencrypt`` 184 | 185 | 186 | ### How to build 187 | 188 | First, make sure there is a Wordpress codebase under the ``website/`` directory. 189 | Check the [website/README.md](website/README.md) for more details. 190 | 191 | ```bash 192 | $ cd docker-nginx-fpm-caches-wordpress 193 | $ ./make_env && docker-compose up --build -d 194 | ``` 195 | 196 | One should adjust the values in the **.env** file updated (and created if non-existent) by **./make_env** 197 | make_env should be executed at every build so that the dynamic docker labels for build date and vcs ref are populated accurately. 198 | 199 | ### How to login to Wordpress Dashboard 200 | 201 | The user is ``admin`` and the initial password can be supplied as ``ADMIN_PASSWORD`` in the **.env** file generated by **./make_env** 202 | 203 | 204 | ### License 205 | 206 | MIT (see the [LICENSE](https://github.com/rija/docker-nginx-fpm-caches-wordpress/blob/master/LICENSE) file) 207 | 208 | ### Credits 209 | 210 | * Eugene Ware for the original work on a [Nginx/Wordpress Dockerfile](https://github.com/eugeneware/docker-wordpress-nginx), whose ideas I've extended upon in this project 211 | * [@renchap](https://community.letsencrypt.org/t/howto-easy-cert-generation-and-renewal-with-nginx/3491/5) and [@DrPain](https://community.letsencrypt.org/t/nginx-installation/3502/5) from [Let's Encrypt Community](https://community.letsencrypt.org/), whose ideas put me on the path of a working and elegant solution for Nginx/LetsEncrypt integration 212 | * [Bjørn Johansen](https://bjornjohansen.no) for his blog articles on hardening a Wordpress installation that informed some of the choices I made 213 | * Rahul Bansal of [EasyEngine](https://easyengine.io/wordpress-nginx/tutorials/) for his tutorials on Nginx/Wordpress integration that informed some of the choices I made 214 | * All the contributors to the [Wordpress.org's Nginx support](http://codex.wordpress.org/Nginx) page 215 | * Mozilla for their awesome [SSL configuration generator](https://mozilla.github.io/server-side-tls/ssl-config-generator/) 216 | * All the the other people whose blog articles I've directly added in the comments in the relevant artefacts of this project 217 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v2 2 | -------------------------------------------------------------------------------- /ansible/deploy-site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Building the env file locally 4 | hosts: localhost 5 | 6 | tasks: 7 | - name: Gather context 8 | command: cd ../website/ && ./make_env 9 | 10 | 11 | - name: Deploy a Wordpress web site to remote host 12 | hosts: all 13 | vars: 14 | ansible_user: "{{docker_host_user}}" 15 | update_image: no 16 | reload: smart 17 | sqldir: "${HOME}/sql" 18 | no_db_import: no 19 | 20 | vars_prompt: 21 | - name: "registry_password" 22 | prompt: "please, enter registry password" 23 | private: yes 24 | 25 | tasks: 26 | 27 | - name: login to the remote registry 28 | docker_login: 29 | registry: "{{registry_url}}" 30 | username: "{{registry_user}}" 31 | password: "{{registry_password}}" 32 | reauthorize: yes 33 | 34 | - name: create the sql directory for database import and backup 35 | file: 36 | path: "{{sqldir}}" 37 | state: directory 38 | mode: 0755 39 | 40 | - name: optionally set up flag to prevent database import 41 | command: touch "{{sqldir}}/no_db_import" 42 | when: no_db_import == "yes" 43 | 44 | - name: deploy web site 45 | docker_service: 46 | project_name: "{{ lookup('ini', 'COMPOSE_PROJECT_NAME type=properties file=../website/.env') }}" 47 | pull: "{{update_image}}" 48 | recreate: "{{reload}}" 49 | definition: 50 | version: '2' 51 | services: 52 | webapp: 53 | restart: 'unless-stopped' 54 | cap_add: 55 | - NET_ADMIN 56 | networks: 57 | - wp_net 58 | image: "{{registry_url}}/{{ lookup('ini', 'IMAGE_NAME type=properties file=../website/.env') }}:{{ lookup('ini', 'VERSION type=properties file=../website/.env') }}" 59 | volumes: 60 | - /etc/letsencrypt:/etc/letsencrypt 61 | - "{{sqldir}}:/root/sql" 62 | ports: 63 | - 443:443 64 | - 80:80 65 | depends_on: 66 | - db 67 | environment: 68 | SERVER_NAME: "{{ lookup('ini', 'SERVER_NAME type=properties file=../website/.env') }}" 69 | ADMIN_EMAIL: "{{ lookup('ini', 'ADMIN_EMAIL type=properties file=../website/.env') }}" 70 | ADMIN_PASSWORD: "{{ lookup('ini', 'ADMIN_PASSWORD type=properties file=../website/.env') }}" 71 | DB_HOSTNAME: dbs 72 | DB_USER: "{{ lookup('ini', 'WP_DB_USER type=properties file=../website/.env') }}" 73 | DB_PASSWORD: "{{ lookup('ini', 'WP_DB_PASSWORD type=properties file=../website/.env') }}" 74 | DB_DATABASE: "{{ lookup('ini', 'WP_DB_NAME type=properties file=../website/.env') }}" 75 | links: 76 | - db:dbs 77 | db: 78 | restart: 'unless-stopped' 79 | networks: 80 | - wp_net 81 | image: mariadb:10.2 82 | volumes: 83 | - mariadb_data:/var/lib/mysql 84 | environment: 85 | MYSQL_RANDOM_ROOT_PASSWORD: 1 86 | MYSQL_USER: "{{ lookup('ini', 'WP_DB_USER type=properties file=../website/.env') }}" 87 | MYSQL_PASSWORD: "{{ lookup('ini', 'WP_DB_PASSWORD type=properties file=../website/.env') }}" 88 | MYSQL_DATABASE: "{{ lookup('ini', 'WP_DB_NAME type=properties file=../website/.env') }}" 89 | 90 | networks: 91 | wp_net: 92 | driver: bridge 93 | 94 | volumes: 95 | mariadb_data: 96 | 97 | register: output 98 | 99 | - debug: 100 | var: output 101 | 102 | - assert: 103 | that: 104 | - "webapp.{{ lookup('ini', 'COMPOSE_PROJECT_NAME type=properties file=../website/.env') }}_webapp_1.state.running" 105 | - "db.{{ lookup('ini', 'COMPOSE_PROJECT_NAME type=properties file=../website/.env') }}_db_1.state.running" 106 | 107 | - name: set up SSL certificate 108 | command: docker exec -i "{{ lookup('ini', 'COMPOSE_PROJECT_NAME type=properties file=../website/.env') }}_webapp_1" bash -l -c "/setup_web_cert" 109 | -------------------------------------------------------------------------------- /ansible/press-site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: build a new image for the Wordpress web site 4 | hosts: localhost 5 | 6 | vars: 7 | force_build: no 8 | download_wp: no 9 | push_image: yes 10 | image_name: "{{ lookup('ini', 'IMAGE_NAME type=properties file=../website/.env') }}" 11 | image_version: "{{ lookup('ini', 'VERSION type=properties file=../website/.env') }}" 12 | 13 | vars_prompt: 14 | - name: "registry_password" 15 | prompt: "please, enter registry password" 16 | private: yes 17 | 18 | tasks: 19 | 20 | - name: optionally download vanilla wordpress from source control 21 | git: 22 | repo: "https://github.com/WordPress/WordPress.git" 23 | dest: ../website/wordpress 24 | when: download_wp == "yes" 25 | 26 | - name: Gather context 27 | command: ./make_env 28 | args: 29 | chdir: ../website/ 30 | 31 | - debug: 32 | msg: "image version: {{image_version}}" 33 | 34 | 35 | - name: login to the remote registry 36 | docker_login: 37 | registry: "{{registry_url}}" 38 | username: "{{registry_user}}" 39 | password: "{{registry_password}}" 40 | reauthorize: yes 41 | 42 | 43 | - name: Build, Tag and push to the registry 44 | docker_image: 45 | state: build 46 | name: "{{registry_url}}/{{ image_name }}" 47 | repository: "{{registry_url}}/{{ image_name }}" 48 | tag: "{{ image_version }}" 49 | path: .. 50 | pull: yes 51 | push: "{{push_image}}" 52 | rm: yes 53 | force: "{{force_build}}" 54 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | webapp: 4 | env_file: website/.env 5 | restart: 'no' 6 | build: 7 | context: . 8 | args: 9 | PHP_MEMLIMIT: ${PHP_MEMLIMIT} 10 | VCS_REF: ${VCS_REF} 11 | BUILD_DATE: ${BUILD_DATE} 12 | VERSION: ${VERSION} 13 | environment: 14 | DEBUG: 'true' 15 | image: ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} 16 | volumes: 17 | - /etc/letsencrypt:/etc/letsencrypt 18 | - /var/run/docker.sock:/var/run/docker.sock:ro 19 | - /${HOME}/Downloads:/root/sql 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | webapp: 4 | env_file: website/.env 5 | restart: 'unless-stopped' 6 | cap_add: 7 | - NET_ADMIN 8 | networks: 9 | - wp_net 10 | image: ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} 11 | volumes: 12 | - /etc/letsencrypt:/etc/letsencrypt 13 | - /tmp:/root/sql 14 | ports: 15 | - 443:443 16 | - 80:80 17 | depends_on: 18 | - db 19 | environment: 20 | SERVER_NAME: 21 | ADMIN_EMAIL: 22 | ADMIN_PASSWORD: ${ADMIN_PASSWORD} 23 | DB_HOSTNAME: dbs 24 | DB_USER: ${WP_DB_USER} 25 | DB_PASSWORD: ${WP_DB_PASSWORD} 26 | DB_DATABASE: ${WP_DB_NAME} 27 | links: 28 | - db:dbs 29 | db: 30 | env_file: website/.env 31 | restart: 'unless-stopped' 32 | networks: 33 | - wp_net 34 | image: mariadb:10.2 35 | volumes: 36 | - mariadb_data:/var/lib/mysql 37 | environment: 38 | MYSQL_ROOT_PASSWORD: 39 | MYSQL_USER: ${WP_DB_USER} 40 | MYSQL_PASSWORD: ${WP_DB_PASSWORD} 41 | MYSQL_DATABASE: ${WP_DB_NAME} 42 | 43 | networks: 44 | wp_net: 45 | driver: bridge 46 | 47 | volumes: 48 | mariadb_data: 49 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | 3 | 4 | ## v2 5 | 6 | * Supervisord 3.0 is PID 1 and properly manages all the processes in the container 7 | * Uses PHP 7.1 8 | * TLS encryption with Let's Encrypt and automated certificate renewal, configured using Mozilla intermediary profile for server side TLS 9 | * Use Nginx 1.13.0 with real_ip, HTTP/2 and TLSv1.3 configured 10 | * FastCGI page caching and cache purge compiled in Nginx 11 | * docker-compose is now the preferred way to use this Dockerfile, directly or through Ansible 12 | * The deployment now relies on git for installing vanilla Wordpress or a Wordpress based web site 13 | * Security has been improved on many layers: 14 | * Setup of Fail2ban for black-listing ip addresses of attackers 15 | * Tightening of file permissions and configuration of server processess and bootstrapping scripts 16 | * Security headers in Nginx responses 17 | * Pre-installed WP Plugins for using Fail2Ban, reducing XML-RPC attack surface, and enabling Content Security Policy 18 | * PGP signature verification of downloaded package through APT or CURL 19 | * The Docker image size has been significantly reduced (from 599MB/46layers to 186.1MB/25layers) 20 | * uses WP-CLI for managing Wordpress 21 | 22 | ## v1: Initial Release 23 | 24 | * Initial release of a Dockerfile to create a Docker container running Wordpress on Nginx with Encryption (TLS) enabled. 25 | * Page caching and cache purging with FastCGI is enabled, so is Opcode for object caching. 26 | * Cron is enabled. Processes are managed by Supervisord. 27 | * Php-fpm is the application server listening on a TCP port. 28 | * In this release, versions are 1.8 for Nginx, 5.5.* for php-fpm. 29 | * The base OS for the image is Ubuntu 14.04. 30 | * A TLS certificate can be created and renewed with one bash command using the included Let’s Encrypt ACME client with no need for configuring Nginx for standard use case. 31 | * Standard use case for this Dockerfile is running the latest version of single site Wordpress with one domain name domain.tld, also aliased as www.domain.tld and with TLS encryption enabled for the whole web site. 32 | * No database server is included in the container, this is a feature. 33 | * No mail server is included in the container, this is a feature. 34 | * An automated build for this Dockerfile can be pulled from Docker Hub. 35 | -------------------------------------------------------------------------------- /docs/Cookbook.md: -------------------------------------------------------------------------------- 1 | **Warning:** 2 | This page is currently out of date. The information may still work (and there maybe somme good tips) 3 | but it doesn't reflect either current Docker best practices or the new deployment philosophy of this Dockerfile or both. 4 | It will be revamped at some point in the future. 5 | 6 | ## Usage patterns 7 | 8 | The typical pattern I've adopted for some use cases is using a container each for Wordpress and Mysql and two data volume containers, one for each as well. Both containers run within one bridge network on a single host. 9 | 10 | 11 | #### Creating a bridge network for Wordpress 12 | 13 | this will create a Docker network on a single host that will be used by the Wordpress container and the MySQL container to communicate with eachother 14 | 15 | ```bash 16 | $ docker network create -d bridge my_bnet 17 | ``` 18 | 19 | #### Deploying Mysql in a Docker container: 20 | 21 | ```bash 22 | $ docker create --name mysqldata -v /var/lib/mysql mariadb:latest 23 | $ docker run --restart=unless-stopped -d --name mysqlserver \ 24 | --volumes-from mysqldata \ 25 | --net=my_bnet \ 26 | --env MYSQL_ROOT_PASSWORD= \ 27 | --env MYSQL_DATABASE=wordpress \ 28 | --env MYSQL_USER= \ 29 | --env MYSQL_PASSWORD= mariadb:latest 30 | ``` 31 | 32 | #### Deploying Wordpress in a Docker container: 33 | 34 | ###### Create a data volume container for Wordpress files 35 | 36 | ```bash 37 | $ docker create --name wwwdata -v /usr/share/nginx/www rija/docker-nginx-fpm-caches-wordpress 38 | ``` 39 | 40 | ###### Run a wordpress container 41 | ```bash 42 | docker run --restart=unless-stopped -d \ 43 | --name wordpress \ 44 | --net=my_bnet \ 45 | --env SERVER_NAME=example.com \ 46 | --env DB_HOSTNAME=172.0.8.2 \ 47 | --env DB_USER=wpuser \ 48 | --env DB_PASSWORD=changeme \ 49 | --env DB_DATABASE=wordpress \ 50 | --volumes-from wwwdata \ 51 | -v /etc/letsencrypt:/etc/letsencrypt \ 52 | -p 443:443 -p 80:80 \ 53 | rija/docker-nginx-fpm-caches-wordpress 54 | ``` 55 | 56 | Using data volume container for Wordpress and Mysql makes some operational task incredibly easy (backups, data migrations, cloning, developing with production data,...) 57 | 58 | A quick way of extracting database connection information from the previously instanciated Mysql container is by running the following command: 59 | 60 | ```bash 61 | docker exec -it mysqlserver bash -c "env" | grep -v ROOT | grep -v HOME | grep -v PWD | grep -v SHLVL | grep -v PATH | grep -v _=| sed "s/^/DB_/" 62 | ``` 63 | 64 | However the database server hostname obtained that way is useless as the Wordpress container has now way to resolve that hostname on its own. 65 | 66 | so you can retrieve the IP of the database server with: 67 | 68 | ```bash 69 | $ docker inspect -f '{{range .NetworkSettings.Networks }}{{.IPAddress}}{{end}}' mysqlserver 70 | ``` 71 | 72 | and use that as value for DB_HOSTNAME instead. 73 | 74 | It's a better way, but still not satisfying as the IP address may change over reboots of the host while the instantiated Wordpress container will be hard-wired with that initial IP address. 75 | 76 | A better approach is to ask the Docker host the IP and hostname of the database server container on the same bridge network, then add the hostname/ip pair to /etc/hosts. 77 | To do so, one can pass to 'docker run' the following option: 78 | 79 | ```bash 80 | --add-host=dockerhost:$(ip route | awk '/docker0/ { print $NF }') 81 | ``` 82 | 83 | then from within the container make an HTTP call to the Docker Remote API on the host using **dockerhost** as basis for the endpoint url. 84 | The REST call can be made using the curl (any version) command in a script. The issue of this method is that it requires the Docker daemon to be available on a TCP port which is not the default configuration and it increases the attack surface of the deployment. 85 | 86 | A better way for connecting to the Docker host is to bind mount the docker daemon unix socket (typically **/var/run/docker.sock** ) to the Wordpress container and from within the container, connect to the Docker Remote API and retrieve the current IP address for the mysql server, then add the hostname/ip pair to the /etc/hosts. This is done by the start.sh script upon container startup. (curl > 7.40+ is needed to make a connection to a Unix socket). 87 | 88 | The whole command to instantiate a container using that method becomes: 89 | 90 | 91 | ```bash 92 | docker run --restart=unless-stopped -d \ 93 | --name wordpress \ 94 | --net=my_bnet \ 95 | --env SERVER_NAME=example.com \ 96 | --env DB_HOSTNAME=4abbef615af7 \ 97 | --env DB_USER=wpuser \ 98 | --env DB_PASSWORD=changeme \ 99 | --env DB_DATABASE=wordpress \ 100 | --volumes-from wwwdata \ 101 | -v /etc/letsencrypt:/etc/letsencrypt \ 102 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 103 | -p 443:443 -p 80:80 \ 104 | rija/docker-nginx-fpm-caches-wordpress 105 | ``` 106 | 107 | or if one wants less typing: 108 | 109 | 110 | ```bash 111 | $ docker exec -it mysqlserver bash -c "env" | grep -v ROOT | grep -v HOME | grep -v PWD | grep -v SHLVL | grep -v PATH | grep -v _=| sed "s/^/DB_/" > dsn.txt 112 | 113 | $ docker run --restart=unless-stopped -d \ 114 | --name wordpress \ 115 | --net=my_bnet \ 116 | --env SERVER_NAME=example.com \ 117 | --env-file ./dsn.txt \ 118 | --volumes-from wwwdata \ 119 | -v /etc/letsencrypt:/etc/letsencrypt \ 120 | -v /var/run/docker.sock:/var/run/docker.sock:ro \ 121 | -p 443:443 -p 80:80 \ 122 | rija/docker-nginx-fpm-caches-wordpress 123 | 124 | ``` 125 | 126 | **TODO:** this is a good place to add something about Docker compose 127 | 128 | ###### Enabling Encryption (TLS) 129 | 130 | It is advised to have read Lets Encrypt's [FAQ](https://community.letsencrypt.org/c/docs/) and [user guide](https://letsencrypt.readthedocs.org/en/latest/index.html) beforehand. 131 | 132 | after the Wordpress container has been started, run the following command and follow the on-screen instructions: 133 | 134 | ```bash 135 | $ docker exec -it wordpress bash -c "letsencrypt certonly" 136 | ``` 137 | 138 | After the command as returned with a successful message regarding acquisition of certificate, nginx needs to be restarted with encryption enabled and configured. This is done by running the following commands: 139 | 140 | ```bash 141 | $ docker exec -it wordpress bash -c "cp /etc/nginx/ssl.conf /etc/nginx/ssl.example.com.conf" 142 | $ docker exec -it wordpress bash -c "nginx -t && service nginx reload" 143 | ``` 144 | It's the same command to run in order to renew the certificate, to duplicate them or to add sub-domains. The above read the content of *'/etc/letsencrypt/cli.ini'*. Most customisation of the certificate would involve changing that configuration file. 145 | 146 | **Notes:** 147 | * There is no change required to nginx configuration for standard use cases 148 | * It is suggested to replace example.com in the file name by your domain name although any file name that match the pattern ssl.*.conf will be recognised 149 | * Navigating to the web site will throw a connection error until that step has been performed as encryption is enabled across the board and http connections are redirected to https. You must update nginx configuration files as needed to match your use case if that behaviour is not desirable. 150 | * Lets Encrypt's' ACME client configuration file is deployed to *'/etc/letsencrypt/cli.ini'*. Update that file to suit your use case regarding certificates. 151 | * the generated certificate is valid for domain.tld and www.domain.tld (SAN) 152 | * The certificate files are saved on the host server in /etc/letsencrypt and they have a 3 months lifespan before they need to be renewed 153 | 154 | 155 | #### Export a data volume container: 156 | 157 | ```bash 158 | $ docker run --rm --volumes-from wwwdata -v $(pwd):/backup rija/docker-nginx-fpm-caches-wordpress tar -cvz -f /backup/wwwdata.tar.gz /usr/share/nginx/www 159 | 160 | ``` 161 | #### Import into a data volume container: 162 | 163 | ```bash 164 | $ docker run --rm --volumes-from wwwdata2 -v $(pwd):/new-data rija/docker-nginx-fpm-caches-wordpress bash -c 'cd / && tar xzvf /new-data/wwwdata.tar.gz' 165 | ``` 166 | 167 | to verify the content: 168 | 169 | ```bash 170 | $ docker run --rm --volumes-from wwwdata2 -v $(pwd):/new-data -it rija/docker-nginx-fpm-caches-wordpress bash 171 | ``` 172 | 173 | 174 | #### Import a sql database dump in Mysql running in a Docker container 175 | 176 | ```bash 177 | 178 | $ 179 | $ docker run --rm --link mysqlserver:db -v $(pwd):/dbbackup -it rija/docker-nginx-fpm-caches-wordpress bash 180 | 181 | /# cd /dbbackup/ 182 | /# zcat | mysql -h $DB_HOSTNAME -u $DB_USER -p$DB_PASSWORD $DB_DATABASE 183 | /# exit 184 | ``` 185 | 186 | to verify the content: 187 | 188 | ```bash 189 | $ docker run --rm --link mysqlserver:db -it rija/docker-nginx-fpm-caches-wordpress bash 190 | $ mysql -h $DB_HOSTNAME -u $DB_USER -p$DB_PASSWORD $DB_DATABASE 191 | ``` 192 | 193 | #### Logs 194 | 195 | The logs are exposed outside the container as a volume. 196 | So you can deploy your own services to process, analyse or aggregate the logs from the Wordpress installation. 197 | 198 | The corresponding line in the Dockerfile is: 199 | 200 | ``` 201 | VOLUME ["/var/log"] 202 | ``` 203 | 204 | you can mount this volume on another container with a command as shown below (assuming your Wordpress container is called 'wordpress'): 205 | 206 | ```bash 207 | docker run --name MyLogAnalyser --volumes-from wordpress -d MyLogAnalyserImage 208 | ``` 209 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## SECURITY: 4 | 5 | * ~~deroot cron~~ 6 | * ~~deroot supervisord~~ _see http://stackoverflow.com/questions/13905861/supervisor-as-non-root-user_ 7 | * ~~replace cron by an supervisord event listener listening on TICK_* events (see http://stackoverflow.com/questions/27341846/using-supervisor-as-cron)~~ 8 | * reverse start.sh/supervisord relationship so that supervisord is the init process and start.sh is under its control **[DONE]** _(Edit: start.sh is now bootstrap_container)_ 9 | * create an apparmor profile for the project 10 | * add a wordpress user on the container's host 11 | * strict file ownership (uploads is only dir writable, all files owned by wordpress user and restricted) **[IN PROGRESS]** 12 | * obfuscate Wordpress URL structure and headers 13 | * disable Wordpress UI for theme update **[DONE]** 14 | * disable Wordpress UI for plugin update **[DONE]** 15 | * disable Wordpress UI for template code editing **[DONE]** 16 | * verify PGP signature of the downloaded nginx source code **[DONE]** 17 | * add support for Fail2ban **[DONE]** 18 | * find a secure, easily configurable way to allow access to xmlrpc.php for staff who can't be tied to one specific IP 19 | * move cronjobs into their own container 20 | 21 | ## ARCHITECTURE: 22 | 23 | * ~~create a Dockerfile for a nginx proxy with fair balancer, streaming, page caching and TLS termination~~ 24 | * ~~include a Docker compose file for instantiating an nginx proxy, a web server and database server on a new network~~ 25 | * reduce size of the image of this project **[DONE]** _(Edit: used bitnamit/minideb)_ 26 | * reduce size of the image of the dependent projects: mariadb 27 | * container independence for Wordpress static files, S3 or shared file system? 28 | * Replace Supervisord with Mozilla Circus 29 | * tweak mysql config to optimise for low memory 30 | * Spread out system's services onto mulitple containers 31 | 32 | ## OPERATIONS: 33 | 34 | * make Ansible playbooks for setting up container host **[DONE]** 35 | * make Ansible playbooks for setting up container **[DONE]** 36 | * automated daily backup of the database **[DONE]** 37 | * add restart policies in docker compose file **[DONE]** 38 | * upgrade docker compose syntax to version 3 39 | 40 | ## ANALYTICS: 41 | 42 | * send all logs to syslog 43 | * and send them to splunk on the cloud 44 | * put google analytic agent away from the application (maybe in nginx using ngx_pagespeed) 45 | * integrate with NewRelics for system monitoring 46 | 47 | ## WORDPRESS & DEVELOPMENT: 48 | * enable wp-cron **[DONE]** 49 | * add wp-cli to the container **[DONE]** 50 | * ~~if installing a custom Wordpress project, allow loading of in-repository database dump~~ 51 | * if installing a custom Wordpress project, allow loading of database dump from host **[DONE]** 52 | * Add support for private git servers **[DONE]** 53 | * use Composer for dependency management 54 | 55 | ## MISC: 56 | 57 | * Docker Hub images automated build that tracks Github tags (v#) **[DONE]** 58 | * upgrade to PHP7 **[DONE]** (Edit: upgraded to 7.1) 59 | * add support for APCu in-memory, in-process key/value userland object caching 60 | * add GUI for opcode status 61 | * enable /status and /ping for php-fpm 62 | * look into Kubernetes on GCE 63 | * install ACME client from OS package **[DONE]** 64 | * review packages installed in Dockerfile, drop the unnecessary ones **[DONE]** 65 | * add support for HTTP/2 **[DONE]** 66 | * customise download endpoint for Wordpress package. **[DONE]** _(Edit: flexibility based on GIT)_ 67 | * ~~checksum verification of Wordpress package when downloading specific version~~ 68 | * Refactor the COOKBOOK 69 | -------------------------------------------------------------------------------- /docs/env-sample: -------------------------------------------------------------------------------- 1 | SERVER_NAME=example.com 2 | COMPOSE_PROJECT_NAME=wordpress 3 | #The email will be used for registering to Lets Encrypt for the TLS certificate 4 | ADMIN_EMAIL=foo@bar.com 5 | ADMIN_PASSWORD=changemenow 6 | WP_DB_USER=wordpress 7 | WP_DB_PASSWORD=wordpress 8 | WP_DB_NAME=wordpress 9 | MYSQL_ROOT_PASSWORD=mysqlrootpassword 10 | IMAGE_NAME=user/project/image 11 | REGISTRY_URL=registry.gitlab.com 12 | BUILD_DATE=2017-09-18T20:08:11Z 13 | VCS_REF=11fd881 14 | VERSION=v3 15 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # $IMAGE_NAME var is injected into the build so the tag is correct. 3 | git clone https://github.com/WordPress/WordPress.git website/wordpress 4 | docker build --build-arg VCS_REF=`git rev-parse --short HEAD` \ 5 | --build-arg BUILD_DATE=`date -u +”%Y-%m-%dT%H:%M:%SZ”` \ 6 | --build-arg VERSION=`cat VERSION` \ 7 | -t $IMAGE_NAME . 8 | -------------------------------------------------------------------------------- /misc-configs/apt-preferences: -------------------------------------------------------------------------------- 1 | Package: * 2 | Pin: release a=stable 3 | Pin-Priority: 700 4 | 5 | Package: * 6 | Pin: release a=jessie-backports 7 | Pin-Priority: 650 8 | 9 | Package: * 10 | Pin: release a=unstable 11 | Pin-Priority: 600 12 | -------------------------------------------------------------------------------- /misc-configs/nginx-forbidden.conf: -------------------------------------------------------------------------------- 1 | [Definition] 2 | failregex = ^ \[error\] \d+#\d+: .* forbidden .*, client: , .*$ 3 | 4 | ignoreregex = 5 | -------------------------------------------------------------------------------- /misc-configs/nginx-noproxy.conf: -------------------------------------------------------------------------------- 1 | # Proxy filter /etc/fail2ban/filter.d/nginx-proxy.conf: 2 | # 3 | # Block IPs trying to use server as proxy. 4 | # 5 | # Matches e.g. 6 | # 192.168.1.1 - - "GET http://www.something.com/ 7 | # 8 | [Definition] 9 | failregex = ^ -.*(GET|HEAD) http.* 10 | ignoreregex = 11 | -------------------------------------------------------------------------------- /misc-configs/ssh_config: -------------------------------------------------------------------------------- 1 | host github.com 2 | HostName github.com 3 | IdentityFile ~/.ssh/key_to_use 4 | User git 5 | 6 | host gitlab.com 7 | HostName gitlab.com 8 | IdentityFile ~/.ssh/key_to_use 9 | User git 10 | 11 | host bitbucket.com 12 | HostName bitbucket.com 13 | IdentityFile ~/.ssh/key_to_use 14 | User git 15 | 16 | host server_to_use 17 | HostName server_to_use 18 | IdentityFile ~/.ssh/key_to_use 19 | User user_to_use 20 | -------------------------------------------------------------------------------- /misc-configs/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/var/run/supervisor/supervisor.sock ; (the path to the socket file) 3 | username = dummy ; see https://github.com/Supervisor/supervisor/issues/717 4 | password = dummy 5 | 6 | [supervisord] 7 | logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) 8 | logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) 9 | logfile_backups=10 ; (num of main logfile rotation backups;default 10) 10 | loglevel=info ; (log level;default info; others: debug,warn,trace) 11 | pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid) 12 | nodaemon=true ; (start in foreground if true;default false) 13 | minfds=1024 ; (min. avail startup file descriptors;default 1024) 14 | minprocs=200 ; (min. avail process descriptors;default 200) 15 | 16 | ; the below section must remain in the config file for RPC 17 | ; (supervisorctl/web interface) to work, additional interfaces may be 18 | ; added by defining them in separate rpcinterface: sections 19 | [rpcinterface:supervisor] 20 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 21 | 22 | [supervisorctl] 23 | serverurl=unix:///var/run/supervisor/supervisor.sock ; use a unix:// URL for a unix socket 24 | username = dummy ; see https://github.com/Supervisor/supervisor/issues/717 25 | password = dummy 26 | 27 | 28 | [program:bootstrap] 29 | command=/bootstrap_container 30 | autorestart=false 31 | startsecs=0 32 | exitcodes=0,2 33 | stdout_events_enabled=true 34 | stderr_events_enabled=true 35 | 36 | [program:install_wordpress] 37 | command=/install_wordpress 38 | autorestart=false 39 | startsecs=0 40 | exitcodes=0,2,128 41 | stdout_events_enabled=true 42 | stderr_events_enabled=true 43 | 44 | 45 | [program:php-fpm] 46 | command=/usr/sbin/php-fpm7.1 -c /etc/php/7.1/fpm 47 | stdout_events_enabled=true 48 | stderr_events_enabled=true 49 | 50 | [program:php-fpm-log] 51 | user=www-data 52 | command=tail -f /var/log/php7.1-fpm.log 53 | stdout_events_enabled=true 54 | stderr_events_enabled=true 55 | 56 | [program:nginx] 57 | command=/usr/sbin/nginx 58 | stdout_events_enabled=true 59 | stderr_events_enabled=true 60 | 61 | [program:nginx-error-log] 62 | user=www-front 63 | command=tail -f /var/log/nginx/error.log 64 | stdout_events_enabled=true 65 | stderr_events_enabled=true 66 | 67 | [program:nginx-cache-purge-log] 68 | user=www-data 69 | autostart=false 70 | command=tail -f /usr/share/nginx/www/wp-content/uploads/nginx-helper/nginx.log 71 | stdout_events_enabled=true 72 | stderr_events_enabled=true 73 | 74 | [program:cron] 75 | command = /usr/sbin/cron -f 76 | stdout_events_enabled=true 77 | stderr_events_enabled=true 78 | 79 | [program:fail2ban] 80 | autostart=false 81 | command=/usr/bin/python /usr/bin/fail2ban-server -f -s /var/run/fail2ban/fail2ban.sock 82 | stdout_events_enabled=true 83 | stderr_events_enabled=true 84 | 85 | [program:rsyslog] 86 | command=/usr/sbin/rsyslogd -n 87 | stdout_events_enabled=true 88 | stderr_events_enabled=true 89 | 90 | [eventlistener:stdout] 91 | command = supervisor_stdout 92 | buffer_size = 100 93 | events = PROCESS_LOG 94 | result_handler = supervisor_stdout:event_handler 95 | -------------------------------------------------------------------------------- /misc-configs/wordpress.conf: -------------------------------------------------------------------------------- 1 | [ssh] 2 | enabled = false 3 | 4 | [wordpress-hard] 5 | enabled = true 6 | filter = wordpress-hard 7 | logpath = /var/log/auth.log 8 | maxretry = 1 9 | port = http,https 10 | action = route 11 | 12 | [wordpress-soft] 13 | enabled = true 14 | filter = wordpress-soft 15 | logpath = /var/log/auth.log 16 | maxretry = 3 17 | port = http,https 18 | action = route 19 | 20 | 21 | [ngx-forb-ddos] 22 | enabled = true 23 | filter = nginx-forbidden 24 | logpath = /var/log/nginx/*error*.log 25 | maxretry = 3 26 | port = http,https 27 | action = route 28 | findtime = 120 29 | bantime = 86400 30 | maxretry = 3 31 | 32 | 33 | [ngx-forb-long] 34 | enabled = true 35 | filter = nginx-forbidden 36 | logpath = /var/log/nginx/*error*.log 37 | maxretry = 3 38 | port = http,https 39 | action = route 40 | findtime = 259200 41 | bantime = 2629800 42 | maxretry = 2 43 | 44 | [nginx-noproxy] 45 | enabled = true 46 | filter = nginx-noproxy 47 | logpath = /var/log/nginx*/*access*.log 48 | port = http,https 49 | action = route 50 | maxretry = 1 51 | bantime = 86400 -------------------------------------------------------------------------------- /misc-configs/wp-config-sample.php: -------------------------------------------------------------------------------- 1 | 6 | location = /favicon.ico { 7 | log_not_found off; 8 | access_log off; 9 | } 10 | 11 | location = /robots.txt { 12 | allow all; 13 | log_not_found off; 14 | access_log off; 15 | } 16 | 17 | # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac). 18 | # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban) 19 | location ~ /\. { 20 | deny all; 21 | } 22 | 23 | # Deny acccess to Wordpress's XMLRPC API. 24 | # see: http://antti.vilpponen.net/2013/08/26/how-to-mitigate-a-wordpress-xmlrpc-php-attack/ 25 | # and: https://bjornjohansen.no/block-access-to-php-files-with-nginx 26 | # and: https://github.com/Automattic/jetpack/issues/1719 27 | # and: http://literalbarrage.org/blog/2014/09/29/stop-xml-rpc-attacks-allowing-jetpack-vaultpress-access/ 28 | 29 | location = /xmlrpc.php { 30 | deny all; 31 | } 32 | 33 | 34 | # Deny access to any files with a .php extension in the standard wodpress directories (includes, content, uploads) 35 | 36 | # but allowing TinyMCE only (otherwise the formatting toolbar won't appear when making/editing posts/pages) 37 | location /wp-includes/js/tinymce/wp-tinymce.php { 38 | allow 127.0.0.1; 39 | deny all; 40 | fastcgi_pass php; 41 | fastcgi_index index.php; 42 | include fastcgi_params; 43 | fastcgi_read_timeout 300s; 44 | 45 | } 46 | 47 | 48 | #location ~* /wp-includes/.*.php$ { 49 | # deny all; 50 | #} 51 | 52 | location ~* /wp-content/.*.php$ { 53 | deny all; 54 | } 55 | 56 | location ~* /(?:uploads|files)/.*.php$ { 57 | deny all; 58 | } 59 | -------------------------------------------------------------------------------- /nginx-configs/security_headers.conf: -------------------------------------------------------------------------------- 1 | add_header X-XSS-Protection "1; mode=block" always; 2 | add_header X-Frame-Options "SAMEORIGIN" always; 3 | add_header X-Content-Type-Options "nosniff" always; 4 | -------------------------------------------------------------------------------- /nginx-configs/sites-available/nginx-site.conf: -------------------------------------------------------------------------------- 1 | # http://wiki.nginx.org/Pitfalls 2 | # http://wiki.nginx.org/QuickStart 3 | # http://wiki.nginx.org/Configuration 4 | 5 | # Upstream to abstract backend connection(s) for php 6 | upstream php { 7 | server 127.0.0.1:9000; 8 | } 9 | 10 | 11 | # general fastcgi configuration 12 | # see: https://rtcamp.com/wordpress-nginx/tutorials/single-site/fastcgi-cache-with-purging/#nginx-config 13 | 14 | fastcgi_cache_path /tmp/nginx-cache levels=1:2 keys_zone=WORDPRESS:10m inactive=60m; 15 | # If you deploy on low memory VPS, make sure to use 16 | # a disk based file for the above path 17 | 18 | 19 | fastcgi_cache_key "$scheme$request_method$host$request_uri"; 20 | fastcgi_cache_use_stale error timeout invalid_header http_500; 21 | fastcgi_ignore_headers Cache-Control Expires Set-Cookie; 22 | 23 | server { 24 | 25 | listen 80; 26 | listen [::]:80; 27 | server_name .server_fqdn; 28 | 29 | location / { 30 | return 301 https://$server_name$request_uri; 31 | } 32 | 33 | include /etc/nginx/acme.challenge.*.conf; 34 | 35 | } 36 | 37 | server { 38 | # listens both on IPv4 and IPv6 on 443 and enables HTTPS support. 39 | listen 443 ssl http2; 40 | listen [::]:443 ssl http2; 41 | 42 | include /etc/nginx/ssl.*.conf; 43 | # conditional include as explained here: http://serverfault.com/a/478344 44 | 45 | root /usr/share/nginx/www; 46 | 47 | 48 | # Make site accessible from http://localhost/ 49 | server_name .server_fqdn localhost; 50 | 51 | 52 | # Add extra http header to responses to indicate caching status (HIT, MISS, BYPASS) 53 | add_header X-Cache-Status $upstream_cache_status; 54 | 55 | # Add security headers 56 | include /etc/nginx/security_headers.conf; 57 | 58 | set $skip_cache 0; 59 | 60 | # POST requests and urls with a query string should always go to PHP 61 | if ($request_method = POST) { 62 | set $skip_cache 1; 63 | } 64 | if ($query_string != "") { 65 | set $skip_cache 1; 66 | } 67 | 68 | # Don't cache uris containing the following segments 69 | if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") { 70 | set $skip_cache 1; 71 | } 72 | 73 | # Don't use the cache for logged in users or recent commenters 74 | if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") { 75 | set $skip_cache 1; 76 | } 77 | 78 | 79 | include restrictions.conf; 80 | 81 | location / { 82 | try_files $uri $uri/ /index.php?$args; 83 | proxy_read_timeout 300; 84 | } 85 | 86 | 87 | error_page 500 502 503 504 /50x.html; 88 | location = /50x.html { 89 | root /usr/share/nginx/www; 90 | } 91 | 92 | location /search { 93 | limit_req zone=wpsearch burst=3 nodelay; 94 | try_files $uri /index.php; 95 | } 96 | # see http://kbeezie.com/securing-nginx-php/2/ 97 | 98 | 99 | location ~* \.(ico|css|js|gif|jpe?g|png)$ { 100 | expires max; 101 | add_header Pragma public; 102 | add_header Cache-Control "public, must-revalidate, proxy-revalidate"; 103 | } 104 | 105 | location ~ \.php$ { 106 | try_files $uri =404; 107 | # see: http://serverfault.com/questions/502790/security-issue-on-nginx-php-fastcgi-split-path-info 108 | 109 | limit_conn phplimit 5; 110 | # see http://kbeezie.com/securing-nginx-php/2/ 111 | 112 | fastcgi_pass php; 113 | fastcgi_index index.php; 114 | include fastcgi.conf; 115 | 116 | 117 | fastcgi_read_timeout 300s; 118 | # see: http://www.kpsolution.com/tips/nginx-php-fcgi-upstream-timed-out-110-connection-timed-out-while-reading-response-header/149/ 119 | # and: http://www.ttlsa.com/nginx/nginx-upstream-timed-out-110-connection-timed-out/ 120 | # and: http://wiki.nginx.org/NginxHttpProxyModule#proxy_read_timeout 121 | 122 | 123 | fastcgi_cache_bypass $skip_cache; 124 | fastcgi_no_cache $skip_cache; 125 | 126 | fastcgi_cache WORDPRESS; 127 | fastcgi_cache_valid 60m; 128 | } 129 | 130 | location ~ /purge(/.*) { 131 | fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1"; 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /nginx-configs/ssl.conf: -------------------------------------------------------------------------------- 1 | # nginx 1.13 | intermediate profile | OpenSSL 1.0.2g 2 | # Oldest compatible clients: Firefox 1, Chrome 1, IE 7, Opera 5, Safari 1, Windows XP IE8, Android 2.3, Java 7 3 | 4 | 5 | 6 | ssl_session_timeout 1d; 7 | ssl_session_cache shared:SSL:50m; 8 | ssl_session_tickets off; 9 | 10 | # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits 11 | ssl_dhparam /etc/nginx/dhparam.pem; 12 | 13 | # intermediate configuration. tweak to your needs. 14 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; 15 | ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; 16 | ssl_prefer_server_ciphers on; 17 | 18 | # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) 19 | add_header Strict-Transport-Security "max-age=15768000; includeSubDomains"; 20 | 21 | # OCSP Stapling --- 22 | # fetch OCSP records from URL in ssl_certificate and cache them 23 | ssl_stapling on; 24 | ssl_stapling_verify on; 25 | 26 | ## verify chain of trust of OCSP response using Root CA and Intermediate certs 27 | ssl_trusted_certificate /etc/letsencrypt/live/server_fqdn/chain.pem; 28 | 29 | # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate 30 | ssl_certificate /etc/letsencrypt/live/server_fqdn/fullchain.pem; 31 | ssl_certificate_key /etc/letsencrypt/live/server_fqdn/privkey.pem; 32 | -------------------------------------------------------------------------------- /schedulers-configs/02periodic: -------------------------------------------------------------------------------- 1 | APT::Periodic::Update-Package-Lists "1"; 2 | APT::Periodic::Download-Upgradeable-Packages "1"; 3 | APT::Periodic::AutocleanInterval "7"; 4 | APT::Periodic::Unattended-Upgrade "1"; -------------------------------------------------------------------------------- /schedulers-configs/50unattended-upgrades: -------------------------------------------------------------------------------- 1 | Unattended-Upgrade::Allowed-Origins { 2 | "${distro_id}:${distro_codename}"; 3 | "${distro_id}:${distro_codename}-security"; 4 | // "${distro_id}:${distro_codename}-updates"; 5 | // "${distro_id}:${distro_codename}-proposed"; 6 | // "${distro_id}:${distro_codename}-backports"; 7 | }; 8 | 9 | Unattended-Upgrade::Package-Blacklist { 10 | "nginx"; 11 | // "vim"; 12 | // "libc6"; 13 | // "libc6-dev"; 14 | // "libc6-i686"; 15 | }; -------------------------------------------------------------------------------- /schedulers-configs/wordpress.cron: -------------------------------------------------------------------------------- 1 | #crontab 2 | @weekly ( date ; certbot renew && /usr/sbin/nginx -t && /usr/sbin/service nginx reload ) >> /var/log/certs.log 2>&1 3 | @daily ( date ; mv /root/sql/wordpress.sql /root/sql/wordpress.sql.old ; mysqldump -h $DB_HOSTNAME -u $DB_USER -p$DB_PASSWORD $DB_DATABASE > /root/sql/wordpress.sql ) >> /var/log/db-backup.log 2>&1 4 | @hourly ( date ; /usr/bin/php /usr/share/nginx/www/wp-cron.php ) >> /var/log/wp-cron.log 2>&1 5 | -------------------------------------------------------------------------------- /scripts/bootstrap_container: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # setting up default for environment variables 6 | SERVER_NAME=${SERVER_NAME:-example.com} 7 | DB_HOSTNAME=${DB_HOSTNAME:-$DB_PORT_3306_TCP_ADDR} 8 | DB_USER=${DB_USER:-$DB_MYSQL_USER} 9 | DB_PASSWORD=${DB_PASSWORD:-$DB_MYSQL_PASSWORD} 10 | DB_DATABASE=${DB_DATABASE:-$DB_MYSQL_DATABASE} 11 | 12 | echo "$(date): Boostraping a new web server instance for $SERVER_NAME" 13 | 14 | 15 | echo "Replacing the placeholder in nginx config files for server name and cert domain name for $SERVER_NAME" 16 | sed -i -e "s/server_fqdn/$SERVER_NAME/" /etc/nginx/sites-available/default 17 | sed -i -e "s/server_fqdn/$SERVER_NAME/" /etc/nginx/ssl.conf 18 | 19 | echo "Creating a config file for letsencrypt for $SERVER_NAME" 20 | sed -i -e "s/server_fqdn/$SERVER_NAME/g" /etc/nginx/le.ini 21 | sed -i -e "s/to_be_replaced_email/$ADMIN_EMAIL/g" /etc/nginx/le.ini 22 | 23 | cp /etc/nginx/le.ini /etc/letsencrypt/cli.ini 24 | 25 | 26 | echo "add server name to /etc/hosts to avoid timeout when code make http call to public url" 27 | EXT_IP=`ip route get 8.8.8.8 | awk '{print $NF; exit}'` 28 | echo "$EXT_IP $SERVER_NAME" >> /etc/hosts 29 | echo "Wrote in /etc/hosts: $EXT_IP $SERVER_NAME" 30 | 31 | echo "We want to be able to curl the web site from the localhost using https (for purging the cache, and for the cron)" 32 | echo "127.0.0.1 $SERVER_NAME" >> /etc/hosts 33 | 34 | echo "boostrapping database backup" 35 | 36 | if [ ! -d /root/sql ];then 37 | mkdir -p /root/sql 38 | fi 39 | touch /root/sql/wordpress.sql 40 | 41 | 42 | echo "bootsrapped on $(date)" > /tmp/last_bootstrap 43 | -------------------------------------------------------------------------------- /scripts/install_wordpress: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # setting up default for environment variables 6 | SERVER_NAME=${SERVER_NAME:-my-example.com} 7 | DB_HOSTNAME=${DB_HOSTNAME:-$DB_PORT_3306_TCP_ADDR} 8 | DB_USER=${DB_USER:-$DB_MYSQL_USER} 9 | DB_PASSWORD=${DB_PASSWORD:-$DB_MYSQL_PASSWORD} 10 | DB_DATABASE=${DB_DATABASE:-$DB_MYSQL_DATABASE} 11 | 12 | # making sure only one instance of this script run 13 | 14 | if [ ! -f /var/run/install_wordpress.pid ];then 15 | echo $$ > /var/run/install_wordpress.pid 16 | else 17 | echo "install_wordpress is already running" 18 | exit 1 19 | fi 20 | 21 | echo "$(date): Installing Wordpress for $SERVER_NAME" 22 | 23 | 24 | 25 | # Installing WP-CLI 26 | 27 | echo "Installing WP-CLI" 28 | cd /tmp && curl -O -fsSL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar 29 | chmod +x wp-cli.phar && mv wp-cli.phar /usr/local/bin/wp 30 | gosu www-data wp --info && echo "WP-CLI installed" 31 | 32 | 33 | # Installing wordpress 34 | echo "Setting up the Wordpress document root" 35 | 36 | if [ ! -d "/usr/share/nginx/www" ]; then 37 | 38 | if [ -d "/usr/share/nginx/wordpress" ]; then 39 | echo "copying wordpress -> www in /usr/share/nginx/" 40 | cp -r /usr/share/nginx/wordpress /usr/share/nginx/www 41 | chown -R www-data /usr/share/nginx/www 42 | else 43 | echo "/usr/share/nginx/wordpress does not exist" 44 | fi 45 | 46 | else 47 | echo "/usr/share/nginx/www already exists" 48 | fi 49 | 50 | 51 | echo "Backing up existing wp-config in case variables have been manually added to it" 52 | if [ -f /usr/share/nginx/www/wp-config.php ]; then 53 | cp /usr/share/nginx/www/wp-config.php /usr/share/nginx/www/wp-config.php.bak 54 | fi 55 | 56 | 57 | # configuring DB connection strings 58 | echo "Replacing database connection details in wp-config with $DB_DATABASE, $DB_HOSTNAME, $DB_USER, DB_PASSWORD (hidden)" 59 | sed -e "s/database_name_here/$DB_DATABASE/ 60 | s/localhost/$DB_HOSTNAME/ 61 | s/username_here/$DB_USER/ 62 | s/password_here/$DB_PASSWORD/" /etc/wp-config-sample.php > /usr/share/nginx/www/wp-config.php 63 | 64 | # generating random keys used in Wordpress for salting 65 | echo "Generating salt for Wordpress secret keys using api.wordrpess.org" 66 | # thanks to http://stackoverflow.com/questions/9437309/bash-sed-find-replace-with-special-characters 67 | perl -i -pe ' 68 | BEGIN { 69 | $keysalts = qx(curl -fsSL https://api.wordpress.org/secret-key/1.1/salt) 70 | } 71 | s/#random_key_here/$keysalts/g 72 | ' /usr/share/nginx/www/wp-config.php 73 | 74 | 75 | echo "generated wp-config.php" 76 | 77 | # check whether it is a new install or not by checking existence of database dump 78 | 79 | 80 | if ! [[ -f /usr/share/nginx/wordpress.sql || -f /usr/share/nginx/wordpress.sql.loaded ]]; then 81 | 82 | echo "no database dump so it's a new install..." 83 | # Replacing Wordpress Default Password handling with wp-password-bcrypt 84 | # explanation here: https://roots.io/wordpress-password-security-follow-up/ 85 | if [ ! -d /usr/share/nginx/www/wp-content/mu-plugins ];then 86 | mkdir -p /usr/share/nginx/www/wp-content/mu-plugins 87 | fi 88 | cd /usr/share/nginx/www/wp-content/mu-plugins && curl -O -fsSL https://raw.githubusercontent.com/roots/wp-password-bcrypt/master/wp-password-bcrypt.php 89 | echo "performed: installed wp-password-bcrypt" 90 | 91 | # Wordpress initial setup 92 | gosu www-data wp core install --url=https://$SERVER_NAME --title=$SERVER_NAME --admin_user=admin --admin_email=$ADMIN_EMAIL --admin_password=$ADMIN_PASSWORD 93 | echo "performed: wp core install (--url=https://$SERVER_NAME --title=$SERVER_NAME --admin_user=admin --admin_email=$ADMIN_EMAIL --admin_password=##HIDDEN##)" 94 | 95 | 96 | # Bootstraping Nginx Helper Wordpress plugin's log 97 | if [ ! -d /usr/share/nginx/www/wp-content/uploads/nginx-helper ];then 98 | mkdir -p /usr/share/nginx/www/wp-content/uploads/nginx-helper 99 | fi 100 | echo "Start logging" >> /usr/share/nginx/www/wp-content/uploads/nginx-helper/nginx.log 101 | chown www-data:www-data /usr/share/nginx/www/wp-content/uploads/nginx-helper/nginx.log 102 | 103 | # installing WPFail2Ban plugin 104 | cd /usr/share/nginx/www 105 | gosu www-data wp plugin install wp-fail2ban --activate 106 | 107 | # installing Nginx Helper plugin 108 | gosu www-data wp plugin install nginx-helper --activate 109 | 110 | # installing WP Content Security Policy plugin 111 | gosu www-data wp plugin install wp-content-security-policy --activate 112 | 113 | # installing Disable XML-RPC Pingback 114 | gosu www-data wp plugin install disable-xml-rpc-pingback --activate 115 | 116 | 117 | 118 | else 119 | 120 | echo "this is an existing Wordpress web site, loading the database dump if not loaded already ..." 121 | if ! [[ -f /usr/share/nginx/wordpress.sql.loaded || -f /root/sql/no_db_import ]]; then 122 | mysql -h $DB_HOSTNAME -u $DB_USER -p$DB_PASSWORD $DB_DATABASE < /usr/share/nginx/wordpress.sql 123 | if [ $? = 0 ]; then 124 | mv /usr/share/nginx/wordpress.sql /usr/share/nginx/wordpress.sql.loaded 125 | echo "performed mysql database import" 126 | fi 127 | else 128 | echo "nothing to do, database dump already imported or no_db_import option set" 129 | fi 130 | 131 | fi 132 | 133 | 134 | # copying wordpress filters for fail2ban 135 | echo "Downloading fail2ban Wordpress filters" 136 | cd /etc/fail2ban/filter.d/ \ 137 | && curl -O -fsSL https://plugins.svn.wordpress.org/wp-fail2ban/trunk/filters.d/wordpress-hard.conf \ 138 | && curl -O -fsSL https://plugins.svn.wordpress.org/wp-fail2ban/trunk/filters.d/wordpress-soft.conf 139 | 140 | # restarting fail2ban 141 | echo "(Re)starting fail2ban..." 142 | supervisorctl restart fail2ban 143 | fail2ban-client -s /var/run/fail2ban/fail2ban.sock reload 144 | 145 | # Ensuring the cronjobs can read the database environment variables 146 | echo "For the database backup cronjob: env | grep DB > /etc/environment" 147 | env | grep DB > /etc/environment 148 | 149 | echo "Wordpress installed on $(date)" > /tmp/last_wordpress 150 | 151 | if [ -f /var/run/install_wordpress.pid ];then 152 | rm /var/run/install_wordpress.pid 153 | fi 154 | 155 | exit 0 156 | -------------------------------------------------------------------------------- /scripts/setup_web_cert: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "generating the cert for $SERVER_NAME and www.$SERVER_NAME" 6 | 7 | ! certbot certificates --cert-name $SERVER_NAME | grep "VALID" && certbot certonly 8 | echo "copying the NGINX SSL conf for $SERVER_NAME" 9 | cp /etc/nginx/ssl.conf /etc/nginx/ssl.$SERVER_NAME.conf 10 | echo "reloading NGINX configuration !" 11 | /usr/sbin/nginx -t && service nginx reload 12 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | This directory must contain the web site to be deployed to container. 2 | It's either vanilla Wordpress install, or an existing Wordpress web site alongside the database schema. 3 | 4 | In both case, add a VERSION file so the ``make_env`` script can read your project's version (it will be used as a tag to the Docker image uploaded to private registry) 5 | 6 | ## Naming convention and file structure for vanilla installation 7 | 8 | ``` 9 | website/ 10 | ├── make_env 11 | ├── README.md 12 | ├── VERSION 13 | └── wordpress 14 | ``` 15 | 16 | For a new web site you can download and unpack Wordpress from Wordpress.org web site 17 | Make sure the downloaded archive is extracted into a directory named "wordpress": 18 | 19 | ```bash 20 | cd website 21 | curl -o wordpress.tar.gz https://wordpress.org/latest.tar.gz 22 | tar xzvf wordpress.tar.gz 23 | rm wordpress.tar.gz 24 | ``` 25 | 26 | Alternatively it's possible to clone Wordpress from its git repository: 27 | ```bash 28 | git clone https://github.com/WordPress/WordPress.git website/wordpress 29 | ``` 30 | 31 | 32 | ## Naming convention and file structure for installing existing wordpress based web site 33 | 34 | ``` 35 | website/ 36 | ├── make_env 37 | ├── README.md 38 | ├── wordpress 39 | ├── VERSION 40 | └── wordpress.sql 41 | ``` 42 | 43 | For existing web site you can retrieve from version control or copy project files directly. 44 | Either way make sure, the web site is in a directory named "wordpress". 45 | The database dump for the existing web site should be placed alongside the web site files and named "wordpress.sql" 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /website/make_env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | cd wordpress 6 | BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` 7 | VCS_REF=`git rev-parse --short HEAD` 8 | VERSION=`cat ../VERSION` 9 | cd .. 10 | 11 | if [ ! -f .env ];then 12 | { 13 | echo "SERVER_NAME=example.com"; 14 | echo "COMPOSE_PROJECT_NAME=wordpress"; 15 | echo "#The email will be used for registering to Lets Encrypt for the TLS certificate" 16 | echo "ADMIN_EMAIL=user@example.com"; 17 | echo "ADMIN_PASSWORD=changeme"; 18 | echo "WP_DB_USER=wordpress"; 19 | echo "WP_DB_PASSWORD=wordpress"; 20 | echo "WP_DB_NAME=wordpress"; 21 | echo "MYSQL_ROOT_PASSWORD=wordpress"; 22 | echo "IMAGE_NAME=my-wordpress-dev"; 23 | echo "REGISTRY_URL=registry.gitlab.com" 24 | echo "BUILD_DATE=$BUILD_DATE"; 25 | echo "VCS_REF=$VCS_REF"; 26 | echo "VERSION=$VERSION"; 27 | } > .env 28 | echo ".env created" 29 | else 30 | sed -i.bak -e "s/BUILD_DATE\s*=\s*.*$/BUILD_DATE=$BUILD_DATE/ 31 | s/VCS_REF\s*=\s*.*$/VCS_REF=$VCS_REF/ 32 | s/VERSION\s*=\s*.*$/VERSION=$VERSION/" .env 33 | echo ".env updated" 34 | fi 35 | 36 | --------------------------------------------------------------------------------