Adding NextCloud(and NextCloud Office) to my Homelab

September 19, 2024

Updated November 4, 2025

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.

  1. 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.
  2. 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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.