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

# WordPress Migration

> Move an existing WordPress site onto an Arct Cloud server.

This guide moves a live WordPress site onto the Compose stack from [Self-host WordPress on a VPS](/apps/wordpress). Read the [migration overview](/migration/overview) first.

You need two things from the old host: the WordPress files under the document root, and a dump of the database. Everything else is rebuilt on the new server.

<Note>Arct Cloud provides the unmanaged Linux VPS. WordPress administration, plugins, themes, and updates remain yours. Arct Cloud does not provide WordPress application support.</Note>

## Export from the Old Host

<Steps>
  <Step title="Put the Site in Maintenance Mode">
    Anything a visitor writes after you take the dump is lost. Enable maintenance mode, or take the export during your quietest hour and repeat it at cutover.
  </Step>

  <Step title="Dump the Database">
    Read the database name, user, and password out of `wp-config.php`, then:

    ```bash theme={null}
    mysqldump --single-transaction --quick --no-tablespaces \
      -u WP_DB_USER -p WP_DB_NAME > ~/wordpress-db.sql
    ```

    On shared hosting without shell access, export the database from phpMyAdmin instead: select the database, choose **Export**, keep the **Quick** method and **SQL** format.
  </Step>

  <Step title="Archive the Files">
    From the document root:

    ```bash theme={null}
    tar -czf ~/wordpress-files.tar.gz -C /path/to/public_html .
    ```

    The archive must contain `wp-content/` in full. Core files are replaced by the container image, but your uploads, themes, plugins, and any `mu-plugins` live only here.
  </Step>

  <Step title="Note the Site URL">
    ```bash theme={null}
    grep -E "siteurl|home" ~/wordpress-db.sql | head
    ```

    Write down the exact scheme and host currently stored. You need it in the search and replace step below.
  </Step>
</Steps>

## Stand Up the Target Stack

Follow [Self-host WordPress on a VPS](/apps/wordpress) through **Start the Stack**, with two changes:

1. **Do not complete the WordPress installer.** You are importing a database, so a fresh install would be overwritten anyway.
2. **Stage the site with an internal certificate.** DNS still points at the old host, so Caddy cannot complete an ACME challenge for your domain yet. Add `tls internal` to the site block in `/opt/wordpress/Caddyfile`:

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

Caddy then issues a certificate from its own local authority. Your browser will warn that it is untrusted, which is expected while staging. You remove the line after cutover.

## Add a WP-CLI Service

The search and replace step needs WP-CLI against the same volume and database. Add this service to `/opt/wordpress/compose.yaml`, alongside the existing ones:

```yaml compose.yaml theme={null}
  wpcli:
    image: wordpress:cli-php8.3
    profiles: ["tools"]
    depends_on:
      - db
    user: "33:33"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - wordpress_data:/var/www/html
```

The `tools` profile keeps it out of `docker compose up`. It runs only when you ask for it by name.

## Import

<Steps>
  <Step title="Copy the Export Across">
    From the Arct Cloud server:

    ```bash theme={null}
    scp OLD_USER@SOURCE_IP:~/wordpress-db.sql ~/
    scp OLD_USER@SOURCE_IP:~/wordpress-files.tar.gz ~/
    ```
  </Step>

  <Step title="Restore wp-content">
    Unpack only `wp-content` into the running container. Core files come from the image and should not be overwritten by the old host's copies:

    ```bash theme={null}
    cd /opt/wordpress
    sudo tar -xzf ~/wordpress-files.tar.gz -C /tmp ./wp-content
    sudo docker compose cp /tmp/wp-content wordpress:/var/www/html/
    sudo docker compose exec wordpress chown -R www-data:www-data /var/www/html/wp-content
    ```
  </Step>

  <Step title="Import the Database">
    ```bash theme={null}
    cd /opt/wordpress
    sudo docker compose exec -T db sh -c \
      'exec mysql -uwordpress -p"$(cat /run/secrets/db_password)" wordpress' < ~/wordpress-db.sql
    ```

    The dump carries its own table prefix. If the old site used a prefix other than `wp_`, add the matching `$table_prefix` line to the WordPress config through `WORDPRESS_CONFIG_EXTRA` in `compose.yaml`, then recreate the container.
  </Step>

  <Step title="Update the Site URL">
    Only if the domain or scheme changed. Serialized values are stored with byte-length prefixes, so a plain `sed` over the dump corrupts widget and plugin settings. WP-CLI rewrites them correctly:

    ```bash theme={null}
    cd /opt/wordpress
    sudo docker compose run --rm wpcli wp search-replace \
      'http://old.example.com' 'https://www.example.com' \
      --skip-columns=guid --report-changed-only
    ```

    Leave `guid` alone. Feed readers key off it, and rewriting it makes every post look new.
  </Step>

  <Step title="Flush Permalinks and Caches">
    ```bash theme={null}
    cd /opt/wordpress
    sudo docker compose run --rm wpcli wp rewrite flush
    sudo docker compose run --rm wpcli wp cache flush
    ```
  </Step>
</Steps>

## Verify Before DNS

Add the new server's IP to your own machine's hosts file, `/etc/hosts` on Linux and macOS or `C:\Windows\System32\drivers\etc\hosts` on Windows:

```
NEW_SERVER_IP www.example.com
```

Load the site, accept the staging certificate warning, and clear the temporary Basic Authentication prompt. Then check:

* The homepage, a post, a category archive, and a page all load
* Media in `wp-content/uploads` resolves rather than 404s
* The admin dashboard signs in and **Settings** shows the expected site address
* Permalinks work on a deep URL, not only the homepage
* Contact forms, search, and any commerce checkout still function
* **Tools** > **Site Health** reports no new critical issues

Remove the hosts entry when you are done.

## Cut Over

<Steps>
  <Step title="Take a Final Export">
    Repeat the database dump and the `wp-content` archive from the old host, then re-import. Comments, orders, and posts written since your first export live only there.
  </Step>

  <Step title="Update DNS">
    Point the `A` and `AAAA` records at the new server. See [Website and DNS Migration](/migration/website-dns) for TTL handling and propagation checks.
  </Step>

  <Step title="Issue the Real Certificate">
    Once DNS resolves to the new server, remove both `tls internal` and the `basic_auth` block from `/opt/wordpress/Caddyfile`, then reload:

    ```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
    curl -fsSI https://www.example.com
    ```

    Caddy completes the ACME challenge and replaces the staging certificate.
  </Step>

  <Step title="Set Up Backups">
    Follow **Back Up and Restore** in the [WordPress deployment guide](/apps/wordpress). The old host's backup schedule does not follow the site.
  </Step>

  <Step title="Decommission the Old Host">
    Keep it for a few days, then run the [checks before you delete it](/migration/overview#before-you-delete-the-old-server).
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Error establishing a database connection">
    The import ran but the table prefix does not match. Check with `sudo docker compose run --rm wpcli wp db tables`, then set `$table_prefix` through `WORDPRESS_CONFIG_EXTRA` to match what the dump created.
  </Accordion>

  <Accordion title="The site redirects to the old domain">
    `siteurl` and `home` still hold the old value. Confirm with `wp option get siteurl`, then re-run the search and replace step.
  </Accordion>

  <Accordion title="Homepage loads but every other URL returns 404">
    Permalink rules were not rebuilt. Run `wp rewrite flush`. The Compose stack proxies through Caddy to Apache, so `.htaccess` from the old host still applies inside the container.
  </Accordion>

  <Accordion title="Images are missing or uploads fail">
    File ownership did not survive the copy. Re-run `chown -R www-data:www-data /var/www/html/wp-content` inside the WordPress container, then check free disk space with `df -h`.
  </Accordion>

  <Accordion title="A caching or security plugin breaks the site after import">
    Plugins that write server-level config expect the old host's stack. Disable them with `wp plugin deactivate PLUGIN_NAME`, load the site, then reconfigure them for a containerized Apache behind a reverse proxy.
  </Accordion>

  <Accordion title="Mixed content warnings after moving to HTTPS">
    The old site stored `http://` URLs in post content. Re-run the search and replace with the `http://` to `https://` pair for the same host.
  </Accordion>
</AccordionGroup>
