Skip to content

Wordpress

Note: this file is copied from the Daps wordpress template’s README.md, located in <daps repository>/templates/wordpress.

A template for running a self-hosted WordPress site in containers managed by DAPS.

Includes Redis object caching and PhpMyAdmin (local dev only).


Before you start: replace the placeholder name

Section titled “Before you start: replace the placeholder name”

This template uses mywpsite as a placeholder throughout. There are two ways to set up a project from this template:

From the DAPS root folder:

Terminal window
dapsman init --template wordpress --name yourprojectname --prod-url yourdomain.com

This copies the template, replaces all mywpsite placeholders with your project name (in file contents and filenames), removes any inherited secrets, writes your production domain into _caddy_sites/yourprojectname.prod.caddy, and registers the project in daps.yaml automatically. --prod-url is optional — if omitted, edit the caddy file manually before deploying.

Do a find-and-replace across the whole folder:

ReplaceWith
mywpsiteyour project name (e.g. janesdrum)
mywpsite_wpyour database name (e.g. janesdrum_wp)
mywpsite.localhostyour local dev hostname (e.g. janesdrum.localhost)
mywpsite.example.comyour real domain name (e.g. janesdrum.com)
mywpsite-prodyour OpenStack server name (e.g. janesdrum-prod)

Files to update (rename the files too):

  • _docker/compose_mywpsite.yaml
  • _docker/compose_mywpsite.dev.yaml
  • _docker/compose_mywpsite.prod.yaml
  • _caddy_sites/mywpsite.dev.caddy
  • _caddy_sites/mywpsite.prod.caddy

Then add an entry to the root daps.yaml:

projects:
mywpsite: # <-- your project name
path: ../daps-wp # <-- path to this folder

Secrets (passwords and WordPress security keys) are generated automatically by dapsman local build the first time you run it. They are written to _secrets/, which is gitignored.

To generate them manually, or to regenerate them (e.g. after a security incident), run:

Terminal window
./_scripts/prerequisites.sh # skip existing secrets
./_scripts/prerequisites.sh --force # regenerate all secrets

Never commit the _secrets/ folder or share its contents.

dapsman local build runs prerequisites.dev.sh automatically, which will attempt to add the hosts entry for you:

  • Windows: requires an elevated terminal (right-click → Run as administrator). If not elevated, a warning is printed and you can add it manually.
  • Mac/Linux: requires sudo. If credentials aren’t cached, a warning is printed with the manual command.
  • Chrome and Firefox resolve *.localhost automatically without a hosts entry, so this step is only strictly needed for other tools (curl, wp-cli, etc.).

If you need to add it manually:

# Windows: C:\Windows\System32\drivers\etc\hosts
# Mac/Linux: /etc/hosts
127.0.0.1 mywpsite.localhost

From the DAPS root folder:

dapsman local build --project mywpsite

Or run docker compose directly from this project folder:

docker compose -f _docker/compose_mywpsite.yaml -f _docker/compose_mywpsite.dev.yaml up -d

Your site will be available at https://mywpsite.localhost (via Caddy) or http://localhost:8080 (direct). PhpMyAdmin is available at http://localhost:8082.

Note that it can take a few minutes for the db server to finish booting up. If you see “Error establishing a database connection” right after running the local build the first time, this could be why. If it persists beyond a few minutes, however, then something else is wrong.

You may also see warnings in WP Admin about “Action Scheduler” actions being past due (specifically wp_mail_smtp_admin_notifications_update and action_scheduler_run_recurring_actions_schedule_hook). This is normal in a local dev environment — WP Mail SMTP checks for admin notifications on a schedule, and without real SMTP credentials configured locally, those checks fail and queue up. This does not affect site functionality and will not cause problems in prod.

Open your site in a browser. WordPress will walk you through choosing a title, admin username, and password.

5. Reset the local admin password (if needed)

Section titled “5. Reset the local admin password (if needed)”

After a database restore the admin password may not match what you expect (e.g. it may still be the production password, or the hash may not verify correctly in the new environment — honestly we — me and Claude — don’t know why but it happens). Reset it via wp-cli from inside the toolkit container:

Terminal window
# open a shell in the toolkit container
docker exec -it daps-toolkit-1 bash -l
# list users to find the username
docker exec mywpsite-wordpress-1 wp user list --allow-root
# reset the password
docker exec mywpsite-wordpress-1 wp user update <username> --user_pass=<newpassword> --allow-root

In WP Admin, go to Plugins > Add New, search for Redis Object Cache, install and activate it. Then go to Settings > Redis and click Enable Object Cache.


Before you can deploy to production you need to define and provision a host in Daps. That process is covered in the Daps readme deployment document (daps/docs/readme-deployment.md).

If multiple hosts are defined in the daps.yaml file, then the project entry may need a provider entry referencing the target host, e.g.:

mywpsite:
path: ../mywpsite
provider: ovhcloud

If you omit this then Daps will assume the target is the first host in the list.

Before you deploy, confirm _caddy_sites/mywpsite.prod.caddy names your real domain, not the mywpsite.example.com placeholder.

Caddy requests an HTTPS certificate for that domain as soon as it starts, and Let’s Encrypt only issues one if the domain already points at your server. So for a new site, create the A record before deploying; when moving a live site, deploy first and flip DNS after, to keep downtime short.

Read DNS and HTTPS in daps/docs/readme-deployment.md before you pick — it covers TTLs, what to do when a certificate doesn’t arrive, and why the hosts-file trick for previewing the new server doesn’t work.

To deploy, execute this command:

Terminal window
dapsman prod deploy --project mywpsite

A note about secrets: prod generates its own secrets on the remote server — they are never copied from your local _secrets/ folder, so the two environments have different passwords and keys. Deploy only creates secrets that don’t already exist on the remote, so re-deploying an existing site leaves them untouched.

dapsman prod deploy will create your Wordpress containers on the remote site. If you set up and customized Wordpress locally before deploying to prod, you’ll want to get your files and data there as well. Do that by running:

Terminal window
dapsman prod sync-from-local --project mywpsite

If you chose to update DNS after deploying, point your A record at the host’s IP address now. Once it propagates, run dapsman prod caddy restart if HTTPS isn’t working yet.

Your site should then load, looking exactly as it does running locally.


From the DAPS root folder:

Terminal window
dapsman prod backup --project mywpsite --provider <name>

This runs _scripts/backup-remote.toolkit.sh from inside the toolkit container. Backups are saved to _backups/from_prod/ inside the project folder. See the script for what is backed up (database dump and wp-content/uploads by default).

To snapshot the local site instead — before a major upgrade, say, or a change you might want to undo:

Terminal window
dapsman local backup --project mywpsite

This runs _scripts/backup-local.toolkit.sh and saves a timestamped database dump and wp-content archive to _backups/from_local/. The site must be running. Restore any snapshot, from either folder, with dapsman local restore --project mywpsite --list-restore-points.


mywpsite/
├── _caddy_sites/ # Caddy reverse proxy configs (imported by DAPS)
│ ├── mywpsite.dev.caddy
│ └── mywpsite.prod.caddy
├── _docker/ # Docker Compose files
│ ├── compose_mywpsite.yaml # Base config (shared dev + prod)
│ ├── compose_mywpsite.dev.yaml # Dev overrides (ports, PhpMyAdmin, volume mounts)
│ ├── compose_mywpsite.prod.yaml # Prod overrides (volume paths, Redis, site URL)
│ └── compose_daps_mywpsite.dev.yaml # Toolkit extension (mounts project into toolkit)
├── _scripts/ # Utility scripts
│ ├── prerequisites.sh # Secret generation (run automatically by dapsman)
│ ├── prerequisites.dev.sh # Dev-only setup: secrets + hosts entry (run automatically by dapsman)
│ ├── backup-remote.toolkit.sh # Pulls a backup down from prod (run from toolkit container)
│ └── backup-local.toolkit.sh # Snapshots the local site (run from toolkit container)
├── _secrets/ # Generated secrets — gitignored, never commit
├── _backups/ # Backup archive — gitignored
│ ├── from_prod/ # Pulled down by `dapsman prod backup`
│ └── from_local/ # Snapshots taken by `dapsman local backup`
├── wp-content/ # WordPress content
│ ├── themes/ # Tracked in git — customise your theme here
│ ├── uploads/ # Gitignored — managed on server, backed up separately
│ └── plugins/ # Gitignored — reinstall via WP Admin or backup restore

ServiceDev portDescription
wordpress8080WordPress (Apache + PHP 8.3)
db3306MySQL 8.0 database
redisRedis 7 object cache
phpmyadmin8082Database browser (dev only)

The template builds a custom image (_docker/wordpress.dockerfile) that extends the official WordPress image with wp-cli baked in. wp-cli is used in the sync and restore scripts for URL rewriting and Redis cache flushing after database imports.

The image must be built before the first remote deploy. dapsman prod deploy handles this via _scripts/build-docker-images.toolkit.sh, which builds the image inside the toolkit container and exports it as a tar to _docker/image-exports/ for upload to the remote server.


10 secret files live in _secrets/ (gitignored). All are generated by prerequisites.sh:

FileUsed by
db_password.txtMySQL user password
mysql_root_password.txtMySQL root password
auth_key.txtWordPress security key
secure_auth_key.txtWordPress security key
logged_in_key.txtWordPress security key
nonce_key.txtWordPress security key
auth_salt.txtWordPress security salt
secure_auth_salt.txtWordPress security salt
logged_in_salt.txtWordPress security salt
nonce_salt.txtWordPress security salt

Secrets are bind-mounted as files into containers at /run/secrets/ and must be chmod 644 so www-data inside the WordPress container can read them.


wp-content/ contains themes, plugins, and uploads. It is handled differently per environment:

  • Dev: bind-mounted from ../wp-content so theme and plugin edits are reflected immediately without rebuilding the container.
  • Prod: lives at /srv/projects/mywpsite/wp-content on the VM and is bind-mounted from there. This path persists across deploys.

themes/ is tracked in git. plugins/ and uploads/ are gitignored — reinstall plugins via WP Admin after init; uploads are managed via backup and restore.


sync-local-to-remote.toolkit.sh — pushes the local WordPress state to the remote server. Rsyncs wp-content/, dumps the local database, imports it on the remote, runs wp search-replace to rewrite local URLs (mywpsite.localhost) to the production domain, then flushes the Redis object cache. Cache flush is required after a DB import, otherwise WordPress reads stale cached options from Redis.

sync-remote-to-local.toolkit.sh — the reverse: pulls wp-content/ and a DB dump from the remote server to local dev, then rewrites URLs to the .localhost domain. Useful for working with real production content locally.


WordPress core files live inside the container image, not in a bind-mounted directory. Do not use the WordPress dashboard to upgrade WordPress core. The dashboard upgrade modifies files inside the container’s writable layer, which works until the container is recreated.

To prevent this from happening silently, the template sets WP_AUTO_UPDATE_CORE false in WORDPRESS_CONFIG_EXTRA, which disables WordPress’s background auto-update feature. Core updates go through the image tag flow below. On the next dapsman local build --build, dapsman prod deploy, or docker compose down + up, the container is rebuilt from the image — and all those in-container changes are gone. If the database was migrated to the newer WP schema but the running core reverts to the old version, the mismatch can cause PHP errors, broken admin screens, or unpredictable behavior.

The correct upgrade path is to update the image tag and rebuild.

  1. Put prod offline if the site has active traffic (prevents writes during the upgrade):

    dapsman prod offline --project mywpsite [--provider <name>]
  2. Sync prod to local (prod is typically canonical for WordPress — don’t lose any recent content):

    dapsman local sync-from-prod --project mywpsite [--provider <name>]
  3. Back up prod (good to have in case something goes wrong and you need to restore the site):

    dapsman prod backup --project mywpsite --provider <name>
  4. Update the image tag in _docker/wordpress.dockerfile:

    # before
    FROM wordpress:6.7-php8.3-apache
    # after (check Docker Hub for the actual available tag)
    FROM wordpress:7.0-php8.3-apache

    Before committing: check Docker Hub for the exact tag and verify PHP compatibility with your installed plugins.

  5. Rebuild and test locally:

    dapsman local build --build --project mywpsite

    Then log into the local site (e.g. mywpsite.localhost).

    WordPress will prompt you to run its database migration on first page load — complete it. Then upgrade any plugins with pending updates via WP Admin. Test the front end and admin thoroughly.

  6. Deploy to prod:

    dapsman prod deploy --build --project mywpsite --provider <name>

    Rebuilds the image from the new tag, exports it, uploads it, and restarts the container. WordPress will auto-migrate prod’s database on first page load.

  7. Sync local to prod (optional: see alternative below):

    dapsman prod sync-from-local --project mywpsite --provider <name>

    Since you ran the DB migration and upgraded plugins locally, local is now ahead of prod. This pushes the migrated database and updated wp-content/ to the remote server.

  8. Bring prod back online:

    dapsman prod online --project mywpsite [--provider <name>]

WordPress handles its own database schema migrations — no manual SQL needed.

Alternative: If you prefer, you can skip step 7 and run the DB migration and plugin upgrades directly in prod WP Admin after step 6. Both paths work — the local-first approach keeps prod offline for less time and lets you catch problems before they reach prod.

Plugins and themes live in wp-content/, which is bind-mounted. These can be updated via WP Admin. Recommended flow: update on prod (since prod has the live data), then sync back to local with dapsman local sync-from-prod.


The template includes two wp-config.php constants that significantly reduce the attack surface of a WordPress installation:

Prevents WordPress from silently upgrading its own core files inside the running container. Without this, WordPress’s background auto-update can update the container’s writable layer to a newer (potentially vulnerable) WordPress version while the Docker image tag stays at the old version. If that auto-updated version has a known exploit, the container is vulnerable with no visible indication. Always keep this set to false and manage WordPress version upgrades through the image tag flow described above.

Prevents WordPress from modifying its own files at runtime. Specifically:

  • Disables plugin and theme installation from WP Admin — attackers who obtain WP Admin credentials (or exploit an unauthenticated API bug) cannot install plugins or upload files through the dashboard
  • Disables the built-in theme/plugin file editor — no in-browser code editing

Trade-off: you can no longer install or update plugins from WP Admin in prod. Manage plugins by adding or updating them locally, then syncing the changes to prod using dapsman prod sync-from-local --project <name>. Depending on how heavily-trafficked your site is, you may want to take it offline in prod first to avoid losing data such as from comments.

WP_AUTO_UPDATE_CORE false is set in both dev and prod compose files. DISALLOW_FILE_MODS true is set in the prod compose file only — dev leaves this off so you can freely install and test plugins locally.


Multiple WordPress projects can run alongside each other on the same DAPS instance. The template is designed for this:

  • db and redis are on an internal project network (mywpsite_net) and are not visible to other projects.
  • Caddy routes to the WordPress container via a project-specific alias (mywpsite) on daps_net, not the generic service name wordpress, so there is no DNS collision.

The one thing you must change manually for a second project is the dev host port bindings in _docker/compose_mywpsite.dev.yaml, since each project uses fixed ports and only one can bind a given host port at a time:

ServiceDefault portChange to (example)
wordpress80808081
db33063307
phpmyadmin80828083

These ports are only used for direct local access (e.g. connecting a DB tool to MySQL). Traffic through https://mywpsite.localhost goes via Caddy and is unaffected.