Add a signaling server to Nextcloud Talk

I’ve been using Nextcloud Talk for video-conferences on my Raspberry-Pi-hosted instance for a while now and I like the general user-friendliness of the app. But as soon as more than three people join, the connection tends to get unstable. Participants drop out, experience long lags or can only see a fraction of the call participants.

Luckily, StrukturAG has now released the source code for a high performance stand-alone signaling server and there are a number of excellent instructions out there to help set it up. I’ve given it a try and improved my platform with a TURN server and the signaling server. The below is a summary of how I managed to get it to work and a description of my current setup. Please feel free to comment if you have any feedback or questions.

A word of warning: the installation process was not trivial and I ran into several issues while following guides on the internet. Some of them that are likely to occur on a Raspberry Pi I have summarised in the troubleshooting section below. But depending on your setup, you might have to tweak your settings to get the preferred outcome. I should also note that my hardware only meets the minimum requirements when it comes to performance (see below). If you need better performance it might be worth considering a different device than a Raspberry Pi.


General setup and requirements

In this guide I will assume that you already have a working Nextcloud instance with Nextcloud Talk and a TURN server on a Raspberry Pi (RPI 1). I have included links to step-by-step instructions so that you can set these up if needed.

The signaling server will be installed on a separate Raspberry Pi (RPI 2) running Ubuntu Server following the instructions below. Alternatively, it is also possible to install the server on the Raspberry Pi with the Nextcloud instance.

  • A working NextcloudPi installation on a Raspberry Pi 4 with 8 GB RAM (RPI 1), accessible online via a domain. Here are the official instructions on how to set it up.
  • Nextcloud Talk installed on RPI 1.
  • A working TURN server on RPI 1. I followed the step-by-step installation guide on the Nextcloud Talk website.
  • A separate Raspberry Pi 4 with 8 GB RAM (RPI 2), running Ubuntu Server 20.04 LTS. Please note that the Janus version installed on older distributions will not work. A handy guide on how to install Ubuntu Server on a Raspberry Pi can be found here.
  • A separate domain for the signaling server. Let’s call it

Preparing the Raspberry Pi

Make sure that you have installed Ubuntu Server 20.04 LTS or above as per the instructions on the Ubuntu website. Now we will need to add a few more bits and pieces to get our signaling server to work. We’ll need working configurations for the nginx web server, MariaDB, PHP and LetsEncrypt to enable an encrypted connection via HTTPS.

As always, we will start with an update of the Ubuntu operating system.

sudo apt update && apt upgrade -V && apt dist-upgrade && apt autoremove 
reboot now

Install nginx

Nginx is available in the Ubuntu package repository and you can easily install it with the following command:

sudo apt-get install nginx

Quickly make sure that its all up and running:

sudo systemctl status nginx

You can now test your web server in your Browser by typing your device IP. If it installed successfully you will see the “Welcome to nginx!” message. In this guide, I will use as an example. If you don’t know your IP, you can find it using the ip addr show command (further details here).

Install MariaDB

Next we will setup the database. For Raspbian as well as Ubuntu, MariaDB is recommended, which was derived from MySQL and is open source – further details can be found in the Knowledge Base. MariaDB is available in the Ubuntu repository, so the installation is straightforward:

sudo apt install mariadb-server

Install PHP packages

Next we will install the following PHP packages:

sudo apt install php-fpm php-gd php-mysql php-curl php-xml php-zip php-intl php-mbstring php-bz2 php-json php-apcu php-imagick php-gmp php-bcmath

Install LetsEncrypt using

To be able to access the website securely using HTTPS we will need to install SSL certificates. The easiest way to do this is via LetsEncrypt (which is also used during the Nextcloud installation) and the installation script I was following the excellent step-by-step instructions on to complete this step.

To avoid using with root privileges, we will add a separate user to deal with all our encryption needs. When queried, you do not need to specify a password, address or email. Just press enter to leave the fields empty.

sudo adduser letsencrypt

Add the user to the www-data group, which is the main user for the web server.

sudo usermod -a -G www-data letsencrypt

Next we add some more configuration to allow letsencrypt to reload nginx without a root password.


Add the following code to the end of the file that just opened:

letsencrypt ALL=NOPASSWD: /bin/systemctl reload nginx.service

Now change to the user letsencrypt.

su - letsencrypt

Before going ahead with the installation it is recommended to check your crontab file (especially if you are not using the user letsencrypt), as will automatically overwrite the existing crontab and delete any jobs that are already scheduled. Type the code below and if any crontabs are listed, make a note of them and add them again later using the sudo crontab -e command.

sudo crontab -l > crontab.bak

Now we can start the installation:

curl | sh

When the installation is done, don’t forget to exit to your normal user by typing:


Configure nginx

Now that we have installed all the required software, we still need to add configuration. We’ll start with the nginx web server. Open the configuration file like this:

sudo nano /etc/nginx/nginx.conf

Make sure the check the following entries:

  • user: should always be www-data
  • worker_processes: defines the number of threads to use for client requests, should be auto
  • server_tokens off: this should be active in the http section to ensure that nginx does not pass on version information

Reload nginx to apply the new settings.

sudo service nginx restart

Configure MariaDB

The MariaDB configuration is fairly straight-forward. Type the following command and answer all questions with yes to ensure the highest level of security.

sudo mysql_secure_installation

Configure PHP

The PHP configurations will be a little bit more involved. For one, we have to check the main configuration file which can be found here:

sudo nano /etc/php/7.4/fpm/pool.d/www.conf

Have a look at the following items and ensure they are all correct and activated (remove the semicolon), as per the below:

user = www-data
group = www-data
listen = /run/php/php7.4-fpm.sock
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp

Next edit the php.ini file which contains general settings for PHP and the FastCGI Process Manager that defines the threads and how they are processed.

sudo nano /etc/php/7.4/fpm/php.ini

Ensure the following settings are correct:

cgi.fix_pathinfo = 0
memory_limit = 512M
opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 1
opcache.save_comments = 1

Finally, ensure the second php.ini file in the cli folder is also correct:

sudo nano /etc/php/7.4/cli/php.ini

In this case we only need to edit:

cgi.fix_pathinfo = 0

Now the PHP service has to be restarted to apply these new settings.

sudo service php7.4-fpm restart

Generate certificates for HTTPS

First we need to create a few directories and set the appropriate permissions to prepare the Raspberry Pi for the generation of the LetsEncrypt certificates. Make sure you enter your own domain details instead of

mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
chown -R www-data:www-data /var/www/letsencrypt
chmod -R 775 /var/www/letsencrypt
mkdir -p /etc/letsencrypt/
mkdir -p /etc/letsencrypt/
chown -R www-data:www-data /etc/letsencrypt
chmod -R 775 /etc/letsencrypt

The acme-challenge folder will be used for a connection test on the webserver and LetsEncrypt can use it to validate a successful connection with the domain and generate the certificates. The /etc/letsencrypt directory will contain the actual certificates, divided into RSA and ECDSA certificates.

Now we have to create the Virtual Host for nginx via a http gateway to install the certificates.

sudo nano /etc/nginx/conf.d/letsencrypt.conf

Copy the following code into the file. The configuration will ensure that the server listens at port 80, which is the default port for http, and connect the local IP to the domain for the signaling server. The specified location is the acme-challenge folder we previously generated, which we will now use for the connection test. Note that all traffic outside of this directory is automatically rerouted to https to ensure an encrypted connection as soon as the certificates are generated.

upstream php-handler {
    server unix:/run/php/php7.4-fpm.sock;

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    root /var/www;

    location ^~ /.well-known/acme-challenge {
        default_type text/plain;
        root /var/www/letsencrypt;

	location / {
		return 301 https://$host$request_uri;

Now reload the nginx web server.

sudo service nginx restart

Now we can test the web server connection. I do not recommend to skip this step. The connection should be working before attempting to generate the certificates with LetsEncrypt, as you only have a number of attempts to get it right before LetsEncrypt will block you from generating any more certificates for the same domain. After this you will have to wait for a week before you can use the service again.

echo "testrun" >> /var/www/letsencrypt/.well-known/acme-challenge/test.txt

You can now enter the following URL in your Browser to see if it worked:

Ideally, you should see the line “testrun” in your Browser. If you get an error during this step, have a look at the troubleshooting section below.

As soon as this step is working, you can remove the test.txt file you just generated.

sudo rm /var/www/letsencrypt/.well-known/acme-challenge/test.txt

Now we are ready to generate the certificates using LetsEncrypt. For this we first need to change into the letsencrypt user.

su - letsencrypt

Use the following code to generate the RSA and ECDSA certificates with the acme script and reload the service.

sudo --issue -d --keylength 4096 -w /var/www/letsencrypt --key-file /etc/letsencrypt/ --ca-file /etc/letsencrypt/ --cert-file /etc/letsencrypt/ --fullchain-file /etc/letsencrypt/ --reloadcmd "sudo /bin/systemctl reload nginx.service"
sudo --issue -d --keylength ec-384 -w /var/www/letsencrypt --key-file /etc/letsencrypt/ --ca-file /etc/letsencrypt/ --cert-file /etc/letsencrypt/ --fullchain-file /etc/letsencrypt/ --reloadcmd "sudo /bin/systemctl reload nginx.service"

Now we can change back to the standard user. The certificates will be automatically renewed every 90 days.


Configure SSL and headers

Next we have to ensure that the web server works with the certificates we just generated. We will also have to setup a virtual host for the signaling server itself. It’s easiest to create a separate directory with a configuration file that can be referred to later when setting up additional virtual hosts.

sudo mkdir -p /etc/nginx/snippets
sudo nano /etc/nginx/snippets/ssl.conf

The following parameters should be added to this file. The DNS resolver should be the local DNS server used to connect web server and domain. In most cases this will be IP of the router, e.g. Alternatively, you can also pick a common DNS server from this list.

# Configure SSL
# RSA certificates
ssl_certificate /etc/letsencrypt/;
ssl_certificate_key /etc/letsencrypt/;
# ECC certificates
ssl_certificate /etc/letsencrypt/;
ssl_certificate_key /etc/letsencrypt/;

# This should be ca.pem (certificate with the additional intermediate certificate)
# See here:
ssl_trusted_certificate /etc/letsencrypt/;

# Cipher suite from
# Max. security, but lower compatibility 

# Cipher suite from

# (Modern) cipher suite from

# Use multiple curves.
ssl_ecdh_curve secp521r1:secp384r1;

# SSL session handling
ssl_session_timeout 1d; 
ssl_session_cache shared:SSL:50m; 
ssl_session_tickets off;

# SSL stapling has to be done separately, because it will not work with self signed certs
# OCSP Stapling fetch OCSP records from URL in ssl_certificate and cache them
ssl_stapling on;
ssl_stapling_verify on;

# DNS resolver

The web server should also include a header configuration, which can be supplied in a separate configuration file.

sudo nano /etc/nginx/snippets/headers.conf

Add the following details to this file to ensure all subdomains are covered under https and comply with common security standards.

# Security related headers
# HSTS (ngx_http_headers_module is required)
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload;" always; 
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Robots-Tag none always;
add_header X-Download-Options noopen always;
add_header X-Permitted-Cross-Domain-Policies none always;
add_header Referrer-Policy no-referrer always;
add_header X-Frame-Options "SAMEORIGIN" always;

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

Set up a virtual host for the signaling server

Now we need to create the virtual host or server block that will manage the hosting of our subdomain on the server and ensure to include all configurations we went over above. It’s easiest to name the conf file after the domain we are going to use.

sudo nano /etc/nginx/conf.d/

Insert the following code into this file. This time we will listen on port 443, which is the secure SSL port. Please note that we will further modify this file after we have setup the signaling server.

server {
	listen 443 ssl http2;
	listen [::]:443 ssl http2;

        # Include SSL configuration
	include /etc/nginx/snippets/ssl.conf;

	# Include headers
    include /etc/nginx/snippets/headers.conf;

        # further configuration details to follow

After all this is completed, restart nginx to apply these settings.

sudo service nginx restart

Install the signaling server

The signaling server or high performance backend for Nextcloud Talk is composed of three components: the actual open source signaling server, a Janus server to handle WebRTC and a NATS server. We will handle these installations one by one.

First we will need to install docker on the Raspberry Pi, which is required for the installation of the NATS server.

sudo apt install

You can verify whether the installation was successful by typing sudo docker info or sudo docker run hello-world.

Generate keys

The next step is to generate the keys we will require for the server installation. Generate each one and make a note of it in a secure document file so that it is not lost.

  • Janus API-Key:
openssl rand -base64 16
  • Hash-Key:
openssl rand -hex 16
  • Block-Key:
openssl rand -hex 16
  • Nextcloud Secret Key:
openssl rand -hex 16

You will also require the security key for the coturn server you have previously set up on RPI 1. It was saved as the variable static-auth-secret in the file /etc/turnserver.conf.

Install libsrtp2-1

The Janus server requires the most up to date version of the libsrtp package, ensure to install it first.

sudo apt-get install -y libsrtp2-1

Install the signaling server

Get the software from github and build it like this:

sudo apt install golang-go
cd /etc/
sudo git clone
cd /etc/nextcloud-spreed-signaling
sudo make build

Next adjust the configuration file like this:

cd /etc/nextcloud-spreed-signaling
sudo cp server.conf
sudo nano server.conf

Make the following changes (most of these just need to be activated by removing the #). Insert the keys you previously generated in the indicated sections.

Section [http]

listen =

Section [sessions]

hashkey = <Hash-Key>
blockkey = <Block-Key>

Section [backend] – any name can be added.

backends = backend1

Now define the backend further below in the section on backend configuration. The url should point to your Nextcloud instance.

# URL of the Nextcloud instance
url =

# Shared secret for requests from and to the backend servers. This must be the
# same value as configured in the Nextcloud admin ui.
secret = <Nextcloud Secret Key>

Section [mcu]

type = janus
url = ws://

Section [turn]. The server address should point to your coturn server address and port.

apikey = <Janus API-Key>
secret = <Turn-Server-Key>
servers =,

Install the Janus server

Install the Janus software using the following command.

sudo apt install janus

The configuration file can be adjusted like this:

sudo nano /etc/janus/janus.jcfg

First add the STUN server – if you do not have one, you can use the public Nextcloud STUN server with the details below. The Janus API Key will also have to be inserted into this file.

# ...
stun_server = ""
stun_port = 443
# ...
full_trickle = true
# ...
turn_rest_api_key = "<Janus API-Key>"
# ...

We need to edit two more Janus configuration files.

sudo nano /etc/janus/janus.transport.http.jcfg

Set the interface to “lo” for local instead of eth0. The Janus server will run locally only.

# ...
interface = "lo" 
# ..

The same change should be made in the following file for the websocket:

sudo nano /etc/janus/janus.transport.websockets.jcfg
# ...
ws_interface = "lo"
# ...

Install the NATS server

The NATS server can be installed using docker. No need to change anything else, these configurations will ensure it is started automatically on boot.

sudo docker pull nats:latest
sudo docker run --name=NATSSERVER -d -p 4222:4222 -ti --restart=always nats:latest

Update nginx

The virtual host file we created before when setting up the Raspberry Pi will now be updated with new configuration to ensure that the janus server is integrated and the signaling server can be accessed from the Nextcloud installation.

sudo nano /etc/nginx/conf.d/

We add the upstream signaling server and two location blocks like this. The SSL and Header configurations refer to those previously generated above.

upstream signaling {

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;

        # Include SSL configuration
	include /etc/nginx/snippets/ssl.conf;

	# Include headers
        include /etc/nginx/snippets/headers.conf;

        # Include location
        location /standalone-signaling/ {
                proxy_pass http://signaling/;
                proxy_http_version 1.1;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        location /standalone-signaling/spreed {
                proxy_pass http://signaling/spreed;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "Upgrade";
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

After making these changes restart the service.

sudo service nginx restart

Start the signaling server

We are now ready to start the signaling server for the first time. For this we need to restart the janus server that it builds on.

sudo service janus restart

Change into the spreed directory and start the server using the previously entered configuration.

cd /etc/nextcloud-spreed-signaling 
./bin/signaling --config server.conf

This should generate a long list of output but should not contain any errors. If you find errors check the settings and if all servers (nginx, janus, nats) are active, then go to the troubleshooting section. Type CTRL + C to stop the command.

Add the signaling server as a service

We need to add a service so that the signaling server does not always have to be started using the terminal. First we add another user to the signaling group.

sudo groupadd signaling
sudo useradd --system --gid signaling --shell /usr/sbin/nologin --comment "Standalone signaling server for Nextcloud Talk." signaling

Copy the configuration file to a different location to keep it safe and add permissions.

sudo mkdir -p /etc/signaling
sudo cp server.conf /etc/signaling/server.conf
sudo chmod 600 /etc/signaling/server.conf
sudo chown signaling: /etc/signaling/server.conf

Open the file for the system service.

sudo nano dist/init/systemd/signaling.service

Update the file configuration. Some of these lines have been filled in the first step.

Description=Nextcloud Talk signaling server

ExecStart=/etc/nextcloud-spreed-signaling/bin/signaling --config /etc/signaling/server.conf


Register the service and activate it.

sudo cp dist/init/systemd/signaling.service /etc/systemd/system/signaling.service
systemctl enable signaling.service
systemctl start signaling.service

The signaling server should now be active and can be included into the Nextcloud instance. If everything is working as it should, you will get a welcome message after typing the following into the console:

sudo curl -i 

Add the signaling server to Nextcloud

Open your Nextcloud instance and go to the admin settings page for Nextcloud Talk. Add the new signaling server by clicking the + next to the heading. Now you can enter the domain details and password. Don’t forget to activate ‘Validate SSL certificate’ if you are using a HTTPS connection.


Shared secret: <Nextcloud Secret Key>


StrukturAG offer a benchmark client to test your signaling server by automatically connecting it to 100 clients. Further details can be found here.


  • The acme-challenge testrun is not working and I do not get any output using the Browser.

In my experience this typically happens when the correct ports have not been opened. Log into your router and make sure that you have port forwarding enabled for ports 80 and 443. If this does not help there might be an issue with the IP you have specified (is it correct?) or your domain.

  • I have installed the signaling server but I do not see the welcome message after typing the curl command above.

You might need to specify the domain as your localhost. Open /etc/hosts and add and your domain name under your localhost.

  • I have installed the signaling server, the curl command and client benchmarking work, but I cannot access the signaling server from my Nextcloud instance.

Check if you have opened all the ports on your router (443 is key in this case) – often a strict or misconfigured firewall can be the reason for connection issues on the frontend.

  • I’m trying to generate a LetsEncrypt certificate but get an error that the rate limit was exceeded.

You have tried too many times to register a certificate. LetsEncrypt only permits 50 certificates per registered domain per week. You will have to wait another week to complete the registration. You can check here how many certificate requests were initiated for your domain.

  • Everything seems to work and Nextcloud can connect to the signaling server, but when I start a call, I get an error. The log says: “Client error: `POST` resulted in a `403 Forbidden` response: Authentication check failed.”

I ran into this issue after completing the installation and the benchmarking. During the benchmark test I had to set allowall=true in server.conf, which caused the error. Setting it back to false resolved this issue.

  • The janus server log shows a lot of warnings about disabled plugins – does this affect the performance of the signaling server?

Important plugins required for the signaling server are the VideoRoom and websocket transport for janus. These must be enabled and working. I had other transport plugins disabled by default and it does not interfere with my signaling server as far as I can tell.

1 thought on “Add a signaling server to Nextcloud Talk”

Leave a Reply

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