Nextcloud docker-compose setup with notify_push (2024)

Nextcloud docker-compose setup with postgres, notify_push, cron and imaginary 2024

this tutorial will teach you to can install Nextcloud today utilizing valuable options like notify_push and imaginary using docker compose. The overall architecture is based on Communty Docker micro-service environment and intended for advance docker compose setups.

I wanted to create an updated version of this tutorial using new possibilities introduced in newer versions. The main difference to most other tutorials is the integration of HPB (notify_push) to reduce system load and running the application as limited user which improves security and reduce attack vector on your system - if the attacker could successfully exploit Nextcloud he has much less options to break out of the container if the application runs in limited user context.

architecture

We are not going to discuss required and important topics like reverse proxy, TLS certificates, backup/restore, split-brain DNS to name the most important from the top of my head. I plan to create more tutorials for topics highly demanded in this forum later. Integration with existing Collabora CODE is handled only from NC point of view, see Collabora integration for details.

disclaimer

this article explains somewhat advanced setup using “manual” docker compose. The nature of Docker and Docker-Compose is you get many many help already and you can easily setup complex applications like Nextcloud within minutes. You will find dozen good tutorials how to setup Nextcloud in minutes. Most of them…

if you are willing to follow the more complex path and learn docker-compose way to setup and maintain Nextcloud follow the steps below.

Tough stuff follows and recommend you to understand all the aspects of your system when you choose to run a self-hosted system.

alternatives and credits for AiO and other appliances

If you are beginner, lack IT knowledge or maybe just want an easily setup Nextcloud without any desire to learn and customize the system other optionsmight be better suited for you. Take a look at #All-in-One project which provides more functionality out of the box at the cost of lower flexibility. Other projects like like ncp and Hansson IT vm and multiple hardware devices pre-configured with Nextcloud and many many manged Nextcloud offers out there - if you are looking for easy to use Nextcloud likely you should stop reading here.

Other tutorials which might be easier to start as a beginner

preparation / prerequisites

start with following pre-requisites

  • working docker-compose
  • system user without login (adopt user ID)
    sudo useradd --no-create-home test-nc --shell /usr/sbin/nologin --uid 1004
  • directory hosting all configs and data

you will create remaining config files using following steps

.env

global environment variables required for the whole project. Likely this is the only file need to edit. general.
I would recommend to start installation with an older version so you can test upgrades and backup/restore before you start adding you productive files. for this reason I start with version 27. upgrade to 28 completes in minutes and you only need to add database indices.

create a file .env using your favorite editor and paste the data (don’t forget to adopt the domain):

.env
#cat .env
# project name is used as container name prefix (folder name if not set)
COMPOSE_PROJECT_NAME=test-nc
# this is the global ENV file for docker-compose
# nextcloud FQDN - public DNS (for traefik labels)
DOMAIN=test-nc.mydomain.tld
NEXTCLOUD_VERSION=27
UID=1004
GID=1004

nextcloud.env

files used on first start of Nextcloud [app] container to create config.php file. Changes are not applied once the container has started once!

this file should work for most cases. In case you are very limited in RAM you want to reduce PHP_MEMORY_LIMIT (or increase in case you can afford). ${DOMAIN} variable is populated in different required places from the top-level .env file so you only need to edit it there.

create a file nextcloud.env using your favorite editor and paste the data:

nextcloud.env
# nextcloud FQDN - public DNS
NEXTCLOUD_FQDN=${DOMAIN}
NEXTCLOUD_TRUSTED_DOMAINS=${DOMAIN}
# reverse proxy config
OVERWRITEHOST=${DOMAIN}
overwrite.cli.url=https://${DOMAIN}
OVERWRITEPROTOCOL=https
# all private IPs
TRUSTED_PROXIES=172.16.0.0/12 192.168.0.0/16 10.0.0.0/8 fc00::/7 fe80::/10 2001:db8::/32
# php
PHP_MEMORY_LIMIT=1G
PHP_UPLOAD_LIMIT=10G
# db
POSTGRES_HOST=db
POSTGRES_DB_FILE=/run/secrets/postgres_db
POSTGRES_USER_FILE=/run/secrets/postgres_user
POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
# admin user
NEXTCLOUD_ADMIN_PASSWORD_FILE=/run/secrets/nextcloud_admin_password
NEXTCLOUD_ADMIN_USER_FILE=/run/secrets/nextcloud_admin_user
# redis
REDIS_HOST=redis

remoteip.conf

this file configures Apache rewrite-ip to show real IP of accessing client. no adjustments required.

create a fil remoteip.conf using your favorite editor and paste the data:

remoteip.conf
#cat remoteip.conf
RemoteIPHeader X-Real-Ip
#RemoteIPTrustedProxy 10.0.0.0/8
#RemoteIPTrustedProxy 172.16.0.0/12
#RemoteIPTrustedProxy 192.168.0.0/16
# https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html
# The RemoteIPInternalProxy directive adds one or more addresses (or address blocks)
# to trust as presenting a valid RemoteIPHeader value of the useragent IP.
# Unlike the RemoteIPTrustedProxy directive, any IP address presented in this header,
# including private intranet addresses, are trusted when passed from these proxies.
RemoteIPInternalProxy 10.0.0.0/8
RemoteIPInternalProxy 172.16.0.0/12
RemoteIPInternalProxy 192.168.0.0/16
RemoteIPInternalProxy fc00::/7
RemoteIPInternalProxy fe80::/10
RemoteIPInternalProxy 2001:db8::/32

cron.sh

unfortunately I don’t see any way to run cron container as non-root. after long research I found a post at Github showing how root container could run cron job as -non-root user. (beware the container runs as root - but cron jobs run as user)

create a file cron.sh using your favorite editor and paste the data:

cron.sh
#!/bin/sh
set -eu
# https://github.com/nextcloud/docker/issues/1740#issuecomment-1308141561
adduser --disabled-password --gecos "" --no-create-home --uid "$UID" cron
mv /var/spool/cron/crontabs/www-data /var/spool/cron/crontabs/cron
exec busybox crond -f -L /dev/stdout

compose.yml

this compose file creates pretty complete Nextcloud installation with postgres running as limited user you define in global .env

  • I didn’t manage redis and cron to run as UID so it still runs as root, but I can live with it given the fact redis is fully isolated inside docker network.
  • for postgres and redis I intentionally choose debian-based image as it turns out popular alpine flavors are slower.
  • I was surprised but it looks one can still use redis without password.
  • all mounts are stored inside of project directory - in case you want to store all or some files in a different place please adopt compose.yml

One Docker feature I didn’t understand and underestimated for long time is “silently included”. Docker Compose creates container “services” using ${COMPOSE_PROJECT_NAME} variable and falls back to directory name of the compose file if this variable is not set. This allows you to run multiple compose installations from the same file by just duplicating the folder/compose file. when you leave the project untouched important containers will start with test-nc prefix and -1 suffix so will find following containers later test-nc-app-1, test-nc-db-1 etc… but you still can use service names to manage containers - running docker compose exec app {command} will always address the service app depending on you current directory - it could be test, dev or prod instance, the command remains the same.

create a file compose.yml using your favorite editor and paste the data:

compose.yml
---
version: "3.3"

services:
  app:
    image: nextcloud:${NEXTCLOUD_VERSION}
    user: ${UID}:${GID}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    env_file:
      - ./nextcloud.env
    secrets:
      - postgres_db
      - postgres_password
      - postgres_user
      - nextcloud_admin_user
      - nextcloud_admin_password
    volumes:
      - ./nextcloud:/var/www/html
      - ./apps:/var/www/html/custom_apps
      - ./data:/var/www/html/data
      - ./config:/var/www/html/config
      # https://github.com/nextcloud/docker/issues/182
      - ./redis-session.ini:/usr/local/etc/php/conf.d/redis-session.ini
      - ./remoteip.conf:/etc/apache2/conf-available/remoteip.conf:ro
    restart: unless-stopped
    labels:
      - traefik.enable=true
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}.entrypoints=web-secure
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}.priority=1             # for notify_push
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${DOMAIN}`) # Nextcloud public FQDN
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls=true
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencryptresolver
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}.middlewares=secHeaders3@file,nextcloud-redirect@file
      - traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=80
    networks:
      - proxy
      - default

  notify_push:
    image: nextcloud:${NEXTCLOUD_VERSION}
    restart: unless-stopped
    user: ${UID}:${GID}
    depends_on:
      - app
    environment:
      - PORT=7867
      - NEXTCLOUD_URL=http://app        # don't go through the proxy to contact the nextcloud server
    entrypoint: /var/www/html/custom_apps/notify_push/bin/x86_64/notify_push /var/www/html/config/config.php
    volumes:
      - ./apps:/var/www/html/custom_apps
      - ./config:/var/www/html/config
    labels:
      - traefik.enable=true
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}_notify_push.entryPoints=web-secure
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}_notify_push.priority=2
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}_notify_push.middlewares=nextcloud_strip_push
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}_notify_push.tls.certresolver=letsencryptresolver
      # necessary for the notify_push app to work:
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}_notify_push.rule=Host(`${DOMAIN}`) && PathPrefix(`/push`)
      - traefik.http.routers.${COMPOSE_PROJECT_NAME}_notify_push.middlewares=nextcloud_striprefix_push@file
      - traefik.http.services.${COMPOSE_PROJECT_NAME}_notify_push.loadbalancer.server.port=7867
    networks:
      - proxy
      - default

  cron:
    image: nextcloud:${NEXTCLOUD_VERSION}
    restart: unless-stopped
    # special UID handling https://github.com/nextcloud/docker/issues/1740
    environment:
      - UID=${UID}
    depends_on:
      - app
    env_file:
      - ./nextcloud.env
    volumes:
      - ./nextcloud:/var/www/html
      - ./apps:/var/www/html/custom_apps
      - ./data:/var/www/html/data
      - ./config:/var/www/html/config
      - ./cron.sh:/cron.sh
    entrypoint: /cron.sh

  db:
    # https://hub.docker.com/_/postgres
    image: postgres:15
    restart: unless-stopped
    user: ${UID}:${GID}
    volumes:
      - ./db:/var/lib/postgresql/data
      - /etc/passwd:/etc/passwd:ro
    environment:
      - POSTGRES_DB_FILE=/run/secrets/postgres_db
      - POSTGRES_USER_FILE=/run/secrets/postgres_user
      - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      start_period: 15s
      interval: 30s
      retries: 3
      timeout: 5s
    secrets:
      - postgres_db
      - postgres_password
      - postgres_user

  redis:
    image: redis:bookworm
    restart: unless-stopped
    # doesn't work so far :(
	  #user: ${UID}:${GID}
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      start_period: 10s
      interval: 30s
      retries: 3
      timeout: 3s

  imaginary:
    image: nextcloud/aio-imaginary:latest
    restart: unless-stopped
    user: ${UID}:${GID}
    expose:
      - "9000"
    depends_on:
      - app	  
    #environment:
    #  - TZ=${TIMEZONE} # e.g. Europe/Berlin
    cap_add:
      - SYS_NICE
    tmpfs:
      - /tmp
networks:
  proxy:
    external: true

secrets:
  nextcloud_admin_password:
    file: ./secrets/nextcloud_admin_password # put admin password in this file
  nextcloud_admin_user:
    file: ./secrets/nextcloud_admin_user     # put admin username in this file
  postgres_db:
    file: ./secrets/postgres_db              # put postgresql db name in this file
  postgres_password:
    file: ./secrets/postgres_password        # put postgresql password in this file
  postgres_user:
    file: ./secrets/postgres_user            # put postgresql username in this file

secrets

Docker secrets is a concept of more secure storage of credentials for Docker applications. Traditionally one would expose credentials like DB password using environment variables which could result this variables are leaked while troubleshooting e.g. when application dump ENV into log files. Docker compose didn’t support secrets earlier and now the implementation is not really sophisticated - the secret is stored as plain file - but this is still better hidden than a simple ENV variable.

The app is build around following secrets stored in a separate “secrets” directory

create a “secret” file for Nextcloud and postgres user and DB as full set of _FILE variables is required by Nextcloud Docker container (you can use random strings as well if you want but it makes it harder)

mkdir secrets 
echo -n "admin" > ./secrets/nextcloud_admin_user
echo -n "nextcloud" > ./secrets/postgres_db
echo -n "nextcloud" > ./secrets/postgres_user
tr -dc 'A-Za-z0-9#$%&+_' < /dev/urandom | head -c 32 | tee ./secrets/postgres_password; echo
tr -dc 'A-Za-z0-9#$%&+_' < /dev/urandom | head -c 32 | tee ./secrets/nextcloud_admin_password; echo

generate secure random password on command line

this procedure might be no really safe cryptographically but should be good enough for most cases - 32 char with letters and some special characters (without problematic like braces, feel free to use [:pirnt:] or [:graph:] classes for more special characters) tr -dc 'A-Za-z0-9#$%&+_' </dev/urandom | head -c 32; echo. using this we generate “secret” file for postgres_password and nextcloud_admin_password

check the contents of folder (ls secrets -w 1)

nextcloud_admin_password  
nextcloud_admin_user  
postgres_db  
postgres_password  
postgres_user

create required mount points

just run the commands, adopt the UID of the user you created in

mkdir apps config data nextcloud db
touch redis-session.ini
sudo chown 1004:1004 apps config data nextcloud db redis-session.ini cron.sh
sudo chmod +x cron.sh

overview of container mounts

you can review the config with docker compose config it will report if there are any error and show expanded variables as docker will use them (especially look at Nextclouds OVERWRITE* and label: > traefik..)
and you could start your cloud first time.

docker compose up -d

I’m using more moder docker compose syntax older implementation instead need docker-compose with a dash, adopt in case it is required.

error message related to notify_push is expected because the app does not exist yet:

Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "/var/www/html/custom_apps/notify_push/bin/x86_64/notify_push": stat /var/www/html/custom_apps/notify_push/bin/x86_64/notify_push: no such file or directory: unknown

it takes a while while all the containers are downloaded and started. don’t be impatient - even after the app container entered “started” state there are some initialization tasks running… wait few more minutes until you are able to access the website and login using the admin user and admin password (./secrets/nextcloud_admin_password).

check status

docker compose ps                          # show containers
docker compose exec redis redis-cli ping   # redis healthcheck
docker compose exec db pg_isready          # postgres healthcheck

initial tasks

few setups steps are not possible (I’m not aware how to perform in a “docker way”) so this configuration steps remain to start using some features.

setup notify_push

run following commands to install and enable notify_push aka “high performance backend for files” - definitely recommended option.

docker compose exec app php occ app:install notify_push
docker compose up -d notify_push
docker compose exec app sh -c 'php occ notify_push:setup https://${OVERWRITEHOST}/push'
# check stats
docker compose exec app php occ notify_push:metrics
docker compose exec app php occ notify_push:self-test

configure system settings

docker compose exec app php occ background:Cron
docker compose exec app php occ config:system:set maintenance_window_start --type=integer --value=1
docker compose exec app php occ config:system:set default_phone_region --value='CH'

setup imaginary

I used the imaginary container from AiO project - it is widely used and should work without issues. in case there are issues only preview generation is and no important functionaly is affected and you can solve issues at your time. for this reason I use :latest docker tag which is not recommended usually.

#review config
docker compose exec app php occ config:system:get enabledPreviewProviders

#enable imaginary
docker compose exec app php occ config:system:set enabledPreviewProviders 0 --value 'OC\Preview\MP3'
docker compose exec app php occ config:system:set enabledPreviewProviders 1 --value 'OC\Preview\TXT'
docker compose exec app php occ config:system:set enabledPreviewProviders 2 --value 'OC\Preview\MarkDown'
docker compose exec app php occ config:system:set enabledPreviewProviders 3 --value 'OC\Preview\OpenDocument'
docker compose exec app php occ config:system:set enabledPreviewProviders 4 --value 'OC\Preview\Krita'
docker compose exec app php occ config:system:set enabledPreviewProviders 5 --value 'OC\Preview\Imaginary'
docker compose exec app php occ config:system:set preview_imaginary_url --value 'http://imaginary:9000'
# limit number of parallel jobs (adopt to your CPU core number)
docker compose exec app php occ config:system:set preview_concurrency_all --value 12
docker compose exec app php occ config:system:set preview_concurrency_new --value 8

install apps

install required apps you need using occ command (faster and more reliable).

docker compose exec app php occ app:install user_oidc
docker compose exec app php occ app:install groupfolders

small list of my recommended apps:

  • twofactor_webauthn
  • polls
  • memories
  • cfg_share_links

logging and troubleshooting

docker compose logs         #show all container logs
docker compose logs -f      #show all container logs and follow them real-time
docker compose logs app     #show logs of app container

Important topics to follow

reverse proxy

this app is intended to run behinnd dedicated reverse proxy which accuires externel TLS certificates and possibly adds another security layer. compose includes “labels” for traefik reverse proxy. If you use it with another proxy you can omit traefik labels and follow respective manuals.

Nextcloud Office (Collabora integration)

I intentionally designed this system to run with separate CODE container. In my eyes a separate CODE is easier to maintain, you have better control about upgrades and more troubleshooting. For installation and customization you can follow native Collabora docs

richdocuments app

richdocuments app is Nextcloud connector used to integrate with WOPI-based Office application. Collabora is preferred but OnlyOffice or others work a swell.This instruction doesn’t apply for Built-In Collabora CODE. In my eyes there is no advantage of running built-in CODE instance. separated instance is easier to setup and troubleshoot.

run following commands in your project folder to connect your NC instance with existing Collabora CODE instance running on https://collabora.mydomain.tld. - adopt with your domain name

docker compose exec app php occ app:install richdocuments
docker compose exec app php occ config:app:set richdocuments wopi_allowlist --value "172.16.0.0/12,fd00:feed:beef::/48"
docker compose exec app php occ config:app:set richdocuments wopi_url --value https://collabora.mydomain.tld
docker compose exec app php occ richdocuments:activate-config

backup / restore / remove

very important topic backup/restore is not covered here. You should implement proper backup strategy including complete outage of your Docker host (offline and off-site backup copy). you should backup your persistent data stored in Docker volume mounts. Database files are not consistent when the system is online so you should perform proper pg backup steps.

following command completely wipe out the installation so you can easily start from scratch or test you restore steps

start from scratch

in case you want to check out your backup strategy you can remove all user and system data and start with clean installation.

remove config and data from mount points

please only continue if you know you want to start from scratch and have proper backup!!

this command silently removes all data and database files!!

# change into your docker compose directory
sudo rm -rf apps config data nextcloud db redis-session.ini cron.sh