Disclaimer (Tech)
Link to originalDanger
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.
Rootless Podman
Source
This section has been heavily inspired by Micheal Jack’s nextcloud-quadlet repository! I simply adapted his proceeding to my needs, by changing some paths, using my monolithic caddy instance for reverse proxying, etc.
Caddy within Caddy?
Note that, similar to the original author (see above), I use two Caddy instances. One to serve Nextcloud itself and another one in front of it for reverse proxying.
The only difference between my and the original author’s approach is that my instance, responsible for reverse proxying, is not part of the Nextcloud pod.
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:
{
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
80HTTP 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
{$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
How long did it take?
Don’t ask!
I won’t admit how long it took me to realize, that my Main Caddy container can’t communicate with a completely differnt caddy container, in a different pod, on a different network using
localhost:8080. Thanks to this excellent blog post, I was finally enlightened and released from my suffering.
Pod
Create the ~/.config/containers/systemd/nextcloud.pod file
[Unit]
Description=Nextcloud Pod
[Pod]
PodName=nextcloud
Network=nextcloud.network
PublishPort=8080:80Network
As we configured a network for our Nextcloud, we will need to create the network, too.
[Unit]
Description=Nextcloud Network
[Network]
Label=app=nextcloud
DisableDNS=false
Internal=falseContainers
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
[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.targetReplace
user: username used for running the rootless podman instance.
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.txtThis 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.txtDelete 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
[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.targetReplace
user: username used for running the rootless podman instance.
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:
[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.targetNextcloud
Now we can finally create the main Nextcloud container.
Create the file under ~/.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.targetReplace
user: username used for running the rootless podman instance.NEXTCLOUD_DOMAIN: FQDN of the Nextcloud instance
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.serviceSet 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/shNow 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’snextcloud-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.1Mime type migrations
php occ maintenance:repair --include-expensiveAdd missing indices
php occ db:add-missing-indicesSet 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
valueThe above value for
valueof1, 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 -ethen paste in the cronjob:
*/5 * * * * podman exec -t -u www-data nextcloud-app php -f /var/www/html/cron.phpSave it and check if everything went smoothly
crontab -lHardening
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:
{
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