Ansible Role linuxfabrik.lfops.wordpress¶
This role installs and configures the WordPress CMS.
Attention: It is intended that when you call wordpress__url you will get a white page because no theme is installed. wordpress__url followed by /wp-admin works as expected.
Available since LFOps 2.0.0.
How the Role Behaves¶
- The WordPress core,
wp-config.phpandwp-content/mu-pluginsbelong toroot, so code running in the web server cannot modify them.wp-contentbelongs toapache, so plugins, themes, translations and uploads can still be installed and updated from the web interface (FS_METHODisdirect). - WordPress therefore cannot update its core itself, and its automatic core updates are switched off (
WP_AUTO_UPDATE_CORE).wordpress-core-minor-update-<instance>.timerinstalls the latest minor release daily instead and reloads a running PHP-FPM afterwards, and--tags wordpress:updateinstallswordpress__version. The update button for the core in the web interface fails. - WP-CLI runs as
rootonly for commands that do not load WordPress (core download, config, checksum verification). Everything that loads WordPress runs asapache, because loading it executes code fromwp-content, whichapachecan write. - WordPress cannot write
.htaccess, so pretty permalinks need either theFallbackResourcefrom "Post-Installation Steps", or the rewrite rules WordPress displays after a change of the permalink structure, added to.htaccessby hand. - Everything that exists once per WordPress instance carries the host and path of
wordpress__urlas the name of the instance, with/replaced by-(example.com,example.com-blog):wordpress-cron-<instance>.timer,wordpress-core-minor-update-<instance>.timerand the export directory. The vHost belongs to the host name and is shared by all instances under it (<host>.80.conf). The role removeswordpress-cron.timerof older role versions, but not their vHost filewordpress.conf. - The REST API only answers logged-in users. The role installs and activates the Disable WP REST API plugin, and uninstalls the Disable REST API (
disable-json-api) plugin where it is present. Anonymous requests to any route, including the routes of plugins installed later, get401 rest_login_required. The plugin has no settings, so a front-end feature that calls the REST API without a login, such as some contact forms, needs an exception in code.
Dependent Roles¶
Any LFOps playbook that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables.
- A web server (for example Apache httpd) must be installed, with a virtual host configured for WordPress (role: linuxfabrik.lfops.apache_httpd).
- MariaDB 10+ must be installed (role: linuxfabrik.lfops.mariadb_server).
- PHP 7.4+ must be installed (role: linuxfabrik.lfops.php).
Multiple Instances on One Host¶
Several WordPress instances can share a host as pseudo hosts in the inventory: one inventory host per instance, all with the same ansible_host, each with its own wordpress__url, wordpress__database_name and wordpress__database_user. The instances can use different host names (https://blog.example.com, https://shop.example.com), different paths under one host name (https://example.com/blog, https://example.com/shop), or both, including an instance at the root of a host name next to instances in its sub-paths. The instances share the web server, PHP-FPM and MariaDB, so:
- Instances under one host name share its vHost. Its document root is the installation directory without the path, for example
/var/www/html/example.comfor/var/www/html/example.com/blog, so if you setwordpress__install_dir, keep that part the same for all of them. Put settings for this vHost (apache_httpd__vhosts__*_var) into a group that contains exactly the pseudo hosts of this host name, or into thehost_varsof the pseudo host if it is the only one. A pseudo host of another host name must not get them: the role injects only the vHost of its own host name, so there the entry lacks itstemplateand the run aborts.apache_httpd__vhosts__group_varis a single variable, so a pseudo host must not be in two groups that set it, otherwise Ansible keeps only one of the values. - Put everything that is not specific to one instance, such as
mariadb_server__admin_user,php__*orapache_httpd__*settings other than the vHosts, into a group that contains all pseudo hosts of the machine, not into theirhost_vars. Pseudo hosts that disagree about a shared configuration file overwrite each other on every run. This includes values that are looked up perinventory_hostname, such as passwords from Bitwarden, andmariadb_server__dump_on_calendar, whose default depends on theinventory_hostname. - Add the pseudo hosts to
lfops_setup_wordpressonly, and the real host to all other playbooks, such assetup_basicor the monitoring. - Do not run the pseudo hosts of a machine in parallel, for example with
--forks 1or one--limitafter the other. Otherwise Ansible configures the same machine several times at once, which leads to package manager lock timeouts, overlapping service restarts and a broken initial MariaDB setup. - All instances run as
apache, so a vulnerable plugin in one instance can modify thewp-contentand read thewp-config.phpof every other instance on the host.
Post-Installation Steps¶
-
Enable automatic updates for the plugins, since the role leaves them to WordPress: in the web interface under Plugins > Installed Plugins, select all plugins and apply the bulk action "Enable Auto-updates", or run
sudo --user=apache /usr/local/bin/wp plugin auto-updates enable --all --path=/var/www/html/example.com. Plugins installed later start with auto-updates off, so repeat this for them. WordPress updates the plugins in the background, triggered bywordpress-cron-<instance>.timer, and restores the previous version of an active plugin if the site shows a fatal error afterwards. For that check the host requests its ownwordpress__url, so it has to reach it, behind a reverse proxy through the proxy: a request that cannot connect counts as a fatal error and rolls every update of an active plugin back. -
Let Apache hand requests for pretty permalinks to WordPress, so they work without rewrite rules in
.htaccess. Add aFallbackResourcefor each instance to therawvariable of the vHost of its host name, where "Multiple Instances on One Host" puts the vHost settings. Each instance needs its own<Directory>block with the absolute path of itsindex.phpin the URL; a relative path does not work for permalinks below the instance. A block for a sub-path takes precedence over the one of an instance at the root above it. The entry is merged into the vHost the role deploys, soconf_server_name,virtualhost_portandraware enough. For example, with an instance athttps://example.comand two athttps://other-example.com/instance1andhttps://other-example.com/instance2:```yaml
host_vars/example.com.yml, the only pseudo host of example.com¶
apache_httpd__vhosts__host_var: - conf_server_name: 'example.com' virtualhost_port: 80 raw: !unsafe |-
FallbackResource /index.php ``````yaml
group_vars/wordpress_other_example_com.yml, a group of the pseudo hosts other-example.com-instance1 and other-example.com-instance2¶
apache_httpd__vhosts__group_var: - conf_server_name: 'other-example.com' virtualhost_port: 80 raw: !unsafe |-
FallbackResource /instance1/index.php FallbackResource /instance2/index.php ```
Tags¶
wordpress
- Installs and configures wordpress.
- Triggers: none.
wordpress:export
- Exports the site content (posts, pages, comments, custom fields, categories and tags) as a wxr file to
/backup/wordpress-export/<instance>, which onlyapacheandrootcan read. - Triggers: none.
wordpress:file_policy
- Gives the core,
wp-config.phpandwp-content/mu-pluginstorootand the rest ofwp-contenttoapache. restorecon -Fvr {{ wordpress__install_dir }}.- Triggers: none.
wordpress:update
- Updates the WordPress core to
wordpress__version. Also applies all DB migrations, and updates all plugins and themes. - Reloads a running PHP-FPM after the core and again after the plugin and theme updates, so that OPcache does not keep serving old files.
- Triggers: none.
Mandatory Role Variables¶
wordpress__admin_email
- The Email of the WordPress admin user.
- Type: String.
wordpress__admin_user
- The WordPress admin user account.
- Type: Dictionary.
-
Subkeys:
-
username:- Mandatory. Username.
- Type: String.
-
password:- Mandatory. Password.
- Type: String.
-
wordpress__database_user
- The database user account with permissions on the
wordpress__database_namedatabase. - Type: Dictionary.
-
Subkeys:
-
username:- Mandatory. Username.
- Type: String.
-
password:- Mandatory. Password.
- Type: String.
-
wordpress__site_title
- The WordPress site title.
- Type: String.
wordpress__url
- The URL under which WordPress is reachable, including
http://orhttps://and an optional path such as/blog, without a trailing slash. Use the URL your users type in the browser, which with a reverse proxy that terminates TLS in front is anhttps://URL even though Apache httpd on the host serves plain HTTP. - It is the single source for the site address: WordPress is installed with it, and the role sets
homeandsiteurlto it on every run, so an address changed in the WordPress settings is set back. Withhttps://, WordPress builds its links for HTTPS and marks its login cookiesSecure, and the role forces HTTPS for the login and the admin area. The host part becomes theServerNameof the vHost, and host and path the default installation directory. - Type: String.
Example:
# mandatory
wordpress__admin_email: 'webmaster@example.com'
wordpress__admin_user:
username: 'wordpress-admin'
password: 'linuxfabrik'
wordpress__database_user:
username: 'wordpress'
password: 'linuxfabrik'
wordpress__site_title: 'WordPress Test Site'
wordpress__url: 'https://wordpress.example.com'
Optional Role Variables¶
wordpress__application_passwords_enabled
- Whether users can create application passwords, with which REST API clients such as the WordPress mobile app or external integrations log in. The role switches them on or off through the must-use plugin
wp-content/mu-plugins/linuxfabrik.php. - Type: Bool.
- Default:
false - Deviates from the upstream default
true: an application password logs a client in without a second factor, so it bypasses a two-factor plugin.
wordpress__database_host
- The host on which the database is accessible.
- Type: String.
- Default:
'localhost'
wordpress__database_name
- The name of the database. Each instance on a host needs its own.
- Type: String.
- Default:
'wordpress'
wordpress__disallow_file_edit
- Prevent editing of plugin / theme files from the admin WebGUI. Strongly recommended to set this to
truefor security reasons. - Type: Bool.
- Default:
true
wordpress__install_dir
- The installation directory for WordPress. For a
wordpress__urlwith a path, it has to end with that path; the part before it becomes the document root of the vHost. - Type: String.
- Default:
/var/www/html/followed by the host and path ofwordpress__url, for example'/var/www/html/wordpress.example.com'or'/var/www/html/example.com/blog'
wordpress__on_calendar_core_minor_update
- When
wordpress-core-minor-update-<instance>.timerinstalls the latest minor release of the WordPress core (systemd timer notation). - Type: String.
- Default:
'04:{{ 59 | random(seed=inventory_hostname) }}'
wordpress__plugins
- List of WordPress plugin slugs. To get a list of already installed plugins and whether they are active, use the WordPress CLI
sudo -u apache /usr/local/bin/wp plugin list. - Type: List of dictionaries.
- Default:
[] -
Subkeys:
-
name:- Mandatory. Plugin slug, path to a local zip file, or URL to a remote zip file.
- Type: String.
-
enabled:- Optional. Activates the plugin (
true) or keeps it installed but deactivated (false). Withfalse,namehas to be the plugin slug, since WP-CLI cannot deactivate a plugin by the path or URL of its zip file. - Type: Bool.
- Default:
true
- Optional. Activates the plugin (
-
state:- Optional. Either
'present'or'absent'. - Type: String.
- Default:
'present'
- Optional. Either
-
wordpress__theme
- The WordPress theme to install. Accepts a theme slug, the path to a local zip file, or a URL to a remote zip file.
- Type: String.
- Default: unset
wordpress__timer_core_minor_update_enabled
- Enables or disables
wordpress-core-minor-update-<instance>.timer, which installs the latest minor release of the WordPress core. With the timer disabled, the core only changes with--tags wordpress:update. - Type: Bool.
- Default:
true
wordpress__trusted_proxies
- IP addresses of the reverse proxies in front of WordPress. Only on requests from one of them does WordPress take the client address from the
X-Forwarded-Forheader, because any client can send that header. List every proxy of a chain, individual addresses only, no CIDR ranges. - Leave it empty for a WordPress that clients reach directly. Behind a proxy that is not listed, WordPress sees the proxy's address for every visitor, for example in comments and in login-limiting plugins.
- Type: List of strings.
- Default:
[]
wordpress__version
- The WordPress version to install. Possible options: version number,
'latest','nightly'. - Type: String.
- Default:
'latest'
wordpress__wxr_export
- Path to a WXR export file on the managed host, readable by
apache, which will be imported when the role installs WordPress. The file includes posts, pages, comments, custom fields, categories and tags, and can be created using the wp-cli export function or thewordpress:exporttag. - Type: String.
- Default: unset
Example:
# optional
wordpress__application_passwords_enabled: true
wordpress__database_host: 'localhost'
wordpress__database_name: 'wordpress'
wordpress__disallow_file_edit: true
wordpress__install_dir: '/var/www/html/wordpress.example.com'
wordpress__on_calendar_core_minor_update: '04:30'
wordpress__plugins:
- name: 'bbPress'
state: 'present'
- name: 'Akismet'
state: 'absent'
- name: 'classic-editor'
enabled: false
wordpress__theme: 'twentysixteen'
wordpress__timer_core_minor_update_enabled: true
wordpress__trusted_proxies:
- '192.0.2.10'
wordpress__version: 'latest'
wordpress__wxr_export: '/tmp/wordpress.xml'