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.
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
- Nextcloud AiO
- Your Guide to the Nextcloud All-in-One on Windows 10 & 11!
- Full Nextcloud Docker Container for Raspberry Pi 4 & 5 with Collabora Online server (Nextcloud Office) behind Nginx Reverse Proxy Manager + GoAccess Charts. Supported with Redis Cache + Cron Jobs
@estebanium created an FPM-based variant of this guide - choose the one you prefer
disclaimer
Tough stuff follows and I recommend you to understand all the aspects of your system when you choose to run a self-hosted system.
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 focus on fast setup and skip complexity which is required to setup stable, secure and fast system. if you are willing to follow the more complex path and learn docker-compose way to setup and maintain Nextcloud follow the steps below.
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 an older version e.g. 27. add some files, backup, brake things, review logs, restore. later upgrade to next version play around and continue to the version you finally want to start your production.
preparation / prerequisites
start with following pre-requisites
- system user without login (adopt user ID)
sudo useradd --no-create-home test-nc --shell /usr/sbin/nologin --uid 1004
- working docker-compose
- working reverse proxy (container)
- docker network proxy
(used to connect proxy container with Nextcloud application - adopt in case your setup differs)
docker network create proxy
- directory hosting all configs and data
you will create remaining config files using following steps
.env
global definition of environment variables used for the whole project. Likely this is the only file need to edit.
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. usually no adjustments required.
create a file 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 (some versions of Docker Compose require docker-compose.yaml
as file name)
- 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 (I choose this method as redis is completely isolated and this needs less credential management)
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.- redis runs as UID as well - it only requires a slight change (command: --save “” or mounting container /data)
- 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 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 (first line version is deprecated in new Compose versions):
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 -d `cat $POSTGRES_DB_FILE` -U `cat $POSTGRES_USER_FILE`"]
start_period: 15s
interval: 30s
retries: 3
timeout: 5s
secrets:
- postgres_db
- postgres_password
- postgres_user
redis:
image: redis:bookworm
restart: unless-stopped
command:
- --save ""
#volumes:
#- ./redisdata:/data
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 (which is often exposed in system reports and logs resulting in a leak of the secrets)
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 and secrets
starting the stack
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 modern docker compose
syntax - older implementation use docker-compose
with a dash instead, 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
depending on your internet and system speed it takes a while while all 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 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 setup 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!!
# make sure to change into your docker compose directory
sudo rm -rf apps config data nextcloud db redis-session.ini cron.sh