Skip to main content

Running in Docker

docker run --network host \
-v /srv/lyno:/etc/lyno:ro \
ghcr.io/telqo/lyno:main \
-config /etc/lyno/configs/lyno.yaml -headless

Mount a directory holding configs/ and sounds/ — the same layout as the repository — so the relative paths inside the YAML keep resolving.

The image is built FROM scratch-sized on distroless: a static binary, about 19 MB, no shell and no package manager.

Compose

compose.yaml
services:
lyno:
image: ghcr.io/telqo/lyno:main
network_mode: host
restart: unless-stopped
volumes:
- ./:/etc/lyno:ro
command:
- -config
- /etc/lyno/configs/lyno.yaml
- -headless

Two things decide whether this works

1. Use host networking

SIP carries IP addresses inside its payload and RTP uses a wide UDP range. Docker's bridge networking rewrites addresses and cannot practically publish ten thousand UDP ports. The symptoms are the classic VoIP ones:

  • calls that ring but never connect;
  • one-way audio;
  • audio that works locally and fails from outside.

network_mode: host avoids all of it, and the firewall on the host does the filtering.

If you genuinely have no choice, you must:

  1. set sip.external_host and media.external_ip to the address the outside world sees;
  2. narrow media.rtp_port_min / rtp_port_max to a range small enough to publish, and publish exactly that range plus the SIP port.
configs/lyno.yaml — bridge networking fallback
sip:
bind_host: 0.0.0.0
bind_port: 5060
external_host: 203.0.113.10

media:
external_ip: 203.0.113.10
rtp_port_min: 16384
rtp_port_max: 16584 # 100 concurrent streams, publishable
compose.yaml — bridge networking fallback
services:
lyno:
image: ghcr.io/telqo/lyno:main
ports:
- "5060:5060/udp"
- "16384-16584:16384-16584/udp"
volumes:
- ./:/etc/lyno:ro

Each concurrent call leg needs a couple of ports, so size the range for your peak concurrency and then some. See Running behind NAT.

2. Mind the file ownership

The image runs as uid 65532, and configuration files are mode 0600 because they contain SIP passwords. A container that cannot read them fails at startup with a permission error.

Either chown the tree:

sudo chown -R 65532:65532 /srv/lyno/configs

or run the container as the user that already owns them:

docker run --user "$(id -u):$(id -g)" ...
compose.yaml
services:
lyno:
user: "1000:1000"

Tags

TagMeaning
mainThe latest commit on the default branch.
vX.Y.ZA released version.

Images are multi-arch and are only published from main and from tags; pull requests build without publishing.

Pin a version tag in production. main moves.

Validating inside the container

docker run --rm -v /srv/lyno:/etc/lyno:ro ghcr.io/telqo/lyno:main \
-config /etc/lyno/configs/lyno.yaml -validate

Worth doing before restarting a running instance: the same binary, the same paths, the same file permissions.

Logs

Always run with -headless in a container. The dashboard needs an interactive terminal, and while Lyno detects a non-terminal stdout and falls back on its own, being explicit keeps the intent visible in the compose file.

Upgrading

  1. Pull the new image.
  2. Run -validate against your configuration with the new image — a schema change surfaces here rather than at restart.
  3. Recreate the container.

Calls in progress are dropped on restart; there is no graceful drain.