Disclaimer (Tech)

Danger

Proceed with caution, use at your own risk!

This is merely a documentation of my specific setup, i.e. what I found works for me.

You might have entirely different requirements and expectations of security, etc.

Always use your Brain™

Always read up on up-to-date documentation and current best practices. Inform yourself, research, and treat my documentation as what it truly is: a mere info-dump.

Link to original

Rootless Podman

Prerequisites

Make sure you have podman installed and a frontend Caddy instance set up.

Data directories

First off, create all the necessary directories:

mkdir -p ~/containers/nextcloud/{data,db,html,caddy/data,caddy/logs}

Backend Caddyfile

First, we tell the Caddy instance within the pod (which I’ll refer to as the backend instance), how to serve the Nextcloud by creating the necessary Caddyfile within ~/containers/nextcloud/caddy/config:

~/containers/nextcloud/caddy/config/Caddyfile
{
	servers {
		trusted_proxies static private_ranges
	}
}
 
:80 {
	root * /var/www/html
	file_server
 
	php_fastcgi nextcloud-app:9000
 
	redir /.well-known/carddav /remote.php/dav/ 301
	redir /.well-known/caldav /remote.php/dav/ 301
 
	header {
		Strict-Transport-Security "max-age=31536000;"
	}
 
	# .htaccess / data / config / ... shouldn't be accessible from outside
	@forbidden {
		path /.htaccess
		path /data/*
		path /config/*
		path /db_structure
		path /.xml
		path /README
		path /3rdparty/*
		path /lib/*
		path /templates/*
		path /occ
		path /console.php
	}
 
	respond @forbidden 404
}

This will

  • serve the Nextcloud on the standard 80 HTTP port
  • for the hostname equal to the name specified for its container
  • within the Nextcloud pod, or more specifically within it’s specified Network.

The pod configuration will take care of forwarding an actual outside/system port to this pod-internal one.

Frontend Caddyfile

As mentioned, the external caddy instance (which I will refer to as the frontend instance) is used for reverse proxying. Note that the backend caddy instance expects incoming traffic on port 8080, as specified in its container config.

Therefore, we simply add a section to the (already present) Caddyfile under ~/containers/caddy/config/Caddyfile

~/containers/caddy/config/Caddyfile
{$NEXTCLOUD_DOMAIN} {
	import subdomain-log {$NEXTCLOUD_DOMAIN}
 
	redir /.well-known/carddav /remote.php/dav/ 301
	redir /.well-known/caldav /remote.php/dav/ 301
 
	header {
		Strict-Transport-Security max-age=31536000;
	}
 
	reverse_proxy http://host.containers.internal:8080
}

NEXTCLOUD_DOMAIN : FQDN of the Nextcloud instance

Pod

Create the ~/.config/containers/systemd/nextcloud.pod file

~/.config/containers/systemd/nextcloud.pod
[Unit]
Description=Nextcloud Pod
 
[Pod]
PodName=nextcloud
Network=nextcloud.network

Network

As we configured a network for our Pod, we will need to create the network, too.

~/.config/containers/systemd/nextcloud.network
[Unit]
Description=Nextcloud Network
 
[Network]
Label=app=nextcloud

Containers

Caddy

To set up the backend Caddy instance, which will use the previously created Caddyfile, we simply create a new ~/.config/containers/systemd/nextcloud-caddy.container file

~/.config/containers/systemd/nextcloud-caddy.container
[Unit]
Description=Nextcloud Web
Wants=nextcloud-app.service
After=nextcloud-app.service
 
[Container]
Pod=nextcloud.pod
Label=app=nextcloud
AutoUpdate=registry
ContainerName=nextcloud-caddy
Image=docker.io/caddy:latest
Network=nextcloud.network
Volume=/home/user/containers/nextcloud/caddy/data:/data:Z
Volume=/home/user/containers/nextcloud/caddy/config:/etc/caddy:Z
Volume=/home/user/containers/nextcloud/caddy/logs:/var/log/caddy:Z
Volume=/home/user/containers/nextcloud/html:/var/www/html:ro,z
PublishPort=8080:80
 
[Install]
WantedBy=default.target

Replace

This will also forward the outside/system port 8080 to the inside port 80, specified in the aformentioned Caddyfile.

Database

Of course, Nextcloud requires a database. We’ll use MariaDB, as it’s one of the recommended choices for Nextcloud, according to the official documentation. Apart from a performance standpoint, it doesn’t really matter, since we won’t clutter our system by using a containerized approach.

Podman Secret

First, we generate a Podman Secret to be used as the database password.

Warning

You should always generate this password!

Humans are not suitable password generators!

We use pwgen to generate a password and store it in a file, to not leak it to our shell history.

pwgen -s 32 1 > pass.txt

This generates a single 32 character long password and stores it in pass.txt.

We can now generate the Podman secret with the name nextcloud-mariadb-password

podman secret create nextcloud-mariadb-password pass.txt

Delete the file

Please remember to purge the password file afterwards! You can store the password securely in a password manager if you want, but you shouldn’t have unencrypted plaintext passwords on your system.

Container File

Create the ~/.config/containers/systemd/nextcloud-db.container file

~/.config/containers/systemd/nextcloud-db.container
[Unit]
Description=Nextcloud Database
 
[Container]
Pod=nextcloud.pod
Label=app=nextcloud
AutoUpdate=registry
ContainerName=nextcloud-db
Image=docker.io/library/mariadb:10.11
Network=nextcloud.network
Volume=/home/user/containers/nextcloud/db:/var/lib/mysql:Z
Environment=MARIADB_RANDOM_ROOT_PASSWORD=1
Environment=MARIADB_AUTO_UPGRADE=1
Environment=MARIADB_DISABLE_UPGRADE_BACKUP=1
Environment=MYSQL_DATABASE=nextcloud
Environment=MYSQL_USER=nextcloud
Secret=nextcloud-mariadb-password,type=env,target=MYSQL_PASSWORD
 
[Install]
WantedBy=default.target

Replace

Redis

For caching and other tasks, redis is a pretty standard choice. I actually planned to use Valkey, but ended up using redis for now. Simply enough, I simply copied this container file.

Create the file under ~/.config/containers/systemd/nextcloud-redis.container:

~/.config/containers/systemd/nextcloud-redis.container
[Unit]
Description=Nextcloud Redis
 
[Container]
Pod=nextcloud.pod
Label=app=nextcloud
AutoUpdate=registry
ContainerName=nextcloud-redis
Image=docker.io/library/redis:alpine
Network=nextcloud.network
 
[Install]
WantedBy=default.target

Nextcloud

Now we can finally create the main Nextcloud container.

Create the file under ~/.config/containers/systemd/nextcloud-app.container

~/.config/containers/systemd/nextcloud-app.container
[Unit]
Description=Nextcloud App
Wants=nextcloud-db.service nextcloud-redis.service
After=nextcloud-db.service nextcloud-redis.service
 
[Container]
Label=app=nextcloud
AutoUpdate=registry
Pod=nextcloud.pod
ContainerName=nextcloud-app
Image=docker.io/library/nextcloud:fpm-alpine
Network=nextcloud.network
Volume=/home/user/containers/nextcloud/data:/var/www/html/data:Z
Volume=/home/user/containers/nextcloud/html:/var/www/html/:Z
Environment=MYSQL_HOST=nextcloud-db
Environment=MYSQL_DATABASE=nextcloud
Environment=MYSQL_USER=nextcloud
Secret=nextcloud-mariadb-password,type=env,target=MYSQL_PASSWORD
Environment=REDIS_HOST=nextcloud-redis
AddHost=NEXTCLOUD_DOMAIN:127.0.0.1
 
[Install]
WantedBy=default.target

Replace

Boot it up

Reload

Reload the daemon

As Quadlet files are systemd service files, you need to reload the daemon.

systemctl --user daemon-reload

This generates appropriate .service files.

Tip

Sometimes, this can fail and not generate a .service file. To debug this, immediately drop into the user journal, to see any error messages

systemctl --user daemon-reload --no-block; journalctl --user -f
Link to original

Auto-Update

Auto-Update

If you enabled the auto-update feature using the AutoUpdate key in the .container file, you still need to enable the auto-update timer

systemctl --user enable --now podman-auto-update.timer
Link to original

Linger

Keep it running

As a rootless setup doesn’t use a system-level service, all services would be stopped upon logout.

To prevent this, we must enable-linger (where user is your username, of course):

loginctl enable-linger user
Link to original

Start

Start the service

systemctl --user start name.service
Link to original

Replace

  • name : nextcloud-pod

Status

Check the status

You can check the status of Podman using

podman ps

and the status of the service itself using either

systemctl --user status name.service

or

journalctl --user -xeu name.service

Tip

Sometimes, the non-service-specific journal can be helpful in debugging a problem. In that case, simply restart the service and immediately drop into the journal:

systemctl --user restart name.service --no-block; journalctl --user -f
Link to original

Replace

  • name : nextcloud-pod

Look for Started Nextcloud Pod, to ensure Nextcloud has started successfully. You can also check every other container’s status by substituting name with the container’s name.

Restart

Following that, you probably still need to restart the frontend Caddy, as we modified its Caddyfile previously:

systemctl --user restart caddy.service

Set it up

You should (hopefully) now be able to access your Nextcloud installer under the FQDN you specified

Debug

Tip

If something doesn’t work right away, try checking the statuses of the caddy, and the (pod’s) container service(s).

You can also prepend an additional portion in front of all the content to the respective Caddyfiles, enabling more verbose error outputs.

{
	debug
}
Link to original

Choose a username for the admin account and generate a (secure) password, store it in your password manager and follow the installer.

Remove the warnings

Most, if not all, of the warnings in your admin dashboard should go away after telling the Nextcloud what domains/proxies to trust.

Enter the container

First we enter the container

podman exec -it -u www-data nextcloud-app /bin/sh

Now we can use Nextcloud’s occ tool

Trust

Set environment variables

  • $SERVER_IP : your server’s public IP
  • $NEXTCLOUD_DOMAIN : FQDN of this Nextcloud instance
  • $REGION : your region, for example, DE
  • $CADDY : hostname of your caddy container (in my guide it’s nextcloud-caddy)
php occ config:system:set trusted_domains 0 --value="$CADDY"
php occ config:system:set trusted_domains 1 --value="$SERVER_IP"
php occ	config:system:set trusted_domains 2 --value="$NEXTCLOUD_DOMAIN"
php occ	config:system:set trusted_proxies 0 --value="$SERVER_IP"
php occ config:system:set overwrite.cli.url --value "https://$FQDN"
php occ config:system:set overwriteprotocol --value "https"
php occ config:system:set default_phone_region --value "$REGION"
php occ config:system:set proxyexclude 1 --value="localhost"
php occ config:system:set proxyexclude 2 --value="127.0.0.1

Mime type migrations

php occ maintenance:repair --include-expensive

Add missing indices

php occ db:add-missing-indices

Set maintenance window

Some maintenance tasks only run once a day. To prevent them from being run during the main usage time, we can set the start of the maintenance window, as per the official documentation:

php occ config:system:set maintenance_window_start --type=integer --value=1

value

The above value for value of 1, means that the aforementioned background job will only be run between 01:00am UTC and 05:00am UTC.

Crontab

In order for the Nextcloud’s crontab to be run regularly, we need to deploy a cronjob on the host side.

Make sure, you have the crontab command available, by installing Fedora.

crontab -e

then paste in the cronjob:

*/5 * * * * podman  exec -t -u www-data nextcloud-app php -f /var/www/html/cron.php

Save it and check if everything went smoothly

crontab -l

Hardening

Security should be more than fine, by using rootless containers (even for the reverse proxy caddy), isolating the network, etc. Still, security is always a concern and should be one of the top priorities.

As always, though, always refer to up-to-date information and best practices and also consider reading up on the official upstream Nextcloud documentation. The Disclaimer applies here, too.

I have collected a couple of additional options for the Backend Caddyfile that should harden the instance even more. Most of these options aim at future-proofing the installation and, for example, prevent access to files which should be unproblematic, but might not be (in the future). If you encounter weird problems or issues, it might be related to too restrictive of a config, so you might need to experiment with the introduced options, to determine which caused the error.

The file, we expand upon, is the Backend Caddyfile, as the frontend one solely describes the reverse proxy behavior. The added/modified portions are highlighted, to enable quick expansion of an already existing (and hopefully working) ~/containers/nextcloud/caddy/config/Caddyfile file:

~/containers/nextcloud/caddy/config/Caddyfile
{
	servers {
		trusted_proxies static private_ranges
	}
}
 
:80 {
	root * /var/www/html
	file_server
 
	php_fastcgi nextcloud-app:9000
 
	redir /.well-known/carddav /remote.php/dav/ 301
	redir /.well-known/caldav /remote.php/dav/ 301
 
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
 
		# More security hardening headers
		Referrer-Policy "no-referrer"
		X-Content-Type-Options "nosniff"
		X-Download-Options "noopen"
		X-Frame-Options "SAMEORIGIN"
		X-Permitted-Cross-Domain-Policies "none"
		X-Robots-Tag "noindex, nofollow"
		X-XSS-Protection "1; mode=block"
		# Permissions-Policy "interest-cohort=()"
 
		# Remove X-Powered-By header, which is an information leak
		-X-Powered-By
		# Replace http with https in any Location header
		Location http:// https://
	}
 
	# Cache control
	@static {
		file
		path *.css *.js *.svg *.gif
	}
 
	header @static {
		Cache-Control "max-age=360"
	}
 
	@fonts {
		path /core/fonts
	}
 
	header @fonts {
		Cache-Control "max-age=604800"
	}
 
	# gzip encoding
	encode {
		gzip 4
		minimum_length 256
 
		match {
			header Content-Type application/atom+xml*
			header Content-Type application/javascript*
			header Content-Type application/json*
			header Content-Type application/ld+json*
			header Content-Type application/manifest+json*
			header Content-Type application/rss+xml*
			header Content-Type application/vnd.geo+json*
			header Content-Type application/vnd.ms-fontobject*
			header Content-Type application/x-font-ttf*
			header Content-Type application/x-web-app-manifest+json*
			header Content-Type application/xhtml+xml*
			header Content-Type application/xml*
			header Content-Type font/opentype*
			header Content-Type image/bmp*
			header Content-Type image/svg+xml*
			header Content-Type image/x-icon*
			header Content-Type application/atom+xmlapplication/javascript*
			# Would this be a good idea?
			header Content-Type text/*
			# header Content-Type text/cache-manifest*
			# header Content-Type text/css*
			# header Content-Type text/plain*
			# header Content-Type text/vcard*
			# header Content-Type text/vnd.rim.location.xloc*
			# header Content-Type text/vtt*
			# header Content-Type text/x-component*
			# header Content-Type text/x-cross-domain-policy*
		}
	}
 
	# .htaccess / data / config / ... shouldn't be accessible from outside
	@forbidden {
		path /.htaccess
		path /.user.ini
		path /.xml
		path /3rdparty/*
		path /autotest
		path /build/*
		path /config/*
		path /console
		path /console.php
		path /data/*
		path /db_
		path /db_structure
		path /indie
		path /issue
		path /lib/*
		path /occ
		path /README
		path /templates/*
		path /tests/*
	}
 
	respond @forbidden 404
}

Of course, you need to at least restart the nextcloud-caddy.service if you changed this file after the Reboot step.

You could in theory also not terminate the TLS chain.

Reboot

Finally, restart the Nextcloud, just for good measure. It should be lightning quick, too.

systemctl --user restart nextcloud-pod