Template for Nextcloud installation with HPB for Talk using docker

Hi,

I wanted to post this in General but there was no option to use markdown to strucure my post. My original idea was to upload a zip file which is not allowed so I used markdown in support category. Feel free to delete/move this post :wink:

Over the last few months, I used a lot of information from this community to get my Nextcloud installation working. This is why I would like to share with you my template for a Nextcloud docker installation with custom port (not 443), nginx as webserver and HPB for Talk. I also tried to get rid of as many warnings as possible in the Admin page. With Talk version 24.0.3 I get a warning for a missing changed-users capability of my HPB. Maybe that gets fixed with a new version of the strukturag/nextcloud-spreed-signaling image.

Anyway, this template is not designed for production nor should it be used if you have no issue opening port 443/80 on your firewall. Use the AIO image or traefik or Caddy instead.

Feel free to remove any IPv6 or http/3 related entries in the compose.yaml and nginx.conf files if you don’t need it. I had issues with http/3 and Firefox so I removed the exposure of the custom UDP port in the compose.yaml in my installation again.

The following assumes that you have a way of getting valid certificates (I used certboot with DNS challenge and a deployment hook to restart nginx when the certificates get renewed) and that you have a working docker installation with docker compose. Set up fail2ban after the Nextcloud installation for security reasons.

I tested this template with Ubuntu 24.04.4 LTS and I tested a talk session with 4 video clients (LAN, WIFI and mobile internet).

Because I don’t want to use port 443 for my installation, I was not able to use the AIO image for Talk. I used the images: strukturag/nextcloud-spreed-signaling, canyan/janus-gateway and nats instead. These images require various keys which is why I created a template to fill in the secrets, my nextcloud domain and the custom port in the various configuration files.

The template is structured like this:

Nextcloud_Template/

|-- Pre_Install.txt

#Make sure that the docker host has a static IP
#On docker host execute
sudo curl -L https://ssl-config.mozilla.org/ffdhe2048.txt -o /etc/dhparam
sudo sysctl -w vm.overcommit_memory=1

#change directory to Template folder
cd Nextcloud_Template
#Change nextcloud domain and port in file template.txt
nano template.txt
#Generate secrets and keys
cat >> template.txt <<EOF
DOCKER_HOST_IP=$(ip -4 route get 1.1.1.1 | awk '{print $7}')
MYSQL_ROOT_PASSWORD=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)
TURN_SECRET=$(openssl rand -hex 32)
SIGNALLING_SECRET=$(openssl rand -hex 32)
HASH_KEY=$(openssl rand -hex 32)
BLOCK_KEY=$(openssl rand -hex 16)
API_KEY=$(openssl rand -base64 16)
HARP_SECRET=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)
EOF

#Make .sh file executable
chmod +x fillTemplate.sh
#Fill in templates and copy files to Nextcloud installation folder
./fillTemplate.sh templates <NextcloudInstFolder>

|-- fillTemplate.sh

####################################################
# Usage:
# ./copy-configs.sh <source_directory> <target_directory>
####################################################

if [ $# -ne 2 ]; then
    echo "Usage: $0 <source_directory> <target_directory>"
    exit 1
fi

SOURCE="$(realpath "$1")"
TARGET="$(realpath "$2")"

CONFIG_FILE="./template.txt"

# Check input
if [ ! -d "$SOURCE" ]; then
    echo "Source directory not found: $SOURCE"
    exit 1
fi

if [ ! -f "$CONFIG_FILE" ]; then
    echo "Configuration file not found: $CONFIG_FILE"
    exit 1
fi

if ! command -v envsubst >/dev/null 2>&1; then
    echo "envsubst not found."
    echo "Install it with:"
    echo "  sudo apt install gettext"
    exit 1
fi

# Load variables from configuration file
set -a
source "$CONFIG_FILE"
set +a

mkdir -p "$TARGET"

echo "Copying configuration files..."

find "$SOURCE" -type f \( \
    -iname "*.yaml" -o \
    -iname "*.conf" -o \
	-iname "*.txt" -o \
    -iname "*.jcfg" \
\) | while IFS= read -r file
do
    # Determine relative path
    rel_path="${file#$SOURCE/}"
    rel_dir="$(dirname "$rel_path")"
    filename="$(basename "$rel_path")"

    # Create destination directory if necessary
    mkdir -p "$TARGET/$rel_dir"

    ####################################################
    # Process template files
    ####################################################
    if [[ "$filename" == _template_* ]]; then

        # Remove "_template_" prefix from filename
        new_filename="${filename#_template_}"
        target_file="$TARGET/$rel_dir/$new_filename"

        echo "Processing template: $rel_path"

        # Replace variables in template
        envsubst "\$NEXTCLOUD_DOMAIN \$NEXTCLOUD_PORT \$DOCKER_HOST_IP \$MYSQL_ROOT_PASSWORD \$TURN_SECRET \$SIGNALLING_SECRET \$HASH_KEY \$BLOCK_KEY \$API_KEY \$HARP_SECRET" < "$file" > "$target_file"

    ####################################################
    # Copy regular files
    ####################################################
    else

        target_file="$TARGET/$rel_dir/$filename"

        echo "Copying: $rel_path"

        cp -p "$file" "$target_file"

    fi
done

echo
echo "======================================"
echo "All files have been successfully created."
echo "Target directory:"
echo "  $TARGET"
echo "======================================"

|-- template.txt

NEXTCLOUD_DOMAIN=domain.tld
NEXTCLOUD_PORT=0000

`– templates/

|-- _template_Post_Install.txt

#Change directory to compose.yaml file of Nextcloud installation
cd <NextcloudInstFolder>
#Download and start containers
docker compose up -d
###############Please note: notify_push container will crash until notify is installed
####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: error during container init: 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
#Unless you filled in user name and password in compose.yaml, complete the installation by opening the following URL in your browser
https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT} 
######Install notify_push
docker compose exec app php occ app:install notify_push
#Restart notify_push container
docker compose up -d notify_push
#Uncomment /push location in nginx.conf and reload nginx
nano nginx.conf
docker compose exec web /bin/bash -c "nginx -s reload"
#Run setup of notify_push
docker compose exec app php occ notify_push:setup https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}/push
#Get rid of warnings in admin page
#Set the region to your country code
docker compose exec app php occ config:system:set default_phone_region --value="XX"
docker compose exec app php occ config:system:set maintenance_window_start --type=integer --value=1
docker compose exec app php occ maintenance:repair --include-expensive
docker compose exec app php occ config:system:set serverid --type=integer --value=1
#This will throw an unhandled exeception in version 34.0.2, which can be ignored
docker compose exec app php occ background:Cron
#####Configure HaRP
https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}/settings/admin/app_api
HaRP Proxy (Host)
docker-install
HaRP-Host: ${DOCKER_HOST_IP}:8780
Shared Secret: ${HARP_SECRET}
Nextcloud-URL: https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}
FRP-Server Address: ${DOCKER_HOST_IP}:8782
#####Install Talk app and go to settings
https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}/settings/admin/talk
#Enable HPB in Talk Settings:
High performace backend
URL: https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}/standalone-signaling
Shared Secret: ${SIGNALLING_SECRET}
#SCROLL down to STUN and TURN
#STUN:
https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}
#TURN:
${NEXTCLOUD_DOMAIN}:3478
Turn Secret: ${TURN_SECRET}
#####Setup Nextcloud Office
https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}/settings/admin/richdocuments
Select Built-In CODE server
#In order to get rid of the WOIP warning, you can also inspect the docker network used in compose.yaml to get the exact networks
#docker network list , docker network inspect <NextcloudInstFolder>_nextcloud_net
#This will work for default docker networks
172.16.0.0/12,fc00::/7
#####Delete nextcloud.log because of the failed Notify installation
docker compose exec app php occ maintenance:mode --on
sudo rm data/nextcloud.log
docker compose exec app php occ maintenance:mode --off

###Finally open ports on your internet facing firewall and forward to ${DOCKER_HOST_IP}
${NEXTCLOUD_PORT} TCP
${NEXTCLOUD_PORT} UDP for http/3
3478 TCP for Talk
3478 UDP for Talk

|-- _template_compose.yaml

networks:
 nextcloud_net:
  enable_ipv6: true
  ipam:
    driver: default
volumes:
  db_host:
services:
  db:
    #Check recommended version at https://docs.nextcloud.com/server/stable/admin_manual/installation/system_requirements.html
    image: mariadb:11.8
    container_name: nextcloud-hpb-mariadb
    networks:
     nextcloud_net:
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
    volumes:
      - db_host:/var/lib/mysql
      - /etc/localtime:/etc/localtime:ro
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} #Usefull to export database for backup
      - MARIADB_AUTO_UPGRADE=1
      - MARIADB_DISABLE_UPGRADE_BACKUP=1
      - MYSQL_PASSWORD=mysql
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
    cap_add:
      - SYS_ADMIN

  app:
    image: nextcloud:fpm #alpine image does not include glibc needed for Built-In CODE server (Nextcloud Office) 
    container_name: nextcloud-hpb-app
    networks:
     nextcloud_net:
    restart: unless-stopped
    depends_on:
      - db
      - redis
    volumes:
      - ./html:/var/www/html:z
      - ./config:/var/www/html/config:z
      - ./data:/var/www/html/data:z
      - /etc/localtime:/etc/localtime:ro
    environment:
      - REDIS_HOST=redis
      - MYSQL_PASSWORD=mysql
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_HOST=db
#      - NEXTCLOUD_ADMIN_USER=xxx
#      - NEXTCLOUD_ADMIN_PASSWORD=*******
      - TRUSTED_PROXIES=172.16.0.0/12 ##For fail2ban and notify_push, default for docker installation unless changed
      - NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}
      - OVERWRITECLIURL=https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}
      # php
      - PHP_MEMORY_LIMIT=1G
      - PHP_UPLOAD_LIMIT=10G

  cron:
    image: nextcloud:fpm
    container_name: nextcloud-hpb-cron
    networks:
     nextcloud_net:
    restart: unless-stopped
    depends_on:
      - app
    volumes_from:
      - app
      # NOTE: The `volumes` config of the `cron` and `app` containers must match
    entrypoint: /cron.sh
  
  web:
    image: nginx
    container_name: nextcloud-hpb-proxy
    networks:
     nextcloud_net:
    restart: unless-stopped
    ports:
     - ${NEXTCLOUD_PORT}:443
     - ${NEXTCLOUD_PORT}:443/udp    #Needed for http/3, see nginx.conf, comment out if you have issues with your Browser 
    depends_on:
      - app
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - /etc/letsencrypt/:/etc/nginx/certs/:ro
      - /etc/dhparam:/etc/dhparam:ro
    volumes_from:
      - app
      
  harp:
    image: ghcr.io/nextcloud/nextcloud-appapi-harp:release
    container_name: nextcloud-hpb-harp
    network_mode: host
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./certs:/certs
    environment:
      - HP_SHARED_KEY=${HARP_SECRET}
      - NC_INSTANCE_URL=https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}
      - HP_EXAPPS_ADDRESS=${DOCKER_HOST_IP}:8780
   
  redis:
    image: redis:alpine
    container_name: nextcloud-hpb-memcache
    networks:
     nextcloud_net:
    restart: unless-stopped
#    command: redis-server --requirepass ****  # notify_push does not work with redis password set

  notify_push:
    image: nextcloud:fpm
    container_name: nextcloud-hpb-notify
    networks:
     nextcloud_net:
    restart: unless-stopped
    depends_on:
      - app
    environment:
      - PORT=7867
      - NEXTCLOUD_URL=https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}  #Needs to point to nginx
    entrypoint: /var/www/html/custom_apps/notify_push/bin/x86_64/notify_push /var/www/html/config/config.php
    volumes_from:
      - app
  nats:
    image: nats:latest
    container_name: nextcloud-hpb-nats-server
    network_mode: host
    restart: unless-stopped
    command: ["-c", "/config/nats.conf"]
    volumes:
      - ./hpb/config/nats.conf:/config/nats.conf:ro
      
  janus:
    image: canyan/janus-gateway:latest
    container_name: nextcloud-hpb-janus-gateway
    network_mode: host
    restart: unless-stopped
    depends_on:
      - coturn
      
    volumes:
      - ./hpb/config/janus/janus.jcfg:/usr/local/etc/janus/janus.jcfg
      - ./hpb/config/janus/janus.eventhandler.wsevh.jcfg:/usr/local/etc/janus/janus.eventhandler.wsevh.jcfg
      - ./hpb/config/janus/janus.transport.websockets.jcfg:/usr/local/etc/janus/janus.transport.websockets.jcfg
      - ./hpb/config/janus/janus.plugin.videoroom.jcfg:/usr/local/etc/janus/janus.plugin.videoroom.jcfg
   
  coturn:
    image: coturn/coturn
    container_name: nextcloud-hpb-turn
    network_mode: host
    restart: unless-stopped
    volumes:
      - ./hpb/config/turnserver.conf:/etc/turnserver.conf:ro
      
  signaling:
    image: strukturag/nextcloud-spreed-signaling:latest
    container_name: nextcloud-hpb-spreed-signaling
    network_mode: host
    restart: unless-stopped    
    depends_on:
      - nats
      - janus
      - coturn
    volumes:
      - ./hpb/config/server.conf:/config/server.conf:ro
    environment:
      - CONFIG_FILE=/config/server.conf

|-- _template_nginx.conf

# Nextcloud nginx configuration — root installation
# Version 2026-03-26

# PHP-FPM backend.
worker_processes auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}

http {
    
	map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
	}
	resolver 127.0.0.11;
	
	log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

	upstream php-handler {
	# Use one of the options below, not both:
	server app:9000;
	#server unix:/run/php/php8.2-fpm.sock;
	}

	# Set the `immutable` cache control options only for assets with a cache busting `v` argument
	map $arg_v $asset_immutable {
		"" "";
		default ", immutable";
	}

	server {
	    #listen 80;
		#listen [::]:80;            # comment to disable IPv6
		#if ($scheme = "http") {
		#	return 301 https://$host$request_uri;
		#}
		#if ($http_x_forwarded_proto = "http") {
		#	return 301 https://$host$request_uri;
		#}
		#listen 443 ssl http2;
		#listen [::]:443 ssl http2;
		# With NGinx >= 1.25.1 you should use this instead:
		listen 443      ssl;
		listen [::]:443 ssl;
		http2 on;
		listen 443 quic;       # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ - please remove "reuseport" if there is already another quic listener on port 443 with enabled reuseport
		listen [::]:443 quic;  # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ - please remove "reuseport" if there is already another quic listener on port 443 with enabled reuseport - keep comment to disable IPv6
		http3 on;                                 # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+
		quic_gso on;                              #OPTIONAL for http3, uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+
		quic_retry on;                            #OPTIONAL for http3, uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+
		# quic_bpf on;                              # improves  HTTP/3 / QUIC - supported on nginx v1.25.0+, if nginx runs as a docker container you need to give it privileged permission to use this option
		add_header Alt-Svc 'h3=":${NEXTCLOUD_PORT}"; ma=86400'; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+

		http3_stream_buffer_size 512k; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+
		
		server_name ${NEXTCLOUD_DOMAIN};

		# Path to the root of your installation
		root /var/www/html;

		# Use Mozilla's guidelines for SSL/TLS settings
		# https://mozilla.github.io/server-side-tls/ssl-config-generator/
		ssl_certificate      /etc/nginx/certs/live/${NEXTCLOUD_DOMAIN}/fullchain.pem;
		ssl_certificate_key  /etc/nginx/certs/live/${NEXTCLOUD_DOMAIN}/privkey.pem;
		
		ssl_dhparam /etc/dhparam; # curl -L https://ssl-config.mozilla.org/ffdhe2048.txt -o /etc/dhparam

		ssl_early_data on;
		ssl_session_timeout 1d;
		ssl_session_cache shared:SSL:10m;

		ssl_protocols TLSv1.2 TLSv1.3;
		ssl_ecdh_curve x25519:x448:secp521r1:secp384r1:secp256r1;

		ssl_prefer_server_ciphers on;
		ssl_conf_command Options PrioritizeChaCha;
		ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256;
	

		# Prevent nginx HTTP Server Detection
		server_tokens off;

		# HSTS settings
		# WARNING: Only add the preload option once you read about
		# the consequences in https://hstspreload.org/. This option
		# will add the domain to a hardcoded list that is shipped
		# in all major browsers and getting removed from this list
		# could take several months.
		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

		# set max upload size and increase upload timeout:
		client_max_body_size 0;
		client_body_timeout 300s;
		fastcgi_buffers 64 4K;

		# Proxy and client response timeouts
		# Uncomment an increase these if facing timeout errors during large file uploads
		#keepalive_timeout 60s;
		#proxy_connect_timeout 60s;
		#proxy_send_timeout 60s;
		#proxy_read_timeout 60s;
		#send_timeout 60s;

		# Enable gzip but do not remove ETag headers
		gzip on;
		gzip_vary on;
		gzip_comp_level 4;
		gzip_min_length 256;
		gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
		gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

		# Pagespeed is not supported by Nextcloud, so if your server is built
		# with the `ngx_pagespeed` module, uncomment this line to disable it.
		#pagespeed off;

		# The settings allows you to optimize the HTTP2 bandwidth.
		# See https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/
		# for tuning hints
		client_body_buffer_size 512k;

		# HTTP response headers borrowed from Nextcloud `.htaccess`
		add_header Referrer-Policy                   "no-referrer"       always;
		add_header X-Content-Type-Options            "nosniff"           always;
		add_header X-Frame-Options                   "SAMEORIGIN"        always;
		add_header X-Permitted-Cross-Domain-Policies "none"              always;
		add_header X-Robots-Tag                      "noindex, nofollow" always;

		# Remove X-Powered-By, which is an information leak
		fastcgi_hide_header X-Powered-By;

		# Set .mjs and .wasm MIME types
		# Either include it in the default mime.types list
		# and include that list explicitly or add the file extension
		# only for Nextcloud like below:
		include mime.types;
		types {
			text/javascript mjs;
		#	application/wasm wasm;
		}

		# Specify how to handle directories -- specifying `/index.php$request_uri`
		# here as the fallback means that Nginx always exhibits the desired behaviour
		# when a client requests a path that corresponds to a directory that exists
		# on the server. In particular, if that directory contains an index.php file,
		# that file is correctly served; if it doesn't, then the request is passed to
		# the front-end controller. This consistent behaviour means that we don't need
		# to specify custom rules for certain paths (e.g. images and other assets,
		# `/updater`, `/ocs-provider`), and thus
		# `try_files $uri $uri/ /index.php$request_uri`
		# always provides the desired behaviour.
		index index.php index.html /index.php$request_uri;

		# Rule borrowed from `.htaccess` to handle Microsoft DAV clients
		location = / {
			if ( $http_user_agent ~ ^DavClnt ) {
				return 302 /remote.php/webdav/$is_args$args;
			}
		}

		location = /robots.txt {
			allow all;
			log_not_found off;
			access_log off;
		}

		# Make a regex exception for `/.well-known` so that clients can still
		# access it despite the existence of the regex rule
		# `location ~ /(\.|autotest|...)` which would otherwise handle requests
		# for `/.well-known`.
		fastcgi_intercept_errors off;

		location ^~ /.well-known {
        # The rules in this block are an adaptation of the rules
        # in the Nextcloud `.htaccess` that concern `/.well-known`.

		# CalDAV & CardDAV
		location = /.well-known/carddav  { return 301 https://$host:${NEXTCLOUD_PORT}/remote.php/dav/; }
		location = /.well-known/caldav   { return 301 https://$host:${NEXTCLOUD_PORT}/remote.php/dav/; }

		# Webfinger & Nodeinfo (Nextcloud 26+ / 28+ / 29)
		location = /.well-known/webfinger { return 301 https://$host:${NEXTCLOUD_PORT}/index.php/.well-known/webfinger; }
		location = /.well-known/nodeinfo  { return 301 https://$host:${NEXTCLOUD_PORT}/index.php/.well-known/nodeinfo; }

		# ACME & PKI
		location /.well-known/acme-challenge { try_files $uri $uri/ =404; }
		location /.well-known/pki-validation { try_files $uri $uri/ =404; }

		# Fallback
		return 301 https://$host:${NEXTCLOUD_PORT}/index.php$request_uri;

		}

		# Rules borrowed from `.htaccess` to hide certain paths from clients
		location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)  { return 404; }
		location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console)                { return 404; }

		# Pass PHP requests to PHP-FPM.
		#
		# Important: this block must appear above the static asset locations
		# below. Those locations fall back to `/index.php$request_uri`; if
		# they appear first, nginx can repeatedly rewrite to `/index.php`,
		# causing an internal redirection loop.
		location ~ \.php(?:$|/) {
			# Rewrite most PHP requests to Nextcloud's front controller (`/index.php`).
			#
			# (Mirrors the rewrite exceptions in Nextcloud's Apache .htaccess.)
			#
			# Exceptions (not rewritten; must remain directly reachable):
			#   index.php, remote.php, public.php, cron.php, status.php
			#   ocs/v1.php, ocs/v2.php, ocs-provider/*
			#   core/ajax/update.php, updater/*
			#   */richdocumentscode(_arm64)?/proxy
			#
			# Other exceptions (e.g. /.well-known) are handled by dedicated
			# location blocks elsewhere in this config.
			#
			# Caution: small edits to this regex can break routing or introduce
			# rewrite loops.
			rewrite ^/(?!index|remote|public|cron|status|ocs\/v[12]|ocs-provider\/.+|core\/ajax\/update|updater\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri;

			# Split `/file.php/path/info` into:
			# - $fastcgi_script_name: `/file.php`
			# - $fastcgi_path_info:   `/path/info`
			#
			# This is required for entry-points such as `remote.php` and `public.php`,
			# which route requests based on PATH_INFO.
			fastcgi_split_path_info ^(.+?\.php)(/.*)$;
			set $path_info $fastcgi_path_info;    # Save before try_files resets it

			# Return 404 for nonexistent PHP scripts (avoids passing arbitrary
			# paths to PHP-FPM, which is a known security risk).
			try_files $fastcgi_script_name =404;

			include fastcgi_params;
			fastcgi_pass php-handler;

			fastcgi_param SCRIPT_FILENAME            $document_root$fastcgi_script_name;
			fastcgi_param PATH_INFO                  $path_info;
			fastcgi_param HTTPS                      on;       # Assumes TLS terminates here
			fastcgi_param modHeadersAvailable        true;     # Avoid duplicate security headers
			fastcgi_param front_controller_active    true;     # Enable pretty URLs

			# Let nginx handle HTTP error responses from PHP-FPM (e.g. custom
			# error pages). Disable for debugging if PHP errors are being hidden.
			fastcgi_intercept_errors on;

			# Required for uploads: PHP-FPM does not support chunked
			# transfer encoding and needs a Content-Length header.
			fastcgi_request_buffering on;

			# Optional PHP-FPM timeout tuning (e.g. for 504 response timeouts).
			# Increase these only if uploads or long-running PHP requests are
			# timing out in your environment.
			#fastcgi_read_timeout 60s;
			#fastcgi_send_timeout 60s;
			#fastcgi_connect_timeout 60s;

			# Disable on-disk buffering of FastCGI responses (reduces disk I/O at
			# the cost of holding responses in memory).
			fastcgi_max_temp_file_size 0;
		}

		# Serve static files
		location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac|mp4|webm)$ {
			try_files $uri /index.php$request_uri;
			# HTTP response headers borrowed from Nextcloud `.htaccess`
			add_header Cache-Control                     "public, max-age=15778463$asset_immutable";
			add_header Referrer-Policy                   "no-referrer"       always;
			add_header X-Content-Type-Options            "nosniff"           always;
			add_header X-Frame-Options                   "SAMEORIGIN"        always;
			add_header X-Permitted-Cross-Domain-Policies "none"              always;
			add_header X-Robots-Tag                      "noindex, nofollow" always;
			access_log off;     # Optional: Don't log access to assets
		}

		location ~ \.(otf|woff2?)$ {
			try_files $uri /index.php$request_uri;
			expires 7d;         # Cache-Control policy borrowed from `.htaccess`
			access_log off;     # Optional: Don't log access to assets
		}

		# Rule borrowed from `.htaccess`
		location /remote {
			return 301 /remote.php$request_uri;
		}
		location /exapps/ {
			proxy_pass http://${DOCKER_HOST_IP}:8780;
			proxy_set_header Host $host:${NEXTCLOUD_PORT};
			proxy_set_header X-Real-IP $remote_addr;
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_set_header X-Forwarded-Proto $scheme;
		}

		location / {
			try_files $uri $uri/ /index.php$request_uri;
		}
		
#		location ^~ /push/ {
#			proxy_pass http://notify_push:7867/;
#			proxy_http_version 1.1;
#			proxy_set_header Upgrade $http_upgrade;
#			proxy_set_header Connection "Upgrade";
#			proxy_set_header Host $host:${NEXTCLOUD_PORT};
#			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#		}
		
		location /standalone-signaling/ {
			proxy_pass http://${DOCKER_HOST_IP}:8081/;
			proxy_http_version 1.1;
			
			# WebSocket support
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection "upgrade";
			
			# Headers
			proxy_set_header Host $host:${NEXTCLOUD_PORT};
			proxy_set_header X-Real-IP $remote_addr;
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_set_header X-Forwarded-Proto $scheme;
			
			# Long-lived connections for WebSocket
			proxy_connect_timeout 7d;
			proxy_send_timeout 7d;
			proxy_read_timeout 7d;
		}

	}
}

`– hpb/

`– config/

|-- _template_server.conf

[http]
listen = 0.0.0.0:8081

[app]
# Shared secret - must match what you configure in Nextcloud Talk settings
secret = ${SIGNALLING_SECRET}

[sessions]
# Hash key: 64 hex characters (32 bytes)
hashkey = ${HASH_KEY}
# Block key: MUST be exactly 32 hex characters (16 bytes), 48 (24 bytes), or 64 (32 bytes)
blockkey = ${BLOCK_KEY}

[nats]
url = nats://127.0.0.1:4222

[mcu]
type = janus
url = ws://127.0.0.1:8188
#Set bitrate to 5MB/s = 5 * 1024 * 1024
maxstreambitrate = 5242880
maxscreenbitrate = 5242880

[backend]
# List all your Nextcloud backends here
backends = backend1

# Backend 1
[backend1]
url = https://${NEXTCLOUD_DOMAIN}:${NEXTCLOUD_PORT}
# This secret must match the signaling secret above
secret = ${SIGNALLING_SECRET}

# TURN server configuration
[turn]
apikey = ${API_KEY}
secret = ${TURN_SECRET}
servers = turn:${NEXTCLOUD_DOMAIN}:3478?transport=udp,turn:${NEXTCLOUD_DOMAIN}:3478?transport=tcp

|-- _template_turnserver.conf

# Please find the complete file at https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf
# TURN listener port for UDP and TCP (Default: 3478).
# Note: actually, TLS & DTLS sessions can connect to the
# "plain" TCP & UDP port(s), too - if allowed by configuration.
#
listening-port=3478

# Lower and upper bounds of the UDP relay endpoints:
# (default values are 49152 and 65535)
#
min-port=20000
max-port=40000

# Uncomment to use fingerprints in the TURN messages.
# By default the fingerprints are off.
#
fingerprint

# Use either lt-cred-mech or use-auth-secret in the conf
# to avoid any confusion.
#
use-auth-secret

# 'Static' authentication secret value (a string) for TURN REST API only.
# If not set, then the turn server
# will try to use the 'dynamic' value in the turn_secret table
# in the user database (if present). The database-stored  value can be changed on-the-fly
# by a separate program, so this is why that mode is considered 'dynamic'.
#
static-auth-secret=${TURN_SECRET}

# Note: If the default realm is not specified, then realm falls back to the host domain name.
#       If the domain name string is empty, or set to '(None)', then it is initialized as an empty string.
#
realm=${NEXTCLOUD_DOMAIN}

# It defaults to 600 secs (10 min) if no value is provided. After that delay,
# the client will get 438 error and will have to re-authenticate itself.
#
stale-nonce=600



`– janus/

|-- _template_janus.jcfg

general: {
    configs_folder = "/usr/local/etc/janus"
    plugins_folder = "/usr/local/lib/janus/plugins"
    transports_folder = "/usr/local/lib/janus/transports"
    events_folder = "/usr/local/lib/janus/events"
    loggers_folder = "/usr/local/lib/janus/loggers"
    debug_level = 4
    log_to_stdout = true
}

nat: {
    stun_server = "127.0.0.1"
	stun_port = 3478
    ice_lite = false
    ice_tcp = false
    full_trickle = true
	turn_rest_api_key = "${API_KEY}"
}

media: {
    ipv6 = false
	rtp_port_range = "20000-40000"
}

plugins: {
}

transports: {
}

|-- janus.eventhandler.wsevh.jcfg

# This configures the WebSockets event handler. Since this plugin only
# forwards each event it receives via WebSockets, you simply need to
# configure (i) which events to subscribe to, and (ii) the address of
# the WebSockets server which will receive the requests.

general: {
	enabled = true		# By default the module is not enabled
	events = "all"		# Comma separated list of the events mask you're interested
						# in. Valid values are none, sessions, handles, jsep, webrtc,
						# media, plugins, transports, core, external and all. By
						# default we subscribe to everything (all)
	grouping = true		# Whether events should be sent individually (one per
						# HTTP POST, JSON object), or if it's ok to group them
						# (one or more per HTTP POST, JSON array with objects)
						# The default is 'yes' to limit the number of connections.

	json = "compact"	# Whether the JSON messages should be indented (pretty),
						# plain (no indentation) or compact (no indentation and no spaces)

						# Address the plugin will send all events to as WebSocket
						# messages. In case authentication is required to contact
						# the backend, set the credentials as well.
	backend = "ws://127.0.0.1:8188"
	# subprotocol = "your-subprotocol"

						# If the WebSocket server isn't reachable or the client has
						# to reconnect, the default behaviour of the handler plugin
						# is to retry with an exponential back-off, all while buffering
						# events that Janus may keep on pushing. Buffering has no
						# limit, so if reconnecting takes a long time (or forever)
						# memory usage will keep on growing; besides, it may cause
						# a network spike when eventually reconnected, as all stored
						# events would need to be sent to the backend before new
						# ones can be relayed as well. You can prune queued events
						# and put a cap on the amount of buffering to perform when
						# reconnecting by setting the 'events_cap_on_reconnect'
						# property accordingly: any number you set will be the
						# maximum number of events stored in memory until we
						# reconnect, which means older packets will be discarded
						# if the cap is exceeded. Notice that setting a value
						# of 0 will not mean "drop all packets", but will disable
						# the cap (default behaviour), which means the minimum
						# possible value is 1. Also notice that, when the cap is
						# enabled, this means the event receiver may end up missing key
						# events when a reconnection actually ends up taking place.
	# events_cap_on_reconnect = 10

						# In case you need to debug connection issues, you can configure
						# the libwebsockets debugging level as a comma separated list of things
						# to debug, supported values: err, warn, notice, info, debug, parser,
						# header, ext, client, latency, user, count (plus 'none' and 'all')
	#ws_logging = "err,warn"
}

|-- janus.plugin.videoroom.jcfg

general: {
#    admin_key = ""
}

room-1234: {
    description = "Demo Room"
    publishers = 6
    bitrate = 128000
    fir_freq = 10
    audiocodec = "opus"
    videocodec = "vp8"
    record = false
}

`– janus.transport.websockets.jcfg

general: {
	json = "compact"				# Whether the JSON messages should be indented (pretty),
									# plain (no indentation) or compact (no indentation and no spaces)
	ws = true						# Whether to enable the WebSockets API
	ws_port = 8188					# WebSockets server port
	#ws_interface = "eth0"			# Whether we should bind this server to a specific interface only
	ws_ip = "127.0.0.1"			# Whether we should bind this server to a specific IP address only
	#ws_unix = "/run/ws.sock"		# Use WebSocket server over UNIX socket instead of TCP
	wss = false						# Whether to enable secure WebSockets
}

admin: {
    admin_ws = true
    admin_ws_port = 7188
}

All files with _template_ need to be adjusted before starting the container up for the first time.

The images used for the HPB need to open UDP ports for the audio/video stream dynamically. When exposing a large port range with docker you might get performance issues (in my environment docker simply crashed every time) which is why I used the host network mode of docker. When using this mode you cannot use localhost, 127.0.0.1, the docker host name nor the name of the container when communicating with the containers running in bridge mode. What worked for me is using the IP of the docker host instead. Adjust your domain and the custom port in the file templates.txt and execute the following:

cat >> template.txt <<EOF

DOCKER_HOST_IP=$(ip -4 route get 1.1.1.1 | awk ‘{print $7}’)

MYSQL_ROOT_PASSWORD=$(openssl rand -base64 48 | tr -dc ‘A-Za-z0-9’ | head -c 32)

TURN_SECRET=$(openssl rand -hex 32)

SIGNALLING_SECRET=$(openssl rand -hex 32)

HASH_KEY=$(openssl rand -hex 32)

BLOCK_KEY=$(openssl rand -hex 16)

API_KEY=$(openssl rand -base64 16)

HARP_SECRET=$(openssl rand -base64 48 | tr -dc ‘A-Za-z0-9’ | head -c 32)

EOF

See also Pre_Install.txt for details. I used the envsubst command to fill in the variables in the files. Execute

fillTemplate.sh templates

and change directory to .
See Post_Install.txt for details.