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

Difficulty: Beginner\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

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](/guides/install-wordpress-lemp).

## 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](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-debian).
- A non-root user with `sudo` rights and SSH key login, prepared as in [Secure a new Linux server](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- A domain such as `example.com` with A (and AAAA) records for `example.com` and `www.example.com` pointing at the server.
- Caddy on the host, installed with [Caddy reverse proxy](/guides/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:

```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](/guides/nginx-reverse-proxy-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.

> **Note**
>
> 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: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

## Next steps

- Prefer running WordPress directly on the server? See [WordPress with Nginx, PHP-FPM and MariaDB](/guides/install-wordpress-lemp).
- Learn the Compose file format in [Docker Compose basics](/guides/docker-compose-basics).
- Compare plans for WordPress sites on the [WordPress hosting](/wordpress-web-hosting) page.
- Read the official [wordpress image documentation](https://github.com/docker-library/docs/blob/master/wordpress/content.md) 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.

---

Source: <https://hyperdc.com/guides/tutorials/install-wordpress-docker>\
Updated: 2026-10-09
