Commit a9381087 authored by Dom Sekotill's avatar Dom Sekotill
Browse files

Add documentation

parent de4e3f0d
Loading
Loading
Loading
Loading

README.md

0 → 100644
+64 −0
Original line number Diff line number Diff line
Wordpress for Docker
====================

Docker images for Wordpress.

The primary aim is to be secure and easy to deploy.  Unlike other Wordpress 
images the statefulness is reduced to the bare minimum; aside from the 
required database only the uploaded media directory needs to be persistent, 
and even that can be avoided with a media cloud storage plugin[^1].

Two images are produced from the Dockerfile:

-   The primary runs a PHP server with a FastCGI (port 9000) interface 
    ([PHP-FPM][]), with Wordpress installed.

-   The second image runs Nginx with an HTTP (port 80) interface, which 
    serves all the static files and proxies non-static requests to the 
    PHP-FPM server.


[^1]: In a future release Simple Storage Service (S3) services may be 
  supported out-of-the-box.

[php-fpm]: https://php-fpm.org/


Usage
-----

Typical usage requires both images be run in containers on the same host, 
sharing a volatile (non-persistent) volume containing the static files, 
mounted at */app/static*. This volume is populated by the PHP-FPM container 
during its startup.

The containers share another persistent volume for user-uploaded media files 
(if no media cloud storage plugin is used) mounted at */app/media*.

The nginx container is not intended to be publicly accessible (it only 
listens on port 80).  Some form of HTTPS termination is required at 
a minimum.

See the [configuration document](/doc/configuration.md) for an explanation 
of the configuration files and the available options.

See the [Kubernetes example document](/doc/k8s-example.md) to see an example 
deployment of pods running these services.


Build
-----

**Note:** Building manually requires Docker 17.05 or later.

To build the PHP-FPM image run:

```shell
docker build -t wordpress:tag .
```

To build the Nginx companion image, run:

```shell
docker build -t wordpress-nginx:tag --target=nginx .
```

doc/configuration.md

0 → 100644
+161 −0
Original line number Diff line number Diff line
Configuration
=============

Configuration files are mounted (or added in a child image) under 
*/etc/wordpress*.  All files in this directory or any subdirectory which 
match `*.conf` will be sourced as Bash scripts. The valid variable names 
which are understood by the entrypoint script are listed below.

Any Bash commands are valid in the `*.conf` files, allowing the 
configuration process to be flexible.

It is recommended that when setting the array variables the "+=" operator be 
used to append, rather than replace values.  This allows separate config 
files to cooperatively configure the service.  It also appends to the 
default values, if any.

The order of file sourcing is Bash's wildcard matching; numeric then 
alphabetic for both files and directories together, and descending into 
directories as they are found. The following shows some example files in 
their sourced order:

```shell
$ ls -1f **/*.conf
00-init.conf
01-first-dir/initial.conf
01-first-dir/next.conf
01-first-dir/zz-final.conf
another.conf
second-dir/example.conf
zz-final.conf
```


Convenience Files
-----------------

In addition to the values specified in `*.conf` files, for convenience the 
files specified by [**PLUGINS_LIST**](#plugins_list), 
[**THEMES_LIST**](#themes_list) and [**LANGUAGES_LIST**](#languages_list) 
options are optional plain text files listing additional entries (one per 
line) to append to the [**PLUGINS**](#plugins), [**THEMES**](#themes) and 
[**LANGUAGES**](#languages) option arrays respectively.


Options
-------

### DB_NAME

**Type**: string\
**Required**: yes

The name of the MySQL database.

### DB_USER

**Type**: string\
**Required**: yes

The username if credential authentication is required to access the 
database.

### DB_PASS

**Type**: string\
**Required**: no

The password if credential authentication is required to access the 
database.

### DB_HOST

**Type**: string\
**Required**: no\
**Default**: localhost

The hostname of the MySQL server providing the database.

### LANGUAGES

**Type**: array\
**Required**: no

This is an array of language packs to install at startup.  Items are POSIX 
locale language tags. (e.g. 'en_US').  If a locale is not available for 
Wordpress core the startup will fail.  If it is not available for a plugin 
the missing language pack will be silently ignored.

### LANGUAGES_LIST

**Type**: string\
**Required**: no\
**Default**: /etc/wordpress/languages.txt

The path to a file containing lines to append to 
[**LANGUAGES**](#languages).

### PLUGINS

**Type**: array\
**Required**: no

This is an array of plugins to install at startup.  Items can be plugin 
names or URLs to .zip files.  When given a name the version installed will 
be the latest stable available in the wordpress.org registry.

### PLUGINS_LIST

**Type**: string\
**Required**: no\
**Default**: /etc/wordpress/plugins.txt

The path to a file containing lines to append to [**PLUGINS**](#plugins).

### STATIC_PATTERNS

**Type**: array\
**Required**: no\
**Default**: various documentation files and formats, certificates and i18n 
data files

This is an array of shell wildcard patterns (non-GNU extensions) matching 
files which will NOT be copied to the static files directory.

**Note:** Files with a .php extension are never copied to the static files 
directory.

### THEMES

**Type**: array\
**Required**: no

This is an array of themes to install at startup.  Items can be theme names 
or URLs to .zip files.  When given a name the version installed will be the 
latest stable available in the wordpress.org registry.

### THEMES_LIST

**Type**: string\
**Required**: no\
**Default**: /etc/wordpress/themes.txt

The path to a file containing lines to append to [**THEMES**](#themes).

### WP_CONFIG_EXTRA

**Type**: string\
**Required**: no\
**Default**: /etc/wordpress/wp-config-extra.php

This is the path to a file containing additional content for *wp-config.php* 
to go near the end of the file.

### WP_CONFIG_LINES

**Type**: array\
**Required**: no

This is an array of lines (without terminating carriage-returns) to add to 
*wp-config.php* just before the contents of the file in 
[**WP_CONFIG_EXTRA**](#wp_config_extra).

doc/k8s-example.md

0 → 100644
+272 −0
Original line number Diff line number Diff line
Kubernetes
==========

This document outlines how to run Wordpress as a Kubernetes Deployment.

It is assumed that there is an [ingress controller][] already configured for 
the cluster, and that the reader is familiar with tasks such as provisioning 
a TLS certificate and a PersistentVolume to back the PersistentVolumeClaim.

The complete example YAML file which goes with this document is 
[k8s-example.yml](k8s-example.yml).


Configuration
-------------

Sensitive information like the database credentials and TLS certificate key 
should go into Secrets, while all other [configuration 
values](configuration.md) can be placed into ConfigMaps.  As the 
configuration files under **/etc/wordpress** can be nested in subdirectories 
there is no problem mounting multiple Secrets and ConfigMaps.

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
data:
  plugins.txt: |
    cache-control
    wp-mail-smtp
    wp-statistics
  wordpress.conf: |
    THEMES=( twentyeighteen twentynineteen )
    LANGUAGES+=( en_GB fr_FR de_DE )

---

apiVersion: v1
kind: Secret
metadata:
  name: my-app-mysql-pass
data:
  mysql.conf: |
    REJfSE9TVD1teXNxbC5leGFtcGxlLmNvbQpEQl9OQU1FPWV4YW1wbGVfZGIKREJfVVNFUj1leGFt
    cGxlX3VzZXIKREJfUEFTUz1aWGhoYlhCc1pWQmhjM04zYjNKa0NnCg==
```


Deployment
----------

The author recommends managing Pods with Deployments.

### Static Files Volume

The two containers which make up a Wordpress Pod (FastCGI server and HTTP 
server) need to share static files which the FastCGI server (which contains 
the Wordpress core, plugin and theme files) has, but which the HTTP server 
needs to serve.  This volume does not need to be persistent, so can be of 
the 'emptyDir' type:

```yaml
apiVersion: apps/v1
kind: Deployment
spec:
  containers:
  - name: fastcgi
    volumeMounts:
    - name: static
      mountPath: /app/static

  - name: http
    volumeMounts:
    - name: static
      mountPath: /app/static

  volumes:
  - name: static
    emptyDir: {}
```

### User Uploads Volume

All content that users upload to Wordpress goes into a tree within 
**/app/media**, which must be a persistent volume mounted in both 
containers.  This volume is requested through a PersistentVolumeClaim:

```yaml
apiVersion: apps/v1
kind: Deployment
spec:
  containers:
  - name: fastcgi
    volumeMounts:
    - name: media
      mountPath: /app/media

  - name: http
    volumeMounts:
    - name: media
      mountPath: /app/media

  volumes:
  - name: media
    persistentVolumeClaim:
      claimName: my-app-media

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-app-media
spec:
  accessModes:
  - ReadWriteMany
  storageClassName: ""
  resources:
    requests:
      storage: 5Gi
```

### Readiness Probe

Prior to staring the FastCGI server the Wordpress image downloads and 
installs plugins, themes, and language packs, then populates the shared 
static files volume.  This takes some time so a readiness probe checking for 
the FastCGI port (9000) is needed:

```yaml
apiVersion: apps/v1
kind: Deployment
spec:
  containers:
  - name: fastcgi
    readinessProbe:
      periodSeconds: 60
      tcpSocket:
        port: 9000
```

### Final

Putting it all together, the complete Deployment for running a single 
instance of a Pod with the two containers:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: fastcgi
        image: docker.kodo.org.uk/singing-chimes.co.uk/wordpress/fastcgi:latest
        imagePullPolicy: Always
        volumeMounts:

        # Keep MySQL credentials in a Secret
        - name: mysql-pass
          mountPath: /etc/wordpress/secret

        # Rest of the config
        - name: config
          mountPath: /etc/wordpress

        # Shared non-persistent volume
        - name: static
          mountPath: /app/static

        # Shared persistent user-media volume
        - name: media
          mountPath: /app/media

        readinessProbe:
          periodSeconds: 5
          tcpSocket:
            port: 9000

      - name: http
        image: docker.kodo.org.uk/singing-chimes.co.uk/wordpress/nginx:latest
        imagePullPolicy: Always
        volumeMounts:
        - name: static
          mountPath: /app/static
        - name: media
          mountPath: /app/media

      volumes:
      - name: mysql-pass
        secret:
          secretName: my-app-mysql-pass
      - name: config
        configMap:
          name: my-app-config
      - name: static
        emptyDir: {}
      - name: media
        persistentVolumeClaim:
          claimName: my-app-media
```


Service and Ingress
-------------------

To expose the Wordpress instance publicly we need a Service and Ingress. The 
Ingress acts as the TLS termination for the website.

```yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  ports:
  - name: http
    protocol: TCP
    port: 80

---

apiVersion: v1
kind: Secret
type: kubernetes.io/tls
metadata:
  name: my-app-cert
data:
  tls.crt: 
  tls.key: 

---

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-app
  annotations:
    nginx.org/hsts: True
spec:
  tls:
  - hosts: [my-app.co.uk]
    secretName: my-app-cert

  rules:
  - host: my-app.co.uk
    http:
      paths:
      - path: /
        backend:
          serviceName: my-app
          servicePort: http
```


[ingress controller]:
  https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/
  "Kubernetes.io Ingress Controllers"

doc/k8s-example.yml

0 → 100644
+151 −0
Original line number Diff line number Diff line
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
data:
  plugins.txt: |
    cache-control
    wp-mail-smtp
    wp-statistics
  wordpress.conf: |
    THEMES=( twentyeighteen twentynineteen )
    LANGUAGES+=( en_GB fr_FR de_DE )

---

apiVersion: v1
kind: Secret
type: kubernetes.io/tls
metadata:
  name: my-app-cert
data:
  tls.crt: 
  tls.key: 

---

apiVersion: v1
kind: Secret
metadata:
  name: my-app-mysql-pass
data:
  mysql.conf: |
    REJfSE9TVD1teXNxbC5leGFtcGxlLmNvbQpEQl9OQU1FPWV4YW1wbGVfZGIKREJfVVNFUj1leGFt
    cGxlX3VzZXIKREJfUEFTUz1aWGhoYlhCc1pWQmhjM04zYjNKa0NnCg==

---

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-app-media
spec:
  accessModes:
  - ReadWriteMany
  storageClassName: ""
  resources:
    requests:
      storage: 5Gi

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: fastcgi
        image: docker.kodo.org.uk/singing-chimes.co.uk/wordpress/fastcgi:latest
        imagePullPolicy: Always
        volumeMounts:

        # Keep MySQL credentials in a Secret
        - name: mysql-pass
          mountPath: /etc/wordpress/secret

        # Rest of the config
        - name: config
          mountPath: /etc/wordpress

        # Shared non-persistent volume
        - name: static
          mountPath: /app/static

        # Shared persistent user-media volume
        - name: media
          mountPath: /app/media

        readinessProbe:
          periodSeconds: 5
          tcpSocket:
            port: 9000

      - name: http
        image: docker.kodo.org.uk/singing-chimes.co.uk/wordpress/nginx:latest
        imagePullPolicy: Always
        volumeMounts:
        - name: static
          mountPath: /app/static
        - name: media
          mountPath: /app/media

      volumes:
      - name: mysql-pass
        secret:
          secretName: my-app-mysql-pass
      - name: config
        configMap:
          name: my-app-config
      - name: static
        emptyDir: {}
      - name: media
        persistentVolumeClaim:
          claimName: my-app-media

---

apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  ports:
  - name: http
    protocol: TCP
    port: 80

---

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-app
  annotations:
    nginx.org/hsts: True
spec:
  tls:
  - hosts: [my-app.co.uk]
    secretName: my-app-cert

  rules:
  - host: my-app.co.uk
    http:
      paths:
      - path: /
        backend:
          serviceName: my-app
          servicePort: http
+6 −0
Original line number Diff line number Diff line
@@ -50,6 +50,12 @@ create_config()
		${DB_HOST+--dbhost=${DB_HOST}} \
		${DB_PASS+--dbpass=${DB_PASS}} \
	<<-END_CONFIG
		/*
		 * Plugins, themes and language packs cannot be configured through the 
		 * admin interface; modify the configuration in /etc/wordpress/ 
		 * according to the documentation for PLUGINS[_LIST], THEMES[_LIST] and 
		 * LANGUAGES[_LIST]
		 */
		define('DISALLOW_FILE_MODS', true);

		/*