nextcloud-spreed-signaling puts a WebRTC gateway behind Nextcloud Talk
Standalone signaling server for Nextcloud Talk.
At a glance
- What is it?
- The standalone signaling server for Nextcloud Talk is a Go service that brokers calls, hides the media gateway from clients and fans messages between instances over NATS. It sits alongside Janus, which does the actual WebRTC, and the whole thing is AGPL-3.0 licensed with v2.1.1 published in March 2026.
- Who is it for?
- Adopt the standalone signaling server if you run Nextcloud Talk at a scale where the built-in backend is the bottleneck, since it moves call setup and message distribution into a service you can run separately. Skip it for a single small instance, where the default Talk backend is simpler.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the signaling server actually does
The repository contains the standalone signaling server which can be used for Nextcloud Talk, and the README points at the documentation for the API of the signaling server. The architecture is best understood from the Janus section, which states the split plainly. A Janus server can be used to act as a WebRTC gateway, and the signaling server uses the VideoRoom plugin of Janus to manage sessions. All gateway details are hidden from the clients, and all messages are sent through the signaling server. Only WebRTC media is exchanged directly between the gateway and the clients. So the signaling server is the control plane: it tells the media gateway what to do, relays signalling messages between participants and the gateway, and keeps the clients unaware of where the gateway lives. The repository topics line up with that role, covering call, clustering, etcd, go, grpc, nextcloud, screensharing, spreed, talk, video and webrtc.
Building it: Go, make and protobuf
Building needs git, go >= 1.26 and make, and the README notes that usually the last two versions of Go are supported, following the Go release policy. Additional tools are required to generate code from .proto files, specifically protobuf-compiler >= 3, and all other dependencies are fetched automatically while building. The build command is short.
$ make buildOn FreeBSD the equivalent is gmake build. Afterwards the binary is created as bin/signaling. The module is versioned as github.com/strukturag/nextcloud-spreed-signaling/v2, with go.mod declaring go 1.26.0, and the dependency list gives a good picture of the internals. NATS is there in both server and client form, Janus is reached through notedit/janus-go, gRPC and protobuf carry the API, pion/ice and pion/sdp handle the WebRTC negotiation pieces, gorilla/websocket and gorilla/mux handle the HTTP and WebSocket layers, easyjson handles the fast JSON encoding, and there is an etcd client and server dependency, a Prometheus client, a JWT library and a MaxMind geoip database reader.
Configuration, running it and the systemd unit
A default configuration file is included as server.conf.in, and the README says to copy this to server.conf and adjust as necessary for the local setup, with comments in the file describing the different parameters that can be changed. The server connects to a NATS server to distribute messages between different instances, and it starts once that NATS server is running and its URL is configured.
$ ./bin/signalingBy default the configuration is loaded from server.conf in the current directory, and a different path can be passed through the --config option.
$ ./bin/signaling --config /etc/signaling/server.confFor daemon operation under systemd, the README asks you to create a dedicated group and user first.
sudo groupadd --system signaling
sudo useradd --system \
--gid signaling \
--shell /usr/sbin/nologin \
--comment "Standalone signaling server for Nextcloud Talk." \
signalingThen you copy server.conf.in to /etc/signaling/server.conf and fix the permissions, copy dist/init/systemd/signaling.service to /etc/systemd/system/signaling.service with the absolute path in ExecStart adjusted to your binary location, and enable and start the service with systemctl.
Docker, compose and the three URLs you must set
Official Docker images for the signaling server and the proxy are available on Docker Hub under strukturag/nextcloud-spreed-signaling, and the README in the docker subfolder covers how to use and configure them. Building the images locally takes two commands, one for the server and one for the proxy.
docker build -f docker/server/Dockerfile -t nextcloud-spreed-signaling .
docker build -f docker/proxy/Dockerfile -t nextcloud-spreed-signaling-proxy .The compose file comes with settings that only make sense on a single machine, and the README is explicit about them. If you use the docker-compose.yml configuration as is, the MCU Url must be set to ws://localhost:8188, the NATS Url must be set to nats://localhost:4222, and TURN Servers must be set to turn:localhost:3478?transport=udp,turn:localhost:3478?transport=tcp. You will likely have to adjust the Janus command line options depending on the exact network configuration on your server. One practical note: docker-compose v2 is required for building, while most distributions ship older versions.
Janus configuration, and the memory warning that comes with it
The Janus setup has specific requirements. At least the VideoRoom plugin, the websocket transport and the websocket events handler of Janus must be enabled, and broadcasting of events must be enabled. To enable sending of events from Janus, the option broadcast must be set to true in the block events of janus.jcfg. In the configuration of the websocket events handler, janus.eventhandler.wsevh.jcfg, the module must be enabled by setting enabled to true, the backend must be set to the websocket url of the signaling server at ws://127.0.0.1:port/spreed or of the proxy at ws://127.0.0.1:port/proxy, and subprotocol must be set to janus-events. At least events of type handles, media and webrtc must be subscribed. Then you edit the server.conf and enter the URL to the websocket endpoint of Janus in the section [mcu] and key url, and during startup the signaling server connects to Janus and logs information about the gateway. Bandwidth limits are configured in the same section through maxstreambitrate and maxscreenbitrate.
The failure mode that will bite you: queued events in Janus
The README attaches a warning to the Janus configuration that is worth reading twice. If the configuration between Janus and the signaling endpoint is interrupted or cannot be established, unsent events will be queued by Janus and will use potentially lots of memory there. This can be limited by setting events_cap_on_reconnect in janus.eventhandler.wsevh.jcfg. By default, all events will be queued, as the connection between Janus and the signaling endpoint is assumed to be stable, most likely on the same machine. That assumption is the whole risk. If you run Janus and the signaling server on different hosts, or put a proxy in between, the default configuration turns a transient disconnection into unbounded memory growth inside the gateway. Setting events_cap_on_reconnect is a one-line mitigation that the README provides and that is easy to leave unset. The repository layout also shows a proxy component and a proxy.conf.in file, which is the mechanism for separating the two.
Scaling to multiple Janus servers and the etcd dependency
To scale the setup and add high availability, a signaling server can connect to one or multiple proxy servers that each provide access to a single Janus server. The way to do that is to set the type key in section [mcu] to proxy and set url to a space-separated list of URLs where a proxy server is running. Each signaling server that connects to a proxy needs a unique token id and a public and private RSA keypair, with the token id configured as token_id in section [mcu] and the path to the private key given there as well. The etcd dependency in go.mod, along with the etcd directory at the repository root, points at how state is shared between instances, and the clustering topic on the repository confirms that dimension. The NATS server distributes messages between different instances, so a deployment has three tiers to reason about: the signaling servers, the NATS message bus, and the Janus gateways behind proxies.
Where the standalone signaling server is the wrong choice
Three limits. The first is that it is one component of a stack, not a product. To run it you need NATS, Janus with its event handler configured, and, for a real deployment, TURN servers, and each of those has its own configuration surface. The README is honest that you will likely have to adjust the Janus command line options depending on your network configuration, which means the operational burden is in the surrounding system rather than in this repository. The second is the AGPL-3.0 licence. That is a strong copyleft, and while running the server as a separate service avoids the question entirely, anyone who modifies it and distributes a modified version has obligations that a permissive licence would not impose. The third is the upgrade coupling to Talk itself. The server is versioned separately from the Nextcloud Talk app, so a Talk upgrade may expect a signalling API the standalone server has not implemented yet, and the API documentation is a separate site rather than a document in the repository. On maintenance, the releases are v2.1.1 from 2026-03-12, v2.1.0 from 2026-02-03 and v2.0.4 from 2025-08-18, with the last push on 2026-10-16.
Editorial conclusion
Adopt the standalone signaling server if you run Nextcloud Talk at a scale where the built-in backend is the bottleneck, since it moves call setup and message distribution into a service you can run separately. Skip it for a single small instance, where the default Talk backend is simpler. Before you commit, read the Janus configuration requirements carefully, because a broken connection between Janus and the signaling endpoint queues unsent events in Janus memory, and set events_cap_on_reconnect before that becomes a problem.
Frequently asked questions
What does the nextcloud-spreed-signaling server do?
It is the standalone signaling server for Nextcloud Talk. It uses the VideoRoom plugin of a Janus server to manage sessions, hides all gateway details from the clients, sends all messages through itself, and only WebRTC media is exchanged directly between the gateway and the clients.
What do I need to build the Nextcloud Talk signaling server?
You need git, go >= 1.26 and make, plus protobuf-compiler >= 3 to generate code from .proto files. All other dependencies are fetched automatically while building. The build command is `make build` (`gmake build` on FreeBSD), and the binary is created as bin/signaling.
What does the signaling server need from NATS and Janus?
It connects to a NATS server to distribute messages between instances. On the Janus side you must enable the VideoRoom plugin, the websocket transport and the websocket events handler, set broadcast to true in the events block of janus.jcfg, and point the event handler backend at the signaling server's websocket url with subprotocol janus-events, subscribing at least to handles, media and webrtc events.
What happens if the Janus connection to the signaling server drops?
Unsent events will be queued by Janus and will use potentially lots of memory there, because by default all events are queued as the connection is assumed to be stable. You can limit this by setting events_cap_on_reconnect in janus.eventhandler.wsevh.jcfg.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/strukturag-nextcloud-spreed-signaling)Was this useful?