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 Nextcloud.

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) Caddy 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
PublishPort=8080:80

Network

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

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

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
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
 
[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
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
 
[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
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

Transclude of Podman#Reload the daemon

Auto-Update

Circular transclusion detected: Podman

Linger

Circular transclusion detected: Podman

Start

Circular transclusion detected: Podman

Replace

  • name : nextcloud-pod

Status

Circular transclusion detected: Podman

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

Transclude of Caddy#Debug

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 Cronie[[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 System Administration applies here, too.

I have collected a couple of additional options for the Nextcloud 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 Nextcloud, 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 Nextcloud 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