> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arct.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-host WordPress on a VPS

> Deploy WordPress on an Ubuntu VPS with the official container image, MySQL, automatic HTTPS, backups, and secure updates.

Arct Cloud provides the unmanaged Linux VPS for this deployment. WordPress is not preinstalled or managed by Arct Cloud. You are responsible for WordPress administration, plugins, themes, security, backups, email delivery, and updates. Arct Cloud does not provide WordPress application support.

<Note>Moving an existing site rather than starting a new one? See [WordPress Migration](/migration/wordpress), which builds on this stack.</Note>

## Choose a Plan

WordPress does not publish a universal CPU, RAM, or disk minimum because resource use depends heavily on traffic, caching, themes, plugins, and media. Its [official modern software baseline](https://wordpress.org/about/requirements/) is PHP 8.3 or newer, MariaDB 10.11+ or MySQL 8.0+, and HTTPS.

| Profile                             | vCPU |  RAM |  NVMe storage | Guidance                                                        |
| ----------------------------------- | ---: | ---: | ------------: | --------------------------------------------------------------- |
| **Practical minimum**               |    1 | 2 GB |         25 GB | A small, cached site with a limited plugin set                  |
| **Recommended Arct starting point** |    2 | 4 GB | 40 GB or more | More headroom for updates, traffic spikes, and media processing |

These are Arct deployment recommendations, not official WordPress hardware requirements. Monitor memory, CPU, database size, and media growth after launch.

## Before You Begin

Prepare the following:

* A fresh Ubuntu 24.04 server
* A hostname such as `www.example.com`
* An `A` record pointing the hostname to the server's public IPv4 address
* Docker Engine with the Docker Compose plugin, installed from Docker's [official Ubuntu repository](https://docs.docker.com/engine/install/ubuntu/)
* An external SMTP provider if the site must send reliable email

The stack below uses the Docker Official Image for WordPress with PHP 8.3 and Apache, MySQL 8.4, and Caddy 2. Only Caddy publishes web ports; WordPress and MySQL remain private on the Compose network.

## Install WordPress

<Steps>
  <Step title="Deploy and Connect">
    [Deploy an Ubuntu server](/compute/virtual-machines/deploy), then [connect over SSH](/compute/virtual-machines/connect-ssh).
  </Step>

  <Step title="Create the Project and Database Secret">
    ```bash theme={null}
    sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/wordpress
    cd /opt/wordpress
    install -d -m 0700 secrets
    openssl rand -hex 32 > secrets/db_password
    chmod 600 secrets/db_password
    ```

    Keep the secret file out of source control and never paste it into a support ticket.
  </Step>

  <Step title="Create the Compose File">
    Create `/opt/wordpress/compose.yaml`:

    ```yaml compose.yaml theme={null}
    name: wordpress

    services:
      db:
        image: mysql:8.4
        restart: unless-stopped
        environment:
          MYSQL_DATABASE: wordpress
          MYSQL_USER: wordpress
          MYSQL_PASSWORD_FILE: /run/secrets/db_password
          MYSQL_RANDOM_ROOT_PASSWORD: "1"
        secrets:
          - db_password
        volumes:
          - db_data:/var/lib/mysql

      wordpress:
        image: wordpress:php8.3-apache
        restart: unless-stopped
        depends_on:
          - db
        environment:
          WORDPRESS_DB_HOST: db:3306
          WORDPRESS_DB_USER: wordpress
          WORDPRESS_DB_NAME: wordpress
          WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
          WORDPRESS_CONFIG_EXTRA: |
            define('DISALLOW_FILE_EDIT', true);
            define('FORCE_SSL_ADMIN', true);
        secrets:
          - db_password
        volumes:
          - wordpress_data:/var/www/html

      caddy:
        image: caddy:2-alpine
        restart: unless-stopped
        depends_on:
          - wordpress
        ports:
          - "80:80"
          - "443:443"
          - "443:443/udp"
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
          - caddy_data:/data
          - caddy_config:/config

    secrets:
      db_password:
        file: ./secrets/db_password

    volumes:
      db_data:
      wordpress_data:
      caddy_data:
      caddy_config:
    ```

    The image tags follow supported stable channels while pinning the PHP, MySQL, and Caddy major or minor line. Review compatibility before changing any of them.
  </Step>

  <Step title="Configure the Domain and HTTPS">
    Generate a temporary setup password hash. The command prompts without echoing the password:

    ```bash theme={null}
    sudo docker run --rm -it caddy:2-alpine caddy hash-password
    ```

    Create `/opt/wordpress/Caddyfile`, replace the example hostname, and replace `REPLACE_WITH_HASH` with the command output:

    ```caddy Caddyfile theme={null}
    www.example.com {
        basic_auth {
            setup REPLACE_WITH_HASH
        }
        encode zstd gzip
        reverse_proxy wordpress:80
    }
    ```

    Temporary Basic Authentication prevents another visitor from claiming the public WordPress installer. Caddy obtains and renews the TLS certificate automatically after DNS resolves and ports `80` and `443` are reachable. The official WordPress image recognizes the forwarded HTTPS scheme.
  </Step>

  <Step title="Start the Stack">
    ```bash theme={null}
    cd /opt/wordpress
    sudo docker compose config --quiet
    sudo docker compose pull
    sudo docker compose up -d
    sudo docker compose ps
    ```

    MySQL can take a minute to initialize on the first start. Follow progress with `sudo docker compose logs -f --tail=100` and press `Ctrl+C` after the services settle.
  </Step>

  <Step title="Complete the First Login Securely">
    Open `https://www.example.com` and complete the WordPress installer. Use a unique administrator username other than `admin`, a generated password, and a monitored email address.

    After signing in:

    * Install all offered core, theme, and plugin updates.
    * Remove unused plugins and themes.
    * Keep the built-in file editor disabled through `WORDPRESS_CONFIG_EXTRA`.
    * Enable two-factor authentication with a well-maintained plugin from the official WordPress plugin directory.
    * Configure authenticated SMTP and test password-reset email before inviting other users.

    Remove the entire `basic_auth` block from `Caddyfile`, then validate and reload Caddy:

    ```bash theme={null}
    cd /opt/wordpress
    sudo docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
    sudo docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
    ```

    Confirm a new private-browser session reaches WordPress without the temporary setup prompt.
  </Step>

  <Step title="Verify the Deployment">
    ```bash theme={null}
    curl -fsSI https://www.example.com
    cd /opt/wordpress
    sudo docker compose ps
    sudo docker compose logs --tail=50 caddy wordpress db
    ```

    Confirm the response uses HTTPS, no service is restarting, and the WordPress dashboard reports the expected site URL.
  </Step>
</Steps>

## Firewall and Port Safety

Allow SSH before enabling UFW:

```bash theme={null}
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
sudo ufw status
```

Port `443/udp` is optional and enables HTTP/3 through Caddy. Do not publish MySQL port `3306` or the WordPress container's port `80`. Docker-published ports can bypass some UFW forwarding rules; this Compose file intentionally publishes only Caddy's public ports.

## Persistent Data and Secrets

The `wordpress_data` volume contains WordPress core files, uploads, themes, and plugins. The `db_data` volume contains the MySQL database. A recoverable backup requires both at the same point in time.

The database password is stored in `/opt/wordpress/secrets/db_password` and mounted with Compose secrets. Keep `/opt/wordpress`, the backup set, and any copied configuration private. Do not add phpMyAdmin or expose the database merely for convenience.

## Back Up and Restore

Create a maintenance window and back up both the database and WordPress files. Stopping Caddy prevents new public requests while the snapshot is created:

```bash theme={null}
cd /opt/wordpress
set -o errexit -o nounset -o pipefail
backup_stamp="$(date -u +%Y%m%dT%H%M%SZ)"
backup_dir="/var/backups/wordpress"
sudo install -d -m 0700 "$backup_dir"
restart_caddy() { sudo docker compose start caddy >/dev/null; }
trap restart_caddy EXIT
sudo docker compose stop caddy
sudo docker compose exec -T db sh -c 'exec mysqldump --no-tablespaces --single-transaction --quick -uwordpress -p"$(cat /run/secrets/db_password)" wordpress' | sudo tee "$backup_dir/.database-${backup_stamp}.sql.tmp" >/dev/null
sudo docker compose exec -T wordpress tar -C /var/www/html -czf - . | sudo tee "$backup_dir/.files-${backup_stamp}.tar.gz.tmp" >/dev/null
sudo tar -C /opt/wordpress -czf "$backup_dir/.config-${backup_stamp}.tar.gz.tmp" compose.yaml Caddyfile secrets
sudo docker image inspect wordpress:php8.3-apache mysql:8.4 caddy:2-alpine --format '{{.RepoTags}} {{.RepoDigests}}' | sudo tee "$backup_dir/.images-${backup_stamp}.txt.tmp" >/dev/null
sudo test -s "$backup_dir/.database-${backup_stamp}.sql.tmp"
sudo test -s "$backup_dir/.files-${backup_stamp}.tar.gz.tmp"
sudo test -s "$backup_dir/.config-${backup_stamp}.tar.gz.tmp"
sudo test -s "$backup_dir/.images-${backup_stamp}.txt.tmp"
sudo mv "$backup_dir/.database-${backup_stamp}.sql.tmp" "$backup_dir/database-${backup_stamp}.sql"
sudo mv "$backup_dir/.files-${backup_stamp}.tar.gz.tmp" "$backup_dir/files-${backup_stamp}.tar.gz"
sudo mv "$backup_dir/.config-${backup_stamp}.tar.gz.tmp" "$backup_dir/config-${backup_stamp}.tar.gz"
sudo mv "$backup_dir/.images-${backup_stamp}.txt.tmp" "$backup_dir/images-${backup_stamp}.txt"
restart_caddy
trap - EXIT
sudo ls -lh "$backup_dir"
```

Copy the complete timestamped set to encrypted storage outside the VPS and keep several generations. The configuration archive contains the database password and must remain private; the image manifest records the exact images needed for a reproducible rollback. Test restoration on a separate deployment: restore the file archive into an empty WordPress volume, import the matching SQL dump into an empty database, then verify URLs, media, users, and plugins before changing DNS. Do not restore only the files or only the database.

Follow the [official WordPress backup guidance](https://developer.wordpress.org/advanced-administration/security/backup/) for retention and database-specific alternatives.

## Update and Roll Back Safely

WordPress in the official image is self-managing inside its persistent volume. Apply WordPress core, plugin, and theme updates from the dashboard after creating a full backup. To update the PHP/Apache, MySQL, and Caddy container images within their pinned channels:

```bash theme={null}
cd /opt/wordpress
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=100 wordpress db caddy
```

Read WordPress release notes and plugin compatibility information before major changes. Keep MySQL on its current major line unless you have reviewed the database vendor's upgrade path. A safe rollback restores the matching pre-update database and files together and pins the Compose images to the digests recorded in that backup set; replacing only the container image cannot reverse a WordPress database migration.

## Troubleshooting

| Symptom                                                    | Check                                                                                                                                              |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Caddy cannot issue a certificate                           | Verify the `A` record, remove a stale `AAAA` record, confirm ports `80` and `443`, and check `docker compose logs caddy`.                          |
| WordPress shows “Error establishing a database connection” | Wait for first-time MySQL initialization, then check `db` logs, the secret mount, database name, and free disk space.                              |
| The browser reports a redirect loop                        | Confirm the public URL is HTTPS and Caddy proxies directly to `wordpress:80`; do not add a second TLS proxy without configuring forwarded headers. |
| Uploads or updates fail                                    | Check free disk space and permissions in the `wordpress_data` volume, then review the WordPress container logs.                                    |
| Email never arrives                                        | The container does not provide reliable outbound mail by itself; configure authenticated SMTP and inspect the SMTP provider's delivery logs.       |
| The site is slow                                           | Check CPU, RAM, database queries, image sizes, plugin count, page caching, and external API calls before resizing.                                 |

## Official Resources

<CardGroup cols={2}>
  <Card title="WordPress Requirements" icon="list-check" href="https://wordpress.org/about/requirements/">
    Current PHP, database, web server, and HTTPS requirements.
  </Card>

  <Card title="WordPress Docker Image" icon="docker" href="https://hub.docker.com/_/wordpress">
    Official image tags, Compose example, secrets, and reverse-proxy behavior.
  </Card>

  <Card title="WordPress Documentation" icon="book-open" href="https://wordpress.org/documentation/">
    Installation, administration, updates, and troubleshooting guidance.
  </Card>

  <Card title="WordPress Releases" icon="clock-rotate-left" href="https://wordpress.org/download/releases/">
    Official release archive and version history.
  </Card>

  <Card title="WordPress Development Repository" icon="github" href="https://github.com/WordPress/wordpress-develop">
    Upstream development source, tests, and issue references.
  </Card>
</CardGroup>

<Note>WordPress is a trademark of the WordPress Foundation. Arct Cloud is an independent infrastructure provider and is not affiliated with, sponsored by, or endorsed by the WordPress Foundation or the WordPress project.</Note>
