Skip to content

TutorialsCMS & websites

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

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Create the project folder and secrets
  3. Step 2 — Write the Compose file
  4. Step 3 — Start WordPress
  5. Step 4 — Publish WordPress over HTTPS with Caddy
  6. Step 5 — Run the WordPress installer
  7. Step 6 — Use WP-CLI from the official cli image
  8. Back up and restore
  9. Update WordPress
  10. Troubleshooting
  11. ERR_TOO_MANY_REDIRECTS
  12. Error establishing a database connection
  13. The uploaded file exceeds the upload_max_filesize directive in php.ini.
  14. Installation failed: Could not create directory
  15. WordPress does not send emails
  16. 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

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:

ResourceMinimum (official)Suggested starting point
CPUNot published1 vCPU
RAMNot published2 GB
DiskNot published20 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:

Bash
sudo mkdir -p /opt/wordpress
sudo chown $USER:$USER /opt/wordpress
cd /opt/wordpress

Write the database settings and two random passwords into .env. Docker Compose reads the file automatically:

Bash
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 .env

You 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:

Bash
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
EOF

Step 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:

YAML
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-apache is the Apache variant on PHP 8.3, which is also what latest points to today. Naming the PHP line makes a PHP upgrade a deliberate change; the image also comes with php8.4 and php8.5 variants and as a PHP-FPM image for setups with a separate web server.
  • WORDPRESS_CONFIG_EXTRA is evaluated inside wp-config.php. Here it disables the built-in theme and plugin file editor, a hardening step that WordPress recommends.
  • mariadb:lts follows MariaDB's current long-term support series. MARIADB_AUTO_UPGRADE runs mariadb-upgrade when 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 wpcli service belongs to the cli profile, so docker compose up does not start it; you call it with docker compose run when 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:

Bash
cd /opt/wordpress
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8080

db should show healthy, and curl returns a 302 redirect to /wp-admin/install.php. Confirm that the PHP settings were loaded:

Bash
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:

Caddyfile
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:

Bash
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.com

The 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:

Bash
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 siteurl

The 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:

Bash
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:

Bash
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:

Bash
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 --all

The images bring PHP, Apache, system libraries and MariaDB. Back up first, then pull and recreate:

Bash
cd /opt/wordpress
docker compose pull
docker compose up -d
docker compose ps

Pulling 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

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

Generer adgangskode

Please confirm