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
Code language: JavaScript (javascript)

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.

# 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 = "" storage = "acme.json" entryPoint = "https" onHostRule = true [acme.httpChallenge] entryPoint = "http" [docker] domain = "" watch = true network = "web"
Code language: PHP (php)

acme.json file to store certificates

touch acme.json chmod 600 acme.json
Code language: CSS (css)

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 \ -l traefik.port=8080 \ --network web \ --name traefik \ traefik:1.7.2-alpine
Code language: PHP (php)

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.port=8080 networks: - internal - web depends_on: - db networks: web: external: true internal: external: true
Code language: JavaScript (javascript)

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;
Code language: PHP (php)

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.port=80 - traefik.frontend.headers.SSLRedirect:true - traefik.frontend.headers.SSLForceHost:true - - traefik.frontend.redirect.regex:[/](.*))* - traefik.frontend.redirect.replacement:$${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
Code language: JavaScript (javascript)

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 and install WordPress. However, I first restore my MySQL database of blog1. This was easily accomplished by the 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 # # 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";}'
Code language: PHP (php)

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
Code language: HTML, XML (xml)

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)
Code language: JavaScript (javascript)

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>
Code language: PHP (php)

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
Code language: JavaScript (javascript)

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

# # <IfModule mpm_prefork_module> StartServers 2 MinSpareServers 2 MaxSpareServers 6 MaxRequestWorkers 15 MaxConnectionsPerChild 200 </IfModule>
Code language: PHP (php)

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 . 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 !