WordPress is a state-of-the-art publishing platform with a focus on aesthetics, web standards, and usability. It is one of the worlds most popular blog publishing applications, includes tons of powerful core functionality, extendable via literally thousands of plugins, and supports full theming.
This appliance includes all the standard features in TurnKey Core, and on top of that:
WordPress configurations:
- Installed from upstream source code to /var/www/wordpress
- Integrated upgrade mechanism: get WordPress updates straight from WordPress' creator Automattic.
- Uploading of media such as images, videos, etc.
- Permalinks configuration supported through admin console (convenience)
- Plugin and theme management remains available through WordPress. These directories intentionally contain web-writable executable code; install only updates and extensions you trust.
- WordPress core is root-owned and does not update automatically. Apply a
supervised core update as
rootwithkeel-wordpress-update. The command verifies official WordPress core checksums and restores the appliance ownership boundary after updating.
Landing page provides links to WordPress plugin search, plus a number of useful and popular Wordpress plugins (none pre-installed):
- Yost SEO: Optimizes your WordPress blog for search engines and XML sitemaps.
- NextGEN Gallery: Easy to use image gallery with thumbnail & slideshow options.
- JetPack by WordPress.com: Jetpack adds powerful features previously only available to WordPress.com users including customization, traffic, mobile, content, and performance tools.
- WP Super Cache: Accelerates your blog by serving 99% of your visitors via static HTML files.
- Social Media Share Buttons & Icons: Promote your content by adding links to social sharing and bookmarking sites.
- Simple Tags: automatically adds tags and related posts to your content.
- BackupWordPress: easily backup your core WordPress tables.
- Google Analytics Dashboard for WordPress: track visitors, AdSense clicks, outgoing links, and search queries.
- WP-Polls: Adds an easily customizable AJAX poll system to your blog.
- WP-PageNavi: Adds more advanced paging navigation.
- Ozh admin dropdown menu: Creates a drop down menu with all admin links.
- Contact Form 7: Customizable contact forms supporting AJAX, CAPTCHA and Akismet integration.
- Seriously Simple Podcasting: Simple Podcasting from your WordPress site.
SSL support out of the box.
Adminer administration frontend for MySQL (listening on port 12322 - uses SSL).
Postfix MTA (bound to localhost) to allow sending of email (e.g., password recovery).
Webmin modules for configuring Apache2, PHP, MySQL (MariaDB) and Postfix.
See the WordPress docs for further details (including multisite howto).
This appliance is built as a Keel layer, not as a whole root filesystem:
bt-layer wordpress --parent mariadb
The parent is the published ``mariadb`` layer today, and it will move. That
layer is already built, tested and published, so what this recipe adds is
Apache, PHP and WordPress and nothing else. An apache-php layer is being
extracted; when it lands, this appliance's parent becomes that layer and the
wordpress layer is rebuilt on it. Nobody should be surprised by that: layers
are content addressed, so a new parent means a new digest and a cheap rebuild,
and .github/workflows/tests.yml and this section are the two places that
name the parent.
Upstream's conf.d/main installs WordPress while the image is being built,
with DB_PASS=turnkey and ADMIN_PASS=turnkey written in the recipe. A
layer is published once and reused by every machine built on it, so those are
the same two passwords on every one of them. This recipe leaves the layer with
no credential at all: no wp-config.php, and a database account created with
a password hash that no input produces.
The installation happens on the first boot, from
/etc/keel/instance.yaml, and the WordPress installer is never offered to a
visitor. firstboot.d/40wordpress reads:
Every secret is a file reference in the instance description, never a literal.
Neither password is ever an argument of another process: wp-config.php is
written by a shell function, and the administrator's password is set through
the environment of one wp-cli call. The hook finishes by asking the site to
authenticate that account, and to refuse a wrong password, before it lets
Apache serve anything.
wp-config.php derives WP_HOME and WP_SITEURL from the Host
header of each request rather than reading them from the database, so the site
answers on whatever address or name reaches it. A WordPress whose recorded site
URL is not the one it is reached by answers a permanent redirect to the
recorded one, which for an appliance reached by its IPv6 literal means sending
the visitor to a name that does not resolve. The recorded URL is the fallback
for WP-CLI and wp-cron, which carry no Host header.
The salts in wp-config.php are generated on the machine rather than fetched
from WordPress's API, because a first boot must not need the internet.
The appliance ships /etc/apt/sources.list.d/keel.sources enabled for
the signed trixie distribution of https://archive.keellinux.org, with
Signed-By naming /usr/share/keyrings/keel-archive-keyring.gpg from the
keel-archive-keyring package, and /etc/apt/preferences.d/keel pins that
origin at 1001. So on a booted appliance:
apt-get update # reads our archive and verifies its signature apt-get upgrade # takes newer Debian and newer project packages apt-get install keel-transition # a project package, from our archive
covers two things:
- the system, from Debian and Debian security, as any Debian machine does;
- the project's own packages:
inithooks,confconsole,keel,keel-archive-keyring,keel-transition,fab.
The boot test proves that path rather than proving that a newer version
happens to exist on the day it runs. It asserts that apt-get update
verifies our archive's signature; that apt-cache policy shows a project
package the image carries with our archive as the source of its candidate at
that pin priority; that keel-transition, which the image does not carry,
comes down from our archive against the digest the signed index holds; and
that a project package the image does carry is downloaded again and put
through dpkg. apt refuses an archive it cannot verify before it asks for a
single byte, so those last two are the signature reaching real files and not
only an index.
All of it stays inside our own archive on purpose. Installing
keel-transition on a real appliance also pulls gpgv from Debian, which
a Debian 13 image does not carry because apt verifies with sqv; that is
fine on a machine with a route to Debian, and the CI runner has no IPv4 route
out, so a test step that needed it would be measuring the runner's network
instead of this appliance.
It does not cover WordPress core, and saying otherwise would be a lie in
the documentation. Nobody packages WordPress for Debian and we do not package
it either: it is unpacked from a release archive pinned by digest in
conf.d/main and verified a second time against WordPress's own per file
checksums. WordPress core therefore updates through WordPress's own mechanism,
either from the dashboard or, preferably on this appliance, with:
keel-wordpress-update
which runs wp core update, verifies WordPress's checksums again and puts
the ownership boundary back: core and wp-config.php root owned, and only
wp-content/uploads, cache, upgrade, plugins and themes
writable by the web server. Automatic core updates are off in
wp-config.php for the same reason: a supervised update keeps that
boundary, an unsupervised one does not.
One consequence of the pin worth knowing, because it is a priority that
downgrades as well as upgrades: an image must not carry a project package that
the signed archive has not got. If it does, apt-get upgrade replaces the
newer installed package with the archive's older one. The boot test's policy
check is where that shows up.
Plugins and themes are WordPress's business and are installed through WordPress. Those two directories are deliberately web writable, which means they hold executable code the web server can change: install only what you trust.
80 and 443 are the site, on both address families; 12320 is the web shell and
12321 Webmin, both from Core. 3306 is closed, although the parent layer
opens it: this appliance reaches its own database over the loopback, and
wp-config.php names [::1]. /etc/cron.d/wordpress-cron reaches
wp-cron.php over the IPv6 loopback literal, because on Debian
localhost is an IPv4 name only.
Adminer, lighttpd and the rest of turnkey/lamp are left out. The
database is administered through webmin-mysql in the panel Core already
carries, and Adminer would bring a second first boot hook at 35 reading the
same DB_PASS for an account this appliance does not use. The parent layer's
own admin database account is left unable to authenticate unless an
instance declares it, which is the state that layer publishes it in.
keel/instance.example.yaml is the description to start from, and
tests/README.md is how to boot and check one by hand. The appliance's own
view of itself, on the machine:
keel diff --spec /etc/keel/instance.yaml
Every password comes from the instance description; none is set here and none is typed at a console on a headless first boot.
- Webmin and SSH: username root, from
secrets.root_password - the WordPress database account: username wordpress
(
app.options.db_user), fromsecrets.db_password - WordPress: username admin (
app.options.admin_user), fromsecrets.app_password
Adminer is not installed, so it has no account here.