In this guide, let’s go through the process of installing and configuring WordPress on a Docker container using docker-compose. The primary goals are to install a bare-bones install with the ability to customise wp-config.php
, php.ini
and my.cnf
. There is an expectation that you are familiar with Docker, WordPress, PHP and MySQL configuration files.
Install WordPress on Docker
On the system hosting Docker, create a working directory for the WordPress installation.
# Create a working directory mkdir example.com # Change the current working directory cd example.com
Inside the working directory create a docker-compose.yml
configuration file for editing. I use Visual Studio Code with the Docker extension, as shown below, but any editor is fine.
# An example command to launch Visual Studio Code code docker-compose.yml
Go to the official page for the WordPress Docker image on Docker Hub.
https://hub.docker.com/_/wordpress
Scroll down on the Description tab until you reach via docker stack deploy or docker-compose.
version: '3.1' services: wordpress: image: wordpress restart: always ports: - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - wordpress:/var/www/html db: image: mysql:5.7 restart: always environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - db:/var/lib/mysql volumes: wordpress: db:

Copy the code block for the Example stack.yml into your editor with docker-compose.yml
open and save the file.
Back in the terminal, create and start the newly saved Docker composition.
# Build, create, start and attach the container services docker-compose up
Once ready, the containers return this feedback.
wordpress_1 | Apache/2.4.38 (Debian) PHP/7.3.11 configured -- resuming normal operations wordpress_1 | Command line: 'apache2 -D FOREGROUND'
A new WordPress installation should be ready at http://localhost:8080. Fill out all the relevant form of information and you’ll be at the WordPress administration page. Congratulations, WordPress is installed!

If you have any conflicts or issues with port 8080. Edit docker-compose.yml
and change the 8080:80
ports configuration in the WordPress container to a different value such as 8500:80
and run docker-compose up
again. You’ll also have to visit the new URL http://localhost:8500.
Resetting your WordPress Docker container
If at any time you wish to revert your container to a fresh install you’ll need to remove the storage volumes with these commands. Note, this cannot be undone.
# Stop and remove the containers docker-compose down # Delete both the wordpress and db file volumes docker volume rm examplecom_db examplecom_wordpress # Rebuild, recreate, start and attach the container services docker-compose up
Improving docker-compose.yml
Renaming the database name, its user and password
The sample docker-compose.yml
configuration is minimal and is not suitable for production. So the first thing you may want to do is replace the environment database name, user and password values for both the WordPress and DB containers. As our database user does not have significant permissions, this process requires the deletion of the existing database volume and any of its data.
WORDPRESS_DB_...
values must match their MYSQL_...
environment values.
wordpress: environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_NAME: myblogdb WORDPRESS_DB_USER: wpinternal WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage
db: environment: MYSQL_DATABASE: myblogdb MYSQL_USER: wpinternal MYSQL_PASSWORD: stencil-running-cargo-savage
Now we need to rebuild the MySQL database with these new values.
# Stop and remove the containers docker-compose down # Delete the db file volume docker volume rm examplecom_db # Rebuild, recreate, start and attach the container services docker-compose up
To test the account and password changes run the following.
# Log into the database with the MySQL command-line client docker-compose exec db mysql --user=wpinternal --password=stencil-running-cargo-savage myblogdb
Then in the mysql>
prompt.
show tables; quit;
Note that show tables;
only return data if the WordPress installation is complete.
mysql> show tables; +-----------------------+ | Tables_in_myblogdb | +-----------------------+ | wp_commentmeta | | wp_comments | | wp_links | | wp_options | | wp_postmeta | | wp_posts | | wp_term_relationships | | wp_term_taxonomy | | wp_termmeta | | wp_terms | | wp_usermeta | | wp_users | +-----------------------+ 12 rows in set (0.00 sec) mysql> quit; Bye
Add metadata labels
Labels are an optional means to apply metadata to Docker containers, images, volumes and networks. Generally, I use a minimal set of description labels for my containers but ignore the other options.
Labels use a distinctive syntax where the reverse domain name followed by the metadata name makes up the key. You can read more about this at Docker object labels but here are a couple of examples.
"com.example.description=A description for example.com" "com.devtidbits.description=A description for this blog, The Developer's Tidbits"
In the docker-compose.yml
I’ll add some labels to the containers and volumes.
version: "3.1" services: wordpress: image: wordpress labels: - "com.example.description=Example WordPress site" restart: always ports: - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: wpinternal WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage WORDPRESS_DB_NAME: myblogdb volumes: - wordpress:/var/www/html db: image: mysql:5.7 labels: - "com.example.description=Example WordPress database" restart: always environment: MYSQL_DATABASE: myblogdb MYSQL_USER: wpinternal MYSQL_PASSWORD: stencil-running-cargo-savage MYSQL_RANDOM_ROOT_PASSWORD: "1" volumes: - db:/var/lib/mysql volumes: wordpress: labels: - "com.example.description=WordPress install" db: labels: - "com.example.description=MySQL datastore"
To apply labels to the volumes, they need to be deleted and rebuilt, resulting in the loss of their data.
# Stop and remove the containers docker-compose down # Delete both the wordpress and db file volumes docker volume rm examplecom_db examplecom_wordpress # Rebuild, recreate, start and attach the container services docker-compose up # Inspect the new volumes docker volume inspect examplecom_db examplecom_wordpress
[ { "CreatedAt": "2019-10-30T02:48:25Z", "Driver": "local", "Labels": { "com.docker.compose.project": "examplecom", "com.docker.compose.version": "1.24.1", "com.docker.compose.volume": "db", "com.example.description": "MySQL datastore" }, "Mountpoint": "/var/lib/docker/volumes/examplecom_db/_data", "Name": "examplecom_db", "Options": null, "Scope": "local" }, { "CreatedAt": "2019-10-30T02:48:25Z", "Driver": "local", "Labels": { "com.docker.compose.project": "examplecom", "com.docker.compose.version": "1.24.1", "com.docker.compose.volume": "wordpress", "com.example.description": "WordPress install" }, "Mountpoint": "/var/lib/docker/volumes/examplecom_wordpress/_data", "Name": "examplecom_wordpress", "Options": null, "Scope": "local" } ]
Long-form syntax
The docker-compose.yml
sample given on the WordPress docker hub page is intentionally compact. But for readability, I like to expand these configurations using the long-form syntax that was introduced in compose format 3.2. Firstly we need to update the version of docker-compose.yml
, which is 3.1. You may as well update it to the newest version at the time of writing, 3.7.
version: "3.7"
The two configurations in our compose file we can express in long-form are ports and volumes.
Ports
ports: - 8080:80
In the long-form syntax.
ports: - target: 80 published: 8080 protocol: tcp
Volumes
The WordPress volume.
volumes: - wordpress:/var/www/html
volumes: - type: volume source: wordpress target: /var/www/html
The database volume.
volumes: - db:/var/lib/mysql
volumes: - type: volume source: db target: /var/lib/mysql
WordPress configurations – wp-config.php
One of the most important files for any WordPress installation is wp-config.php
. In this Docker container though we can embed any configurations directly into docker-compose.yml
. A complete list of these configurations is at the WordPress support article https://wordpress.org/support/article/editing-wp-config-php/.
It’s important to note that most of the required database connection configurations are irrelevant because these are in the container’s environment values. Any additional configurations can be added using the WORDPRESS_CONFIG_EXTRA
variable. For example, these extra configs may be required and set to true
in a production environment.
environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: wpinternal WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage WORDPRESS_DB_NAME: myblogdb WORDPRESS_CONFIG_EXTRA: | # Require SSL for admin and logins define( 'FORCE_SSL_ADMIN', false ); # Disable plugin and theme update and installation define( 'DISALLOW_FILE_MODS', false ); # Disable the plugin and theme editor define( 'DISALLOW_FILE_EDIT', false );
Any changes to the WORDPRESS_CONFIG_EXTRA
variable requires the deletion of the existing wp-config.php
file that is on the WordPress volume. Otherwise, the changes are not applied.
wordpress_1 | WARNING: environment variable "WORDPRESS_CONFIG_EXTRA" is set, but "wp-config.php" already exists wordpress_1 | The contents of this variable will _not_ be inserted into the existing "wp-config.php" file.
To delete an existing wp-config.php
in the container.
# Run the container in detached mode docker-compose up --detach # Run an optional test shell command on the wordpress container docker-compose exec wordpress ls # Delete the wp-config.php docker-compose exec wordpress rm wp-config.php # Restart the container to generate a new wp-config.php docker-compose down && docker-compose up --detach # View the content of the new wp-config.php docker-compose exec wordpress cat wp-config.php
The output from cat wp-config.php should reveal the following appended additions.
// WORDPRESS_CONFIG_EXTRA # Require SSL for admin and logins define( 'FORCE_SSL_ADMIN', false ); # Disable plugin and theme update and installation define( 'DISALLOW_FILE_MODS', false ); # Disable the plugin and theme editor define( 'DISALLOW_FILE_EDIT', false );
Add configurations to PHP
WordPress is a PHP: Hypertext Preprocessor application, an old open-sourced language for the web that was very popular in the late 1990s and 2000s. PHP has its methods of configuration directives in the form of php.ini files.
Fix the 2 MB WordPress upload limit
Out of the box PHP and so also WordPress has a hard 2 MB upload limit for file forms. It means any images, videos or imported data uploads have a size limit when using WordPress. We can quickly fix that by creating our PHP configuration and binding it to the WordPress container.

Create a new PHP configuration file, and name it docker-uploads.ini
. Add the following configuration then save the changes.
# Allow HTTP file uploads file_uploads = On # Maximum size of an uploaded file upload_max_filesize = 64M # Maximum size of form post data post_max_size = 64M
Update the docker-compose.yml
to bind the docker-uploads.ini
to the wordpress container and then restart the WordPress container.
volumes: - type: volume source: wordpress target: /var/www/html - type: bind source: ./docker-uploads.ini target: /usr/local/etc/php/conf.d/docker-uploads.ini
# Rebuild our container docker-compose down && docker-compose up --build
Visit the Upload New Media page to confirm the changes are active, http://localhost:8080/wp-admin/media-new.php

Add configurations to Apache
The WordPress container usages a Debian installation of Apache HTTP Server to serve the web application. This combination uses a unique directory structure for Apache’s configuration files, but thankfully we can still bind our configurations to this setup.
Fix the Apache “could not reliably determine the server’s fully qualified domain name” notice
One problem we currently have in the log files that is fixable is this warning from Apache.
wordpress_1 | apache2: Could not reliably determine the server's fully qualified domain name, using 172.21.0.3. Set the 'ServerName' directive globally to suppress this message
Create a new Apache configuration file, and name it docker-servername.conf
. Add the following configuration then save the changes.
# Hostname and port that the server uses to identify itself ServerName localhost
Update the docker-compose.yml
to bind the docker-servername.conf
to the WordPress container and then rebuild.
volumes: - type: volume source: wordpress target: /var/www/html - type: bind source: ./uploads.ini target: /usr/local/etc/php/conf.d/uploads.ini - type: bind source: ./docker-servername.conf target: /etc/apache2/conf-enabled/docker-servername.conf
# Rebuild our container docker-compose down && docker-compose up --build
The Apache warning for the fully qualified domain name is gone.
Add configurations to MySQL
Fix the MySQL “TIMESTAMP with implicit DEFAULT value is deprecated” warning
Out of the box, MySQL 5.7 throws this warning on startup. We can quickly fix this by binding our own MySQL configuration file to the DB container.
db_1 | [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
Create a new MySQL configuration file, and name it docker-fixes.cnf
. Add the following configuration then save the changes.
[mysqld] explicit_defaults_for_timestamp=on
Update the docker-compose.yml
to bind the docker-fixes.cnf
to the DB container and then restart.
volumes: - type: volume source: db target :/var/lib/mysql - type: bind source: ./docker-fixes.cnf target: /etc/mysql/conf.d/docker-fixes.cnf
docker-compose down && docker-compose up
The TIMESTAMP with implicit DEFAULT value is deprecated warning should now not appear.
Add Adminer, a web interface for MySQL
For an optional, lightweight web interface for the MySQL database, you can attach an Adminer container to the composition. It also uses port 8080, which conflicts with our WordPress container, so I’ve swapped it to port 8070.
Update the docker-compose.yml
to add the Adminer container to the composition and then restart.
services: adminer: image: adminer restart: always ports: - target: 8080 published: 8070 protocol: tcp
docker-compose down && docker-compose up
Visit http://localhost:8070 and use the username, password and database values saved to the MYSQL_DB
… environment values to sign-in.

Congratulations, now you know how to customise all the components your WordPress Docker installation. The final result for the docker-compose.yml
is shown below and is also available at https://github.com/bengarrett/devtidbits.com.
version: "3.7" services: wordpress: image: wordpress labels: - "com.example.description=Example WordPress site" restart: always ports: - target: 80 published: 8080 protocol: tcp environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: wpinternal WORDPRESS_DB_PASSWORD: stencil-running-cargo-savage WORDPRESS_DB_NAME: myblogdb WORDPRESS_CONFIG_EXTRA: | # Require SSL define( 'FORCE_SSL_ADMIN', false); # Disable plugin and theme update and installation define( 'DISALLOW_FILE_MODS', false ); # Disable the plugin and theme editor define( 'DISALLOW_FILE_EDIT', false ); volumes: - type: volume source: wordpress target: /var/www/html - type: bind source: ./docker-uploads.ini target: /usr/local/etc/php/conf.d/docker-uploads.ini - type: bind source: ./docker-servername.conf target: /etc/apache2/conf-enabled/docker-servername.conf db: image: mysql:5.7 labels: - "com.example.description=Example WordPress database" restart: always environment: MYSQL_DATABASE: myblogdb MYSQL_USER: wpinternal MYSQL_PASSWORD: stencil-running-cargo-savage MYSQL_RANDOM_ROOT_PASSWORD: "1" volumes: - type: volume source: db target: /var/lib/mysql - type: bind source: ./docker-fixes.cnf target: /etc/mysql/conf.d/docker-fixes.cnf adminer: image: adminer restart: always ports: - target: 8080 published: 8070 protocol: tcp volumes: wordpress: labels: - "com.example.description=WordPress install" db: labels: - "com.example.description=MySQL datastore"