Putting Nextcloud Into a Container

Howdy! I host my own Nextcloud instance (nothing big, just for a few personal data) on a rented server. Sadly, my provider is a bit very slow when it comes to updating the distributions it provides — but that is another rant. The crux is that I am currently stuck with PHP 7, which is not supported by Nextcloud anymore. In fact, the last release that supported PHP 7 came out the 8th of December 2022, and there are no more security releases.

Prequel: Switching Apache to PHP-FPM

My server runs Apache, and the easiest way to get PHP support there is to use mod_php. This module allows Apache to directly execute PHP scripts. However, even if it is easy to use, it comes with severe drawbacks (as listed on the Apache site). For me, the main drawback was that it prevented me from using a different multi-processing module (MPM) than mpm_prefork, which not only is less performant than other MPMs, but in turn also prevents me from using mod_http2 to provide HTTP/2 support.

As such, the first step was to switch the PHP implementation over to FPM. Luckily, Debian comes with everything packaged and ready to go:

apt install php-fpm
a2dismod php7.3
a2enconf php7.3-fpm

The reason why this becomes interesting is because — as we will figure out in the next section — Nextcloud does provide a Docker image that exposes the FPM port. This means that we don't need to run the full-fledged Nextcloud image that internally runs another Apache, and we can stick with the more lightweight setup.

Combining Nextcloud FPM and Apache

The easiest way to get the latest Nextcloud version is to use their Docker image. It comes in two flavors: One version that has a HTTPd built in and only needs a reverse proxy, and one version that exposes only the FPM port — and therefore needs a HTTPd on the outside to handle the actual HTTP requests, and serve the static files.

The idea now is to re-use the Apache I already have running, together with mod_proxy_fcgi — which is also what the PHP-FPM setup uses. In addition to that, I want to re-use the MariaDB server that runs on the host, to avoid having a second database server in the Docker container as well.

I therefore use the following invocation to run the Nextcloud container:

docker run --rm \
    -v /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock \
    -v /var/www/cloud:/var/www/html \
    -p 127.0.0.1:9000:9000 \
    nextcloud:23-fpm

I use version 23 to ensure that I start with the same version as I had installed, and then can upgrade the server later.

For Apache, I started with the following configuration, as is shown on the mod_proxy_fcgi documentation:

ProxyPassMatch "^/(.*\.php(/.*)?)$" "fcgi://localhost:9000/var/www/html/" enablereuse=on max=5 connectiontimeout=20ms

I added the max and connectiontimeout parameters to ensure that the connections are smooth. Without them, I had a lot of timeouts and nonresponsive requests. The value of 5 matches what is set in PHP-FPM in the container (see /usr/local/etc/php-fpm.d/www.conf):

; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
; This value sets the limit on the number of simultaneous requests that will be
; served. [...]
pm.max_children = 5

This setup however has an issue: ProxyPassMatch is evaluated before other directives, such as the rewrite directives. The better option is to use SetHandler, that however requires a bit more of configuration:

DocumentRoot /var/www/cloud
ProxyFCGIBackendType GENERIC
ProxyFCGISetEnvIf "true" SCRIPT_FILENAME "/var/www/html/%{reqenv:SCRIPT_NAME}"
<FilesMatch "\.php$">
  <If "-f %{REQUEST_FILENAME}">
    SetHandler "proxy:fcgi://localhost:9000/"
  </If>
</FilesMatch>
<Proxy "fcgi://127.0.0.1:9000/" enablereuse=on max=5>
</Proxy>

Setting SCRIPT_FILENAME is important, as that is one of the differences between using ProxyPass and SetHandler. Without that, the handler will always return "Primary script unknown".

With this setup, I can run the Nextcloud installer to get a working Nextcloud 23 instance. For the database host, use localhost:/var/run/mysqld/mysqld.sock to ensure that connection is done through the passed-through Unix socket.

Migrating the Data

The image readme has a section on how to migrate an existing instance, and following that guide, I managed to get my data into the container. Since I did not have custom apps, I was fine with simply restoring the database content and the data/ folder from the backup.

The longest amount of time here was spent watching rsync work, especially since there were still many backups from earlier update processes.

Updating Nextcloud

Since I used the Nextcloud 23 image, I still had to do the actual update. This is very easy though with the image: You can simply pull the next version and run it, the update will then automatically be done when the container starts with the new image.

Note though that only updates spanning one major version are supported, but that's not a problem: If you're on version 23, simply pull nextcloud:24-fpm next, and then nextcloud:25-fpm.

Final Touches

There were still a few changes to be done in order to complete the setup:

First of all, some settings needed to be adjusted. The "Administration overview" in the Nextcloud settings should tell you what's up.

Then, the cronjob needs to be adjusted. I used the method with the system cron daemon, which was set to execute php -f /var/www/cloud/cron.php. However, now this needed to be done through Docker:

docker run --rm \
    -v /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock \
    -v /var/www/cloud:/var/www/html \
    --user=33 \
    nextcloud:25-fpm \
    php -f /var/www/html/cron.php

The same command can be used to run the occ management tool, by replacing cron.php with occ. Alternatively, you can exec into the running container.

Finally, the Nextcloud container also deserves a systemd file, so that it is started automatically:

[Unit]
Description=Nextcloud Docker container
Requires=mariadb.service

[Service]
ExecStart=docker run --rm -v /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock -v /var/www/cloud:/var/www/html -p 127.0.0.1:9000:9000 nextcloud:25-fpm
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Conclusion

Keeping services up-to-date is important, especially when you have web-facing, publicly accessible applications. Otherwise, you risk having unpatched security vulnerabilities just waiting to be exploited. Docker provides a good way to get up-to-date software, even if your hoster is unhelpful and provides old systems.

While updating my setup, I have learned the following things:

  • The modern way to use PHP applications is not mod_php, as simple as it might seem. FCGI using PHP-FPM seems like the way to go, as it provides better performance, better support for non-prefork workers and a lower memory overhead.
  • The Nextcloud docker image is very nice, and migrating was painless. As someone who started "late" with the Docker-game and kept many services non-containerized, this is nice to see.
  • Migrating the instance was easier than expected — granted, it is a small instance, but it was still nice that this process was painless (as experimenting with the Apache/FPM/… things was painful enough).
  • It's good to keep the instance clean. A lot of time was spent copying old, big files that I no longer need, or old backups of the Nextcloud installer. There's some time to be saved here, even though I don't plan on migrating that often.
  • One area that is a bit harder to work with is errors. Since we now have more points of failure, it was hard to debug some of the errors. For example, before setting SCRIPT_NAME, there was an error — even though the console showed that /index.php was (correctly) requested, and it worked before when still using ProxyPassMatch.

Now I'm sure there are a few points where my setup could still be optimized (such as not re-creating the container every time we restart it), but it seems to be working well so far. There's also the possibility to run Nextcloud 25 on PHP 7 (although not officially supported), but I'd rather not get into those wonky things.