DigitalOcean Docker Apache/WordPress Optimization

I was running multiple docker containers on DO cloud droplet with low 1G memory and 1 vCore, specifically –

  • Traefik Reverse Proxy x 1
  • MariaDb x 3
  • WordPress Apache x 3
  • Adminer x 3

All in all a cool 3 DBs, and 7 webservers. Recipe for disaster right. No wonder as Apache spooled up worker processes and MariaDb’s InnoDb engine started fulfilling requests, one of the containers ran out of memory and the droplet CPU was running all time high.

docker ps -a
docker stats
docker logs 4703f6a45c3a
htop
free -m

Now I still wish to serve up all these sites, so what do I do ? First take backup of the databases.

docker inspect 4703f6a45c3a  | grep IPAddress
mysqldump -u USER -p -h  XXX.XXX.XX.XX  blog1 |  gzip > blog1.gz

Decrease Number of Containers

Only one database now serves up all three websites and has its one Adminer interface.

  • Traefik Reverse Proxy x 1
  • MariaDb x 1
  • WordPress Apache x 3
  • Adminer x 1

Managing Docker Networks

Docker container has two networks connected as:

  • Web – for Traefik – browser, Adminer – browser, and WP/Apache-browser communications
  • Internal – for MariaDb – WP and MariaDb – Adminer communications

Both networks were defined externally (and not in docker-compose).

docker network ls
docker network create web
docker network create --driver bridge internal
docker network inspect internal

Access /Edit files and Bash inside Docker Containers

First access the containers name and id. Then access the relevant container and copy the relevant file to the current directory. Edit the file and copy it back to the container. Restart container

docker ps -a
docker cp 4703f6a45c3a:/etc/mysql/my.cnf .
nano my.cnf 
docker cp my.cnf 4703f6a45c3a:/etc/mysql/my.cnf
docker restart 4703f6a45c3a

One can also access the bash shell of the container

docker exec -it 4703f6a45c3a bash

Traefik Configuration

Generate the traefik dashboard access password

htpasswd -nb admin secure_password

traefik.toml file.

#https://www.digitalocean.com/community/tutorials/how-to-use-traefik-as-a-reverse-proxy-for-docker-containers-on-ubuntu-18-04
defaultEntryPoints = ["http", "https"]

[entryPoints]
  [entryPoints.dashboard]
    address = ":8080"
    [entryPoints.dashboard.auth]
      [entryPoints.dashboard.auth.basic]
        users = ["traefikusername:$hasedPassword"]
  [entryPoints.http]
    address = ":80"
      [entryPoints.http.redirect]
        entryPoint = "https"
  [entryPoints.https]
    address = ":443"
      [entryPoints.https.tls]

[api]
entrypoint="dashboard"

[acme]
email = "mail@provider.com"
storage = "acme.json"
entryPoint = "https"
onHostRule = true
  [acme.httpChallenge]
  entryPoint = "http"

[docker]
domain = "blog1.com"
watch = true
network = "web"

acme.json file to store certificates

touch acme.json
chmod 600 acme.json

Command to spin up Traefik container

    docker run -d \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v $PWD/traefik.toml:/traefik.toml \
      -v $PWD/acme.json:/acme.json \
      -p 80:80 \
      -p 443:443 \
      -l traefik.frontend.rule=Host:traefik.blog1.com \
      -l traefik.port=8080 \
      --network web \
      --name traefik \
      traefik:1.7.2-alpine

Docker-Compose configuration

Docker-compose for MariaDb Database

version: "3"

services:
  db:
    image: mariadb:10.4.3
    environment:
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    networks:
      - internal
    labels:
      - traefik.enable=false
    volumes:
      - ./datadir:/var/lib/mysql
      
  adminer:
    image: adminer:4.6.3-standalone
    labels:
      - traefik.backend=adminer
      - traefik.frontend.rule=Host:adminer.blog1.com
      - traefik.docker.network=web
      - traefik.port=8080
    networks:
      - internal
      - web
    depends_on:
      - db
      
networks:
  web:
    external: true
  internal:
    external: true

Note the MariaDb root password and keep it safe

docker ps -a
docker logs dbContainerName | grep password

Connect to the MySQL container through a mysql client. Once in the mysql shell, create a User and Database in MariaDb for your blog and grant that user privileges to your database.

docker inspect logs dbContainerName | grep  IPA
# NOte the IP address
mysql -u root -h XXX.XXX.XX.XX -p
# Enter the root password to reach the database shell >
 
CREATE USER 'MariaDbUser'@'%' IDENTIFIED BY 'MariaDbUserPassword';
GRANT ALL ON blog1.* TO 'username'@'%';  
flush privileges;

Docker-compose for Apache/Wordpress images.

version: "3"

services:
  blog1:
    image: wordpress:5.1.1-apache

    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: MariaDbUser
      WORDPRESS_DB_PASSWORD: MariaDbUserPassword
      WORDPRESS_DB_NAME: blog1
    labels:
      - traefik.backend=blog1
      - traefik.frontend.rule=Host:www.blog1.com, blog1.com
      - traefik.docker.network=web
      - traefik.port=80
      - traefik.frontend.headers.SSLRedirect:true
      - traefik.frontend.headers.SSLForceHost:true
      - traefik.frontend.headers.SSLHost:www.blog1.com
      - traefik.frontend.redirect.regex:https://blog1.com([/](.*))*
      - traefik.frontend.redirect.replacement:https://www.blog1.com$${1}

    volumes:
      - ./wordpress_files:/var/www/html
      - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini

    networks:
      - internal
      - web
    
networks:
  web:
    external: true
  internal:
    external: true

PHP Configuration

A note about uploads.ini. It is to ensure uploading of files and plugins to the wordpress site. Its contents include –

file_uploads = On
memory_limit = 128M
upload_max_filesize = 128M
post_max_size = 20M
max_execution_time = 600

WordPress Installation and Database Restore

Now I could have visited www.blog1.com and install WordPress. However, I first restore my MySQL database of blog1. This was easily accomplished by the adminer.blog1.com. While logging in, for host, use ‘db’, and either login as root or as the MariaDbUser with the appropriate password.

Apache Tuning

The WordPress Apache image can start consuming a lot of memory very fast. The defaults can be viewed by logging on to the calendar

docker exec -it dbContainerName bash
cat /etc/apache2/apache2.conf
# View the enabled Modules
apache2ctl -M
# View the prefork module settings
cat /etc/apache2/mods-enabled/mpm_prefork.conf
# https://www.jeffgeerling.com/blog/3-small-tweaks-make-apache-fly
# Apache thread memory consumption
ps aux | grep 'httpd' | awk '{print $6/1024 " MB";}'
ps aux | grep 'httpd' | awk '{print $6/1024;}' | awk '{avg += ($1 - avg) / NR;} END {print avg " MB";}'

apache2.conf defaults

Timeout 300
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
HostnameLookups Off

LogLevel warn
ErrorLog ${APACHE_LOG_DIR}/error.log

PidFile ${APACHE_PID_FILE}
DefaultRuntimeDir ${APACHE_RUN_DIR}

IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.conf

Include ports.conf

<Directory />
        Options FollowSymLinks
        AllowOverride None
        Require all denied
</Directory>

<Directory /usr/share>
        AllowOverride None
        Require all granted
</Directory>

<Directory /var/www/>
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
</Directory>
AccessFileName .htaccess

<FilesMatch "^\.ht">
        Require all denied
</FilesMatch>

LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent

apache2ctl -M

Loaded Modules:
 core_module (static)
 so_module (static)
 watchdog_module (static)
 http_module (static)
 log_config_module (static)
 logio_module (static)
 version_module (static)
 unixd_module (static)
 access_compat_module (shared)
 alias_module (shared)
 auth_basic_module (shared)
 authn_core_module (shared)
 authn_file_module (shared)
 authz_core_module (shared)
 authz_host_module (shared)
 authz_user_module (shared)
 autoindex_module (shared)
 deflate_module (shared)
 dir_module (shared)
 env_module (shared)
 expires_module (shared)
 filter_module (shared)
 mime_module (shared)
 mpm_prefork_module (shared)
 negotiation_module (shared)
 php7_module (shared)
 reqtimeout_module (shared)
 rewrite_module (shared)
 setenvif_module (shared)
 status_module (shared)

Among these, the mpm_prefork module is required for serving up PHP pages and is the primary target of tuning.

mpm_prefork.conf defaults

# prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of requests a server process serves

<IfModule mpm_prefork_module>
        StartServers              5
        MinSpareServers           5
        MaxSpareServers           10
        MaxRequestWorkers         150
        MaxConnectionsPerChild    0
</IfModule>

Editing the config files

This is better done on host machine since the container lacks vim or nano.

docker cp dbContainerName:/etc/apache2/apache2.conf .
cp apache2.conf apache2.conf.default
nano apache2.conf 
docker cp apache2.conf dbContainerName:/etc/apache2/apache2.conf

docker cp dbContainerName:/etc/apache2/mods-available/mpm_prefork.conf .
cp mpm_prefork.conf mpm_prefork.conf.default
nano mpm_prefork.conf  dbContainerName:/etc/apache2/mods-available/mpm_prefork.conf

Some settings for apache2.conf for low traffic WP sites

MaxKeepAliveRequests 30
KeepAliveTimeout 3
LogLevel error

Some settings for mpm_prefork.conf for low traffic WP sites

# https://www.linode.com/docs/web-servers/apache-tips-and-tricks/tuning-your-apache-server/
# https://httpd.apache.org/docs/2.4/misc/perf-tuning.html
<IfModule mpm_prefork_module>
	StartServers	   2
	MinSpareServers    2
	MaxSpareServers	   6
	MaxRequestWorkers   15
	MaxConnectionsPerChild   200
</IfModule>

Restart the WP/Apache container

docker containerId restart

Check apache version inside container

apachectl -v
Server version: Apache/2.4.25 (Debian)
Server built:   2018-11-03T18:46:19

Download and Extract the 64 bit deb installer files from https://www.modpagespeed.com/doc/download . After extraction, you will have two more zip files that again need to be extracted. we are looking for etc, var and usr folders and their contents in the data.tar.gz archive inside the deb file. Move these there folders to a data folder.

docker cp ./path/to/mod_pagespeed_24/data/. containerID:/

Enable the module and restart the docker container

docker exec -it containerID a2enmod pagespeed
docker restart containerID

Check that the pagespeed module is active or not LINK

Hopefully, these tips will help you in achieving better performance with small resources !