Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@austinjp
Last active September 7, 2023 12:04
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save austinjp/f19e4de86398dad6a50d to your computer and use it in GitHub Desktop.
Save austinjp/f19e4de86398dad6a50d to your computer and use it in GitHub Desktop.
An *unprivileged* LXC container dedicated to running Wordpress

What and why

Create an unprivileged Linux container (LXC) dedicated to running Wordpress websites.

If you install Wordpress regularly on Linux you can instead simply clone this container.

The Wordpress installation is nothing fancy. It's not multi-site, it's not SSL enabled by default, or anything like that. Just a plain bog-standard Wordpress installation serving a single domain.

Warning!

This is for my personal use and is not recommended for anyone else. I use it as an aide memoire for creating LXC containers in general. Hopefully others will find it useful. I may or may not keep it updated.

Another warning!

If you want to create a container that is suitable for cloning, read the bit about cloning at the end of this doc before following the instructions.

Final warning!

For reasons I have yet to debug, the Wordpress core upgrade process silently fails. This means that Wordpress cannot be updated using the web interface, which is a major inconvenience.

On the upgrade page, Wordpress reports:

"If you need to re-install version –, you can do so here or download the package and re-install manually"

Note that it cannot tell what version is installed. Odd.

For now, a workaround is to install wp-cli and run wp core upgrade which seems to work fine. I definitely intend to fix this annoyance! FIXME

Requirements

Ubuntu Linux.

Install LXC on Ubuntu

...use apt to install LXC... (FIXME)

Create a new user

This is the user that will own the LXC containers, and be used to operate them. This should not be root! In this doc I've created a user called lxc.

sudo adduser lxc

Start a shell as your LXC user and fix some env vars:

su - lxc
unset XDG_RUNTIME_DIR XDG_SESSION_ID

For more on XDG_* see https://gist.github.com/julianlam/4e2bd91d8dedee21ca6f

Create an unprivileged Ubuntu LXC container

See https://www.stgraber.org/2014/01/17/lxc-1-0-unprivileged-containers/

Name the container with the domain name of the website you are deploying the container for. This is important, since the method outlined in this approach uses the guest machine's hostname, which is taken from the lxc.utsname config setting, which in turn appears to be set from the container's name.

lxc-create -t download -n domain-name.com -- -d ubuntu -r trusty -a amd64

Note that the first time you run this it may spit out an error suggesting that you create a file ~/.config/lxc/default.conf with some appropriate settings. Do as you are told 😄

Static IP address for the container

On the host edit /etc/default/lxc-net to include the line:

LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf

Then edit /etc/lxc/dnsmasq.conf to look something like the following:

dhcp-host=domain-name.com,10.0.3.101
dhcp-host=another-domain-name.com,10.0.3.102

...and so on. Restart the necessary services:

sudo service lxc-net restart
sudo killall -s SIGHUP dnsmasq

Start the container and attach to it

Warning!

The next few steps are hacky fixes to get around issues I was having. They seem to have solved things, but YMMV and I'm not entirely convinced that this is a sensible approach.

The following steps are specific and necessary to avoid issues with permissions that will prevent the container starting.

One particular cgroup setting seems to give particular problems. Check the config file, which should be in /home/lxc/.local/share/lxc/domain-name.com/config and comment out the line lxc.cgroup.use = freezer,devices.

Then:

# FIXME Here be dragons... hacks ahoy:
sudo mkdir -p /run/user/1000/
sudo chown -R lxc:lxc /run/user/1000/
sudo chmod 0770 /run/user/1000/

You may need to check these permissions if at some point you notice that lxc-ls -f or lxc-info return errors like this:

$ lxc-info -n domain-name.com
lxc-info: utils.c: mkdir_p: 253 Permission denied - failed to create directory '/run/user/1000/lxc/'
failed to create lock
Failure to retrieve information on /home/lxc/.local/share/lxc:domain-name.com
$ lxc-ls -f
failed to create lock
446: error creating container domain-name.com

Then create ~/bin/lxc-prep.sh:

mkdir -p ~/bin
cat > ~/bin/lxc-prep.sh << PREPSCRIPT
#!/bin/bash
sudo mkdir -p /run/user/1000/lxc
sudo chown lxc:lxc /run/user/1000
sudo chown lxc:lxc /run/user/1000/lxc
sudo cgm create all $USER
sudo cgm chown all $USER $(id -u) $(id -g)
cgm movepid all $USER $$
PREPSCRIPT
chmod +x ~/bin/lxc-prep.sh
ln -s ~/bin/lxc-prep.sh ~/bin/lxc-prep

Every time I log in as my lxc user to manipulate the LXC instances I run that script first. Add ~/bin/ to your path by editing ~/.bashrc.

Start the container:

lxc-prep
lxc-start -n domain-name.com

You may notice the following error message:

lxc-start: utils.c: setproctitle: 1455 Operation not permitted - setting cmdline failed

This probably is not a problem, see http://www.linuxquestions.org/questions/slackware-14/lxc-start-since-last-upgrade-in-current-reproducible-error-4175559143/

Check everything is as you want it to be with lxc-ls -f. If you have problems with the IP address being not what you expect, check /var/log/syslog for lines containing dnsmasq-dhcp -- that might shed some light on things. For example, apparently LXC does not reload the dnsmasq configuration unless the bridge device is down for the host -- a fun issue to debug.

Attach to the container:

lxc-attach -n domain-name.com

Some basic setup might be required:

# Now is a good time to change the root password:
passwd
# Fix the path:
export PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin

You may want to create ~/.bashrc something like this:

export EDITOR=emacs
export TERM=xterm-color
export PATH=$PATH:/usr/local/sbin:/usr/sbin:/sbin
alias ls="ls -h --color"
alias cp="cp -i"
alias mv="mv -i"
alias rm="rm -i"
cd ~

Check ca-certificates and run dpkg if any errors

apt-get update
apt-get install ca-certificates # or apt-get install wget, for example
# If errors, run this, it may take a few minutes:
dpkg --configure -a
# Then try again and hopefully it will work
apt-get install ca-certificates

Inside the container, install MySQL and PHP5

Download the latest .deb file for MySQL from http://dev.mysql.com/downloads/repo/apt/

Put it somewhere handy like /tmp. For instance:

apt-get install -y wget
cd /tmp
wget http://dev.mysql.com/get/mysql-apt-config_0.6.0-1_all.deb

Then use the Debian package manager to install the .deb file:

sudo dpkg -i /tmp/mysql-apt-config_0.6.0-1_all.deb

Then use Ubuntu's apt (advanced package tool) to complete the installation of MySQL, PHP, and other packages necessary for the installation process:

apt-get update
apt-get install mysql-server mysql-client php5 php5-mysql php5-curl man unzip curl

Install Apache or lighttpd

Apache is well-tested and works with Wordpress. Lighttpd is far more lightweight, and although it seems to work fine with a vanilla Wordpress installation there are no guarantees that it will work with more exotic Wordpress configurations without serious tinkering. Take your pick!

If you want to use Apache, do this:

apt-get install apache2 libapache2-mod-php5

If you want to use lighttpd, do this:

apt-get install lighttpd php5-cgi

You will need to configure PHP for lighttpd. Edit /etc/php5/cgi/php.ini. Find the line cgi.fix_pathinfo=1 and make sure it is NOT commented out. Then service lighttpd restart.

Configure MySQL

Optionally you may want to edit /etc/mysql/my.cnf at this point. My installations are for small personal websites, so I reduce MySQL's resources to the minimum sensibly required (note my config is slightly different to the one on that page, and I'm thinking of working out how to tweak it further):

query_cache_size=0
max_connections=10
#key_buffer_size=8
thread_cache_size=1
#host_cache_size=0

# per thread or per operation settings
thread_stack=131072
sort_buffer_size=32K
read_buffer_size=8200
read_rnd_buffer_size=8200
max_heap_table_size=16K
tmp_table_size=1K
bulk_insert_buffer_size=0
join_buffer_size=128
net_buffer_length=1K

# settings that relate to the binary log (if enabled)
#binlog_cache_size=4K
#binlog_stmt_cache_size=4K

#innodb_buffer_pool_size=5M
#innodb_log_buffer_size=256K
#innodb_ft_cache_size=1600000
#innodb_ft_total_cache_size=32000000
#innodb_sort_buffer_size=64K

Install postfix

Wordpress typically uses sendmail to send emails to users. Postfix is a drop-in replacement for sendmail, so install that. This process will ask you a few questions during installation; for full details see https://help.ubuntu.com/community/Postfix

apt-get install -y --no-install-recommends postfix

If you want to create a container that can be cloned (see the end of this doc) then add the following to your /etc/rc.local:

(sleep 1; sed -i.bak "s/mydestination = .*/mydestination = localhost.localdomain, localhost, $(hostname)/" "/etc/postfix/main.cf")&

The sleep and backgrounding & are hacks to ensure all commands in rc.local are executed without any failures that might cause the whole rc.local to stop.

Secure MySQL

sudo /usr/bin/mysql_secure_installation

Add wordpress user to MySQL

Create /tmp/mysqlconf.sql:

CREATE DATABASE wordpress;
GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,ALTER
ON wordpress.*
TO wordpress@localhost
IDENTIFIED BY 'yourpasswordhere';
FLUSH PRIVILEGES;

Then cat /tmp/mysqlconf.sql | sudo mysql -u root -p

Install Wordpress

See https://help.ubuntu.com/lts/serverguide/wordpress.html

apt-get install wordpress

This installation may be out of date -- the quickest way to fix this is to use http://wp-cli.org -- see below. But first you MUST visit the website in your browser to complete the installation process.

Wordpress comes bundled with 27M of langauge files. You can probably delete these safely:

mkdir /tmp/languages
sudo mv /usr/share/wordpress/wp-content/languages/* /tmp/languages
sudo rm -f /var/lib/wordpress/wp-content/languages/*

Fix Wordpress permissions

sudo chown -hR www-data:www-data /usr/share/wordpress/
sudo chown -hR www-data:www-data /etc/wordpress/htaccss

Edit Apache config

If you installed Apache...

Edit /etc/apache2/sites-enabled/000-default.conf and make it look something like this:

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
    UseCanonicalName Off

    # DocumentRoot /usr/share/wordpress/
    VirtualDocumentRoot /usr/share/wordpress/
    Options All

    RewriteEngine On

    # Fix issue with http://domain.com/wp-admin/ not showing dashboard
    RewriteRule "^/wp-admin/$" "/wp-admin/index.php" [R]

    RewriteRule ^/wp-content/(.*)$ /usr/share/wordpress/wp-content/$1
    RewriteRule ^/wp-includes/(.*)$ /usr/share/wordpress/wp-includes/$1
    RewriteRule ^/wp-admin/(.*)$ /usr/share/wordpress/wp-admin/$1

    RewriteRule ^index\.php$ - [L]
    RewriteCond /usr/share/wordpress%{REQUEST_URI} !-f
    RewriteCond /usr/share/wordpress%{REQUEST_URI} !-d
    RewriteRule . /usr/share/wordpress/index.php [L]
    # Also needed if using PHP-FPM / Fast-CGI
    # RewriteCond %{REQUEST_URI} !^/php5-fcgi/*

#    <Directory /usr/share/wordpress>
#       DirectoryIndex index.html index.php
#        Options FollowSymLinks
#       AllowOverride Limit Options FileInfo
#       DirectoryIndex index.php
#       Order allow,deny
#       Allow from all
#    </Directory>

#    <Directory /usr/share/wordpress/wp-content>
#       DirectoryIndex index.html index.php
#       Options FollowSymLinks
#       Order allow,deny
#       Allow from all
#    </Directory>

</VirtualHost>

Note that there is a fix in there for an issue where http://domain-name.com/wp-admin/ fails to show the Wordpress dashboard. The simple fix is to redirect /wp-admin/ to /wp-admin/index.php

Apache modules

If you installed Apache...

Enable necessary modules:

a2enmod vhost_alias
a2enmod rewrite

Configure lighttpd

If you installed lighttpd...

Edit /etc/lighttpd/lighttpd.conf to look like this:

server.modules = (
        "mod_access",
        "mod_alias",
        "mod_compress",
        "mod_redirect",
        "mod_rewrite",
        "mod_accesslog",
        "mod_fastcgi",
)

server.document-root        = "/usr/share/wordpress/"
#server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
server.errorlog             = "/var/log/lighttpd/error.log"
server.accesslog            = "/var/log/lighttpd/access.log"
server.pid-file             = "/var/run/lighttpd.pid"
server.username             = "www-data"
server.groupname            = "www-data"
server.port                 = 80
server.error-handler-404    = "/index.php"

index-file.names            = ( "index.php", "index.html", "index.lighttpd.html" )
url.access-deny             = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

compress.cache-dir          = "/var/cache/lighttpd/compress/"
compress.filetype           = ( "application/javascript", "text/css", "text/html", "text/plain" )

# default listening port for IPv6 falls back to the IPv4 port
## Use ipv6 if available
#include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
include_shell "/usr/share/lighttpd/create-mime.assign.pl"
include_shell "/usr/share/lighttpd/include-conf-enabled.pl"

fastcgi.server = ( ".php" => ((
    "bin-path" => "/usr/bin/php5-cgi",
    "socket" => "/tmp/php.socket",
    "max-procs" => 1,
    "bin-environment" => ( "PHP_FCGI_CHILDREN" => "4", "PHP_FCGI_MAX_REQUESTS" => "1000" ),
    )))

Restart it with service lighttpd restart

Fix Wordpress/MySQL bug

On initial setup, Wordpress appears to try inserting date formats that MySQL doesn't like. This leads to errors and failure to create database tables (see here and here). To fix this, run the following:

As root:

for i in $(grep -rle '0000-00-00 00:00:00' /usr/share/wordpress/*); do sed -i.bak "s/0000\-00\-00 00\:00\:00/1000-01-01 00:00:00/" $i; done

Configure Wordpress

Create (or edit) /etc/wordpress/config-localhost.php and write the following lines:

<?php
  define('DB_NAME', 'wordpress');
  define('DB_USER', 'wordpress');
  define('DB_PASSWORD', 'yourpasswordhere');
  define('DB_HOST', 'localhost');
  define('WP_CONTENT_DIR', '/usr/share/wordpress/wp-content');
?>

Then:

sudo chmod 0640 /etc/wordpress/config-localhost.php
sudo chgrp www-data /etc/wordpress/config-localhost.php

Create a symlink for the config file so you'll be able to access it using a web browser from another machine (substitute the appropriate IP address and domain name):

sudo ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-domain-name.com.php
sudo ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-10.0.3.101.php

The following symlinks are also useful:

sudo ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-127.0.0.1.php
sudo ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-.php
sudo ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-com.php

To ensure these symlinks are present on boot (for example, if you intend to regularly clone this container -- see the end of this doc) you could add the following lines to /etc/rc.local just before the last line (which should say exit 0):

(sleep 6; ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-.php || /bin/true)&
(sleep 5; ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-com.php || /bin/true)&
(sleep 4; ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-127.0.0.1.php || /bin/true)&
(sleep 3; ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-$(echo $(hostname -I)).php || /bin/true)&
(sleep 2; ln -s /etc/wordpress/config-localhost.php /etc/wordpress/config-$(hostname).php || /bin/true)&

The sleep and backgrounding & are hacks to ensure all commands in rc.local are executed without any failures that might cause the whole rc.local to stop.

Restart Apache or lighttped with service apache2 restart or service lighttpd restart. FIXME: This may generate the error

/usr/sbin/apache2ctl: 87: ulimit: error setting limit (Operation not permitted)
Setting ulimit failed. See README.Debian for more information.

... but it seems to work regardless.

Check your guest's IP address. From the host run lxc-ls -f. If the guest has no IP address try

sudo service lxc-net restart
sudo killall -s SIGHUP dnsmasq

... then lxc-ls -f again.

The guest IP will probably be of the form 10.0.?.?.

Try accessing the site from a web browser. If the CSS isn't loading, check the HTML source, and you may find that Wordpress is generating HTML containing the 10.* IP address, which obviously a browser on another machine will not be able to resolve. You can fix this by editing /usr/share/wordpress/wp-config.php (copy wp-config-sample.php if necessary) and adding the following lines just before the final ?>:

define('WP_HOME','http://'.gethostname());
define('WP_SITEURL','http://'.gethostname());

This uses the PHP gethostname() function. LXC gets this from the lxc.utsname config setting, which in turn appears to be set from the container's name, so ensure your container is named exactly the same as the hostname you desire.

Once you can access the site in your browser, remove those lines and set them in the Wordpress settings at settings -> general.

Upgrade Wordpress

Before doing this you MUST complete the installation by visiting the website in your browser.

For reasons I haven't yet worked out, if apt installs an old version of Wordpress (which seems highly likely) then the Wordpress web dashboard fails to report the version number and upgrading is impossible through the dashboard. wp-cli fixes that. Inside your container (not the host), as root, do the following:

cd /tmp/
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
ln -s wp-cli.phar wp
export HTTP_HOST=$(echo $(hostname -I))
./wp --allow-root --path=/usr/share/wordpress/ core upgrade

After that, you should probably fix permissions again:

chown -R www-data:www-data /usr/share/wordpress/

Container errors

FIXME Here be dragons.

The container created above seems to work perfectly well. However, it does generate the following errors (which can be discovered running it in the foreground, logging to a file):

      lxc-start 1452378700.622 ERROR    lxc_utils - utils.c:open_without_symlink:1620 - No such file or directory - Error examining security in /usr/lib/x86_64-linux-gnu/lxc/sys/kernel/security
      lxc-start 1452378700.622 INFO     lxc_conf - conf.c:mount_entry:1727 - failed to mount '/sys/kernel/security' on '/usr/lib/x86_64-linux-gnu/lxc/sys/kernel/security' (optional): No such file or directory
      lxc-start 1452378700.622 ERROR    lxc_utils - utils.c:open_without_symlink:1620 - No such file or directory - Error examining pstore in /usr/lib/x86_64-linux-gnu/lxc/sys/fs/pstore
      lxc-start 1452378700.622 INFO     lxc_conf - conf.c:mount_entry:1727 - failed to mount '/sys/fs/pstore' on '/usr/lib/x86_64-linux-gnu/lxc/sys/fs/pstore' (optional): No such file or directory
      lxc-start 1452378700.623 ERROR    lxc_utils - utils.c:open_without_symlink:1620 - No such file or directory - Error examining efi in /usr/lib/x86_64-linux-gnu/lxc/sys/firmware/efi/efivars
      lxc-start 1452378700.626 INFO     lxc_conf - conf.c:mount_entry:1727 - failed to mount '/sys/firmware/efi/efivars' on '/usr/lib/x86_64-linux-gnu/lxc/sys/firmware/efi/efivars' (optional): No such file or directory

I haven't got around to debugging this yet. All I have found so far is that this seems to relate to entries in the config files /usr/share/lxc/config/ubuntu.common.conf and /usr/share/lxc/config/ubuntu.userns.conf.

Create a container suitable for cloning

The above instructions are suited to creating a single container running a Wordpress website on a single domain name. If you regularly install Wordpress on dedicated domains, you may prefer to install LXC and simply clone a bare-bones container every time. It's far quicker.

In this case, follow the instructions above but replace domain-name.com with something like WORDPRESS-SKELETON or similar. Also scan the above instructions for references to rc.local and make sure you create a suitable /etc/rc.local file.

Remove symlinks and temp files

Before cloning your container, attach to it and remove the config-*.php symlinks that are in /etc/wordpress/. Then remove all temp files and unnecessary packages from the apt downloads -- no need to be cloning them, they'll consume disk space unnnecessarily:

apt-get purge man -y
apt-get purge unzip -y
apt-get purge wget -y
apt-get purge curl -y
apt-get autoremove -y
apt-get clean -y
rm -rf /tmp/*

Peronsally I like to remove my bash history files too:

rm -f ~/.bash_history
ln -s /dev/null ~/.bash_history

Stop the container. Don't start the container again after this, or the symlinks will be created again. Your clones don't want these symlinks. Instead, once you have created the clones, they will create their own symlinks on boot.

Clone and start

After your container is ready you should be able to clone it -- provided that you have already provided appropriate settings for dnsmasq:

# As your LXC user:
lxc-stop -n WORDPRESS-SKELETON
lxc-clone WORDPRESS-SKELETON domain-name.com
lxc-clone WORDPRESS-SKELETON another-domain.org
lxc-start -n domain-name.com
lxc-start -n another-domain.org
lxc-ls -f
NAME                STATE    IPV4        IPV6  GROUPS  AUTOSTART
----------------------------------------------------------------
WORDPRESS-SKELETON  STOPPED  -           -     -       NO
domain-name.com     RUNNING  10.0.3.101  -     -       NO
another-domain.org  RUNNING  10.0.3.102  -     -       NO

Then you can simply create an Apache config on the host machine to proxy through to the appropriate container. For example, in the example above, I'd create /etc/apache2/sites-available/domain-name.com to look something like this:

<VirtualHost domain-name.com:80>
     <IfModule mod_proxy.c>
         ProxyPass        /       http://10.0.3.101/
         ProxyPassReverse /       http://10.0.3.101/
         ProxyPassMatch   ^/(.*)$ http://10.0.3.101/$1
     </IfModule>

     LogLevel warn
     ErrorLog /var/log/apache2/domain-name-error.log
     CustomLog /var/log/apache2/domain-name-access.log combined
</VirtualHost>

...and of course sudo a2ensite domain-name.com.conf and sudo service apache2 reload.

To recap, the entire process for creating a complete virgin Wordpress install for a new domain name would be:

  1. Add line to dnsmasq settings.
  2. Clone container and start it.
  3. Add Apache config file and reload Apache.

A brief discussion about security

Cloning containers as described will introduce a security risk -- passwords will be the same inside all containers. It is up to you to ensure this is not the case! :bowtie:

I've specifically used unprivileged containers here so even if an attacker gains root inside your container, they do not automatically gain root in your host. Obviously, this depends on the LXC project to ensure this -- and assumes that my hacks above do not introduce any security holes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment