Adding NextCloud(and NextCloud Office) to my Homelab
September 19, 2024
Updated November 4, 2025
- added how to run crontab for a docker install of NextCloud
- improved commands to keep things clean after an update
- added how to recover from “maintenance mode” in a docker container
I’ve been dancing around this one for a while. NextCloud is a beast… it can have so many bells and whistles it can almost “do it all”. But every bell and whistle needs maintenance and may not be the best and a bunch of them need a separate application to run the thing, so why would I do it. Well, having a working online filesystem is handy, access to my own work anywhere without having to pay extortionate sums or be beholden to any of the possible big brothers out there, I like that. Having a working online office system is nice so I don’t have to have a desktop app installed to edit and manage “real” documents.
The release of Hub 9 or version 30, however you want to account for that persuaded me. With the hierarchical tree view of the folders and a few other improvements including performance I’m willing to plunge in once more. You see I tried this quite a while ago… when I was first starting up the homelab. I didn’t know very much at all about any of it, how docker works, where to look for troubleshooting, what builds and docker compose files to look for, needless to say, standing up NextCloud was too much for me at the time.
This time however I managed to get it all working. There were a few gotchas that I’ll go through here.
I started with getting just NextCloud working by itself without anything at all added and not bothering with a URL or reverse proxy. Just trying to get the thing to stand up and run. This guide was a big help getting started. The vast majority of sample Docker Compose files out there are too simple to use usefully. They are pretty much missing everything but the most basic requirements to get the software up and running. This documentation was helpful but in the end misleading but at least it has many of the possible environment variables.
My initial Docker Compose looked a bit like the following (understanding that anything really sekrit has been replaced with a sample value).
services:
db:
container_name: nextcloud_db
image: mariadb
restart: unless-stopped
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW --innodb-file-per-table=1 --skip-innodb-read-only-compressed
volumes:
- ./data:/var/lib/mysql
env_file:
- db.env
networks:
- next-private
redis:
container_name: nextcloud_redis
image: redis
restart: always
command: redis-server --requirepass sekrit-pass-for-redis
networks:
- next-private
app:
container_name: nextcloud_app
image: nextcloud:latest
restart: unless-stopped
ports:
- 9080:80
links:
- db
- redis
volumes:
- ./html:/var/www/html
environment:
MYSQL_HOST: nextcloud-db
REDIS_HOST_PASSWORD: sekrit-pass-for-redis
NEXTCLOUD_DATA_DIR: /var/www/html/data
NEXTCLOUD_ADMIN_USER: adminuser
NEXTCLOUD_ADMIN_PASSWORD: adminuser-sekrit-pass
SMTP_HOST: mail.myhostedmail.domain
SMTP_SECURE: ssl
SMTP_PORT: 465
SMTP_AUTHTYPE: LOGIN
SMTP_NAME: mailusername
SMTP_PASSWORD: mail-sekrit-pass
MAIL_FROM_ADDRESS: me@myhostedmail.domain
MAIL_DOMAIN: myhostedmail.domain
env_file:
- db.env
depends_on:
- db
- redis
networks:
- next-private
- public
A few things I’m doing here… one is a private network for service containers (redis and the database, these don’t need to be on a public network). Volumes are where I can find them and back them up and easily access them for each application I run in my environment… this is a personal choice.
I normally eschew .env files to keep things neat and in a single file (it’s full of passwords anyway… why bother separating things) but in this case, it comes in handy to reduce duplication as the environment variables for the database settings in NextCloud use the same name value pairs as the database itself.
the db.env has the following in it.
# Nextcloud and nextcloud database environment variables
TZ=America/Los_Angeles
MYSQL_ROOT_PASSWORD=db-root-sekrit-pass
MYSQL_PASSWORD=db-user
MYSQL_DATABASE=nextcloud-db
MYSQL_USER=nextcloud-db-user
NextCloud environment variables are many, varied and complex and the documentation that I could find is difficult if you’re new . I preset the adminstration password and username, in addition I also tried to preset the email connection settings… The mail settings ended up working in the end but initial settings (not included here) didn’t work so swell.
These settings worked but weren’t necessary in the end and I have since removed them from my docker-compose. I had a couple of false starts and discovered that in the end mucking with the config.php file was better than continuing to fool around in the docker-compose.yml. documentation for how this file works can be found here.
Note that at this point when I login and install the system as admin, I’m NOT choosing to install all the bits that NextCloud offers to install up front (calendar, talk, notes etc). I’m trying to keep the setup as simple as humanly possible for me.
At one point I decided I should “re-install” (since the first time you get the container up and running it installs the rest of the application once there’s an admin user). It turns out triggering this isn’t too strenuous but you need to do 2 things to make this work.
- go into /localnextcloudvolumedirectory/html/config/config.php and change the ‘installed’ => true, flag to false, do NOT lose the comma as this is a list. This requires an understanding of how volumes work in Docker which is beyond the scope of this write up.
- add a file to the same config directory with the name CAN_INSTALL (touch will do for this to work).
The other thing I found was the email configuration interface was a bit funky and manipulating the settings in the config.php was helpful.
The next iteration of the docker compose made the nextcloud interface public by adding the following labels under the app: service, I use Traefik as a reverse proxy for my setup.
labels:
- traefik.enable=true
- traefik.http.routers.nextcloud.entrypoints=secureentrypt
- traefik.http.routers.nextcloud.rule=Host(`selfhosted.nextcloud.domain`)
- traefik.http.routers.nextcloud.tls=true
- traefik.http.routers.nextcloud.tls.options=default
- traefik.http.routers.nextcloud.tls.certresolver=theresolverforcerts
- traefik.http.services.nextcloud.loadbalancer.server.port=80
pretty standard stuff really and it just worked which was gratifying. I can not say enough good things about cloudflare and letsencrypt, it’s so easy to get this stuff to work with traefik.
The next trick was to stand up a Collabora Online server. I added another service to the above compose file
nextcloud-collabora:
image: collabora/code
container_name: nextcloud-collabora
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment:
TZ: America/Los_Angeles
DOMAIN: selfhosted\\.collabora\\.domain
username: collaborauser
password: collabora-user-sekrit-pass
DONT_GEN_SSL_CERT: "true"
extra_params: "--o:ssl.enable=false --o:ssl.termination=true"
server_name: selfhosted.collabora.domain
networks:
- public
expose:
- 9980
cap_add:
- MKNOD
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.nextcloud-collabora.rule=Host(`selfhosted.collabora.domain`)
- traefik.http.routers.nextcloud-collabora.entrypoints=secureentrypt
- traefik.http.services.nextcloud-collabora.loadbalancer.server.port=9980
- traefik.http.routers.nextcloud-collabora.tls=true
- traefik.http.routers.nextcloud-collabora.tls.certresolver=theresolverforcerts
- "traefik.http.routers.nextcloud-collabora.rule=Host(`selfhosted.collabora.domain`) && (PathPrefix(`/lool`) || PathPrefix(`/hosting/discovery`) || PathPrefix(`/hosting/capabilities`) || PathPrefix(`/loleaflet`))"
This successfully builds a working collabora but it won’t integrate with NextCloud and that messed with me for a bit. Note that I’ve added a few environment variables (again the documentation for these is thin on the ground, but it should make sense in that… I wanted to preset an admin username and password and I didn’t want it to generate an SSL certificate of it’s own as Traefik will be taking care of that. Note that this largely came from this source.
So a few things were wrong on both sides to make this work. First, NextCloud needs a “trusted domain”. I thought I needed to manage the environment variables for NextCloud to configure the trusted domain.
environment:
MYSQL_HOST: nextcloud-db
REDIS_HOST_PASSWORD: sekrit-pass-for-redis
NEXTCLOUD_DATA_DIR: /var/www/html/data
NEXTCLOUD_TRUSTED_DOMAIN: selfhosted.nextcloud.domain
TRUSTED_PROXIES: <IP/mask> for the proxy system (in my case where my traefik server lives)
This didn’t work for me… strictly speaking, in that the
'trusted_domains' =>
array (
0 =>
),
element in the config.php (see above for where you can find this) remained empty. Populating it with the nextcloud domain did get this part working
'trusted_domains' =>
array (
0 => 'selfhosted.nextcloud.domain',
),
Note the trusted domain is the Nextcloud domain, not the Collabora domain.
So this got me past the permission denied problem (400 error) when trying to access Collabora but it STILL didn’t work which was annoying. Further research suggested that the last line of the traefik label for Collabora was the problem, if you open the console and look at the errors showing up (this is an unexpected but handy way of understanding problems between NextCloud and Collabora) you can see that the “expected links” aren’t there. They are expecting cool and browser, it turns out and not lool and loleaflet. So the final fix to make it all work was to fix the expression in the traefik label for Collabora and then it just worked…
- "traefik.http.routers.nextcloud-collabora.rule=Host(`selfhosted.collabora.domain`) && (PathPrefix(`/cool`) || PathPrefix(`/hosting/discovery`) || PathPrefix(`/hosting/capabilities`) || PathPrefix(`/browser`))"
Cleaning up
Now that collabora is working let’s have look and fix a couple of things I didn’t know were broken based on documentation found while solving other problems. The following needs to be added to Traefik’s dynamic.yaml (if you have a middlewares already defined just copy everything but that line).
middlewares:
nextcloud-redirect:
redirectregex:
regex: "/\\.well-known/(?:cal|card)dav(?:/)?$"
replacement: "/remote.php/dav/"
This middleware expression was found here. This comes with the following additional labels for NextCloud in the docker-compose.yml.
- traefik.http.middlewares.nextcloud_redirect.redirectregex.regex='https://(.*)/.well-known/(?:card|cal)dav'
- traefik.http.middlewares.nextcloud_redirect.redirectregex.replacement='https://$${1}/remote.php/dav'
This should enable dav, caldav and carddav functions (I haven’t tried these yet as I’ve been busy writing this and figuring out what else I want running in NextCloud).
On the adminstration page dashboard NextCloud does a self-check and lets you know what else is wrong with your install. Mine had a complaint about the proxy which cleaned itself up overnight after I’d setup the middleware and made sure the rest of my reverse proxy setup was right. It was also complaining about missing indices in the database. This is easily fixed by running an occ command (these are command line functions for managing nextcloud). Since it’s in a docker it’s a bit annoying to run CLI on nextcloud but it is relatively simple if you’re patient and setup up the command correctly.
docker exec -ti --user www-data <nextcloud docker container name> /var/www/html/occ db:add-missing-indices
docker exec -u www-data <nextcloud docker container name> php occ db:add-missing-indices
use docker ps to ensure you have the right name for the container you want to run this in. You’re looking for the app (as named in the services above).
The last warning I had was a security warning about STS headers for the server. Traefik is taking care of this for me (now that I’ve added one more line to my traefik labels).
- traefik.http.routers.nextcloud.middlewares=secureHeaders@file
the middleware for secureHeaders in my dynamic.yaml for traefik looks like this
secureHeaders:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
sslRedirect: true
# HSTS Configuration
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN"
And that seems to have got me started on the path to nextcloud and all the things I can do with it…
One more thing… there’s an online security scan that NextCloud offer free of charge which provides feedback on security measures and configuration you’ve created to protect your site.
I discovered after doing the scan that I had left one thing out, _html-prefix was still some kind of issue, however a number of individuals have posted in the nextcloud forums very helpfully that adding the line
'overwriteprotocol' => 'https',
to config.php fixes even this issue resulting in an A+ rating on the security site. Note, this is a public service using publicly known information to challenge your server. While it feels good to get the A+ that doesn’t mean you don’t have other weaknesses on the same server.
Running Crontabs in a Docker AIO NextCloud
Turns out this is NOT simple and you know, you need the out-of-the box crontabs to run (if you want the system to stay healthy) and if you’ve added the Calendar and want to sync with Google (or any other external source) for calendar data you may be frustrated to discover this isn’t working out of the box. I did find this discussion which made what looks like the easiest suggestion if you have control of the host for your Docker setup. In my case I do so I setup the following crontab entry to get this working… and it did, which makes me quite happy. The way I can tell is that my Calendar (which I did eventually install) is getting 5 minute updates from my Google Calendar which is awesome.
Setting a crontab in a docker container directly doesn’t seem to make a whole lot of sense. So instead what you’re doing is setting a crontab in the host which calls the cron.php in the nextcloud container directly. In the host system use the following command
sudo crontab -e
This will prompt a Debian system for which editor you’d like to use (nano or tiny), pick one and then add the following line (ensure it’s not commented out by #) and save and exit as you normally would the text editor
*/5 * * * * docker exec -u www-data <nextcloud container name> php /var/www/html/cron.php
The above line basically calls the command “docker exec…php” every 5 minutes.
The output from saving this line to the file should be something like
crontab: installing new crontab
Maintenance Tasks after an update
Commonly after a nextcloud update you will notice in your admin overview interface complaints about indices in the database and mimetype migrations. As above the indices can be corrected as follows:
docker exec -u www-data nextcloud_app php occ db:add-missing-indices
The mimetypes and a few other things can be corrected with the following:
docker exec -u www-data nextcloud_app php occ maintenance:repair --include-expensive
What to do if you can’t login after an update
So, after one of the updates I found the system unresponsive and didn’t understand what was wrong. After some homework and pfaffing about I came to understand that the system had put itself into “maintenance mode”. This effectively kills nextcloud, regardless of issues or not, the system simply won’t let anyone log in. There’s a command to turn this off fortunately.
docker exec -u www-data nextcloud_app php occ maintenance:mode --off