How to install WordPress with Docker Compose and HTTPS
Run WordPress with the official wordpress and mariadb Docker images, keep secrets in .env, publish it through Caddy with HTTPS and back up database and files.
- Beginner
- 30 min read
- Updated
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13
On this page
- Prerequisites
- Step 1 — Create the project folder and secrets
- Step 2 — Write the Compose file
- Step 3 — Start WordPress
- Step 4 — Publish WordPress over HTTPS with Caddy
- Step 5 — Run the WordPress installer
- Step 6 — Use WP-CLI from the official cli image
- Back up and restore
- Update WordPress
- Troubleshooting
- ERR_TOO_MANY_REDIRECTS
- Error establishing a database connection
- The uploaded file exceeds the upload_max_filesize directive in php.ini.
- Installation failed: Could not create directory
- WordPress does not send emails
- Next steps
WordPress is an open-source content management system for blogs, company sites and shops. Running it in Docker keeps PHP, Apache and the database in versioned images, makes the setup reproducible on another server and lets you run several isolated sites side by side.
This guide uses the official wordpress and mariadb images from Docker Hub with Docker Compose. Secrets live in a .env file, data in named volumes, and WordPress is published only on 127.0.0.1 behind Caddy, which provides HTTPS. You also raise the PHP upload limits, run WP-CLI from the official cli image, and set up backups, restores and updates. If you prefer a classic installation without containers, see WordPress on Nginx, PHP-FPM and MariaDB.
Prerequisites
- A server running Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12 or Debian 13 with Docker Engine and the Compose plugin: see Install Docker on Ubuntu or Install Docker on Debian.
- A non-root user with
sudorights and SSH key login, prepared as in Secure a new Linux server and Set up SSH keys. - A domain such as
example.comwith A (and AAAA) records forexample.comandwww.example.compointing at the server. - Caddy on the host, installed with Caddy reverse proxy.
WordPress recommends PHP 8.3 or newer and MariaDB 10.11 or newer; the images in this guide provide both. WordPress does not publish CPU or memory minimums, so treat these figures as a conservative starting point for one site:
| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 1 vCPU |
| RAM | Not published | 2 GB |
| Disk | Not published | 20 GB SSD plus your media library |
Step 1 — Create the project folder and secrets
Keep the site in /opt/wordpress, owned by your admin user:
sudo mkdir -p /opt/wordpress
sudo chown $USER:$USER /opt/wordpress
cd /opt/wordpressWrite the database settings and two random passwords into .env. Docker Compose reads the file automatically:
cat > .env <<EOF
WORDPRESS_DB_NAME=wordpress
WORDPRESS_DB_USER=wordpress
WORDPRESS_DB_PASSWORD=$(openssl rand -hex 24)
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 24)
EOF
chmod 600 .envYou do not need to generate WordPress keys and salts: when the image creates wp-config.php from WORDPRESS_* variables, it fills them with unique random values.
Raise PHP's upload and memory limits with a small ini file. The official image documentation explains that files in PHP's conf.d directory are loaded automatically; Step 2 mounts this one there:
mkdir -p /opt/wordpress/php
cat > /opt/wordpress/php/uploads.ini <<'EOF'
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 120
EOFStep 2 — Write the Compose file
Create /opt/wordpress/compose.yaml. It extends the example from the image documentation with a MariaDB health check, a loopback-only port, the PHP settings file and an optional WP-CLI service:
services:
wordpress:
image: wordpress:php8.3-apache
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
WORDPRESS_CONFIG_EXTRA: |
define( 'DISALLOW_FILE_EDIT', true );
volumes:
- wordpress:/var/www/html
- ./php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
db:
image: mariadb:lts
restart: unless-stopped
environment:
MARIADB_DATABASE: ${WORDPRESS_DB_NAME}
MARIADB_USER: ${WORDPRESS_DB_USER}
MARIADB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_AUTO_UPGRADE: "1"
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db:/var/lib/mysql
wpcli:
image: wordpress:cli-php8.3
profiles: ["cli"]
user: "33:33"
environment:
HOME: /tmp
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
volumes:
- wordpress:/var/www/html
depends_on:
- db
volumes:
wordpress:
db:What the choices mean:
wordpress:php8.3-apacheis the Apache variant on PHP 8.3, which is also whatlatestpoints to today. Naming the PHP line makes a PHP upgrade a deliberate change; the image also comes withphp8.4andphp8.5variants and as a PHP-FPM image for setups with a separate web server.WORDPRESS_CONFIG_EXTRAis evaluated insidewp-config.php. Here it disables the built-in theme and plugin file editor, a hardening step that WordPress recommends.mariadb:ltsfollows MariaDB's current long-term support series.MARIADB_AUTO_UPGRADErunsmariadb-upgradewhen a newer server version starts on existing data, and keeps a backup of the system tables first.- The MariaDB container creates the database and user from the
MARIADB_*variables on first start only. Later changes to these variables have no effect on existing data. - The
wpcliservice belongs to thecliprofile, sodocker compose updoes not start it; you call it withdocker compose runwhen you need it.
Step 3 — Start WordPress
Start the stack, wait for the database to report healthy and check that WordPress answers on the loopback port:
cd /opt/wordpress
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8080db should show healthy, and curl returns a 302 redirect to /wp-admin/install.php. Confirm that the PHP settings were loaded:
docker compose exec wordpress php -i | grep -E 'upload_max_filesize|post_max_size'Both values should read 64M. Do not open the installer through 127.0.0.1:8080: WordPress stores the address you install from as the site URL.
Step 4 — Publish WordPress over HTTPS with Caddy
Add the site to /etc/caddy/Caddyfile. The second block redirects www to the bare domain; swap the two names if you prefer www.example.com as the main address:
example.com {
reverse_proxy 127.0.0.1:8080
}
www.example.com {
redir https://example.com{uri} permanent
}Reload Caddy, allow only SSH and web traffic, and test:
sudo systemctl reload caddy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
curl -I https://example.comThe response is again a redirect to the installer, now over HTTPS with a valid certificate. HTTPS detection needs no extra work: Caddy sends X-Forwarded-Proto: https, and because you configured the container with WORDPRESS_* variables, the image adds the matching HTTP_X_FORWARDED_PROTO check to wp-config.php. The image also trusts X-Forwarded-For from private and loopback addresses, so WordPress sees real visitor IP addresses. With Nginx instead of Caddy, set proxy_set_header X-Forwarded-Proto $scheme; yourself; see Nginx with Certbot.
Step 5 — Run the WordPress installer
Open https://example.com, choose the language and enter the site title, an administrator username, a strong password and your email address. Avoid admin and other obvious usernames, and tick "Discourage search engines" only on a staging site. After logging in, open Settings → Permalinks, select Post name and save; the Apache image includes the rewrite module, so pretty URLs work immediately.
Step 6 — Use WP-CLI from the official cli image
WP-CLI runs in a short-lived container that shares the WordPress volume and the database settings. The cli images are based on Alpine, where www-data has UID 82, while the Debian-based Apache image uses UID 33; the image documentation therefore recommends running WP-CLI as 33:33 with HOME=/tmp, which the Compose file already does:
cd /opt/wordpress
docker compose run --rm wpcli wp core version
docker compose run --rm wpcli wp plugin list
docker compose run --rm wpcli wp option get siteurlThe last command must print https://example.com. Use the same pattern for any WP-CLI command, for example wp user list or wp search-replace.
Back up and restore
The site lives in two named volumes, wordpress (core, themes, plugins, uploads, wp-config.php) and db (MariaDB data), plus the files in /opt/wordpress. Compose prefixes volume names with the project folder, so on disk they are wordpress_wordpress and wordpress_db; check with docker volume ls. Dump the database with MariaDB's own tool through docker compose exec and archive the files volume with a temporary container:
sudo mkdir -p /opt/backups
sudo chown $USER:$USER /opt/backups
cd /opt/wordpress
docker compose exec -T db sh -c 'exec mariadb-dump --single-transaction -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' | gzip > /opt/backups/wordpress-db-$(date +%F).sql.gz
docker run --rm -v wordpress_wordpress:/data:ro -v /opt/backups:/backup alpine tar czf /backup/wordpress-files-$(date +%F).tar.gz -C /data .
tar czf /opt/backups/wordpress-config-$(date +%F).tar.gz -C /opt/wordpress .env compose.yaml php
chmod 600 /opt/backups/wordpress-*If you only want the content without the core files, archive just the wp-content subfolder of the volume instead of .. Copy /opt/backups off the server regularly, because a backup on the same machine is lost together with it.
To restore on a new server, install Docker, unpack the config archive into /opt/wordpress and run:
cd /opt/wordpress
docker compose up -d --wait db
gunzip -c /opt/backups/wordpress-db-2026-10-09.sql.gz | docker compose exec -T db sh -c 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"'
docker compose create wordpress
docker run --rm -v wordpress_wordpress:/data -v /opt/backups:/backup alpine tar xzf /backup/wordpress-files-2026-10-09.tar.gz -C /data
docker compose up -d--wait returns once MariaDB is healthy, and docker compose create creates the files volume without starting WordPress, so the image does not copy a fresh core into it first. Replace the dates with those in your file names, then add the Caddy site block from Step 4 on the new server.
Update WordPress
There are two layers to keep current. WordPress core, plugins and themes live in the volume and update the same way as on any WordPress site: minor and security releases install automatically, and Dashboard → Updates handles the rest. You can also update from the shell:
cd /opt/wordpress
docker compose run --rm wpcli wp core update
docker compose run --rm wpcli wp core update-db
docker compose run --rm wpcli wp plugin update --allThe images bring PHP, Apache, system libraries and MariaDB. Back up first, then pull and recreate:
cd /opt/wordpress
docker compose pull
docker compose up -d
docker compose psPulling a new wordpress image does not change the WordPress version in the volume. To move to a newer PHP line, change both php8.3 tags in compose.yaml after checking that your plugins support it. Read the WordPress release notes before major WordPress updates, and test big changes on a copy of the site first.
Troubleshooting
ERR_TOO_MANY_REDIRECTS
WordPress believes the request arrived over HTTP and keeps redirecting to HTTPS. Make sure your proxy sends X-Forwarded-Proto (Caddy does by default; Nginx needs proxy_set_header X-Forwarded-Proto $scheme;) and that a CDN in front of the server does not connect to it over plain HTTP. If the site URL was stored with the wrong address, fix it with docker compose run --rm wpcli wp option update home 'https://example.com' and the same command for siteurl.
Error establishing a database connection
Check docker compose ps: the db service must be healthy. If you changed the passwords in .env after the first start, MariaDB still uses the old ones, because it only reads the MARIADB_* variables when the data directory is empty. Put the original values back, or change the user's password inside MariaDB with ALTER USER.
The uploaded file exceeds the upload_max_filesize directive in php.ini.
The PHP settings file is not mounted or has the wrong path. Check that /opt/wordpress/php/uploads.ini exists, that the volume line in compose.yaml targets /usr/local/etc/php/conf.d/uploads.ini, and run docker compose up -d to recreate the container. A proxy in front of Caddy may enforce its own body size limit.
Installation failed: Could not create directory
The files in the volume do not belong to www-data, often after a restore or after running WP-CLI as root. Fix the ownership with docker compose exec wordpress chown -R www-data:www-data /var/www/html and always run WP-CLI through the wpcli service.
WordPress does not send emails
The official image does not contain a mail server, so PHP's mail() function cannot deliver messages. Install an SMTP plugin and connect it to a transactional email provider with host smtp.example.com, port 587 and STARTTLS; then test with a password reset.
Next steps
- Prefer running WordPress directly on the server? See WordPress with Nginx, PHP-FPM and MariaDB.
- Learn the Compose file format in Docker Compose basics.
- Compare plans for WordPress sites on the WordPress hosting page.
- Read the official wordpress image documentation for all variables and variants.
Frequently asked questions
Do I need extra settings for HTTPS behind a reverse proxy?
Usually not. The official wordpress image adds the HTTP_X_FORWARDED_PROTO check to wp-config.php automatically when you configure it with WORDPRESS_ environment variables, and Caddy sends the X-Forwarded-Proto header by default. Install WordPress through the HTTPS address so the site URL is stored correctly.
Does pulling a new wordpress image update WordPress itself?
No. The image copies WordPress into the volume only on first start; after that WordPress updates itself inside the volume through its normal update mechanism. Pulling a new image updates PHP, Apache and the system libraries.
Why does the WP-CLI container run as user 33?
The cli image is based on Alpine, where www-data has UID 82, while the Apache image uses Debian's www-data with UID 33. Running WP-CLI as 33:33 keeps file ownership in the shared volume correct, as the image documentation recommends.
Can WordPress in Docker send email?
Yes, through an SMTP plugin, because the official image includes no mail transfer agent for PHP's mail function. Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request through a support ticket. Until then, send mail through an SMTP relay on port 587.
Can I run this on a HyperDC server?
Yes. The guide works on a HyperDC Linux VPS, VDS or dedicated server with root access running Ubuntu 24.04, Ubuntu 26.04, Debian 12 or Debian 13 with Docker installed. Size the server for your traffic and media.
Sources
- github.com/docker-library/docs/blob/master/wordpress/content.md
- github.com/docker-library/docs/blob/master/wordpress/README.md
- github.com/docker-library/docs/blob/master/wordpress/compose.yaml
- github.com/docker-library/wordpress/blob/master/wp-config-docker.php
- github.com/docker-library/wordpress/blob/master/latest/php8.3/apach…
- github.com/docker-library/docs/blob/master/mariadb/README.md
- wordpress.org/about/requirements
- developer.wordpress.org/advanced-administration/security/https
- developer.wordpress.org/advanced-administration/wordpress/wp-config
- caddyserver.com/docs/caddyfile/directives/reverse_proxy