Skip to main content

Running in Docker

docker run --network host \
-v /srv/lyno:/etc/lyno:ro \
-v /srv/lyno-data:/var/lib/lyno \
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
- lyno-data:/var/lib/lyno
command:
- -config
- /etc/lyno/configs/lyno.yaml
- -headless

volumes:
lyno-data:

The second volume is only needed once call logs, recordings or voicemail are switched on — but adding it up front costs nothing and saves a restart.

Three 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.externalHost and media.externalIp to the address the outside world sees;
  2. narrow media.rtpPortMin / rtpPortMax to a range small enough to publish, and publish exactly that range plus the SIP port.
bridge networking fallback
configVersion: lyno/v1
kind: System
spec:
sip:
bindHost: 0.0.0.0
bindPort: 5060
externalHost: 203.0.113.10

media:
externalIp: 203.0.113.10
rtpPortMin: 16384
rtpPortMax: 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"

3. Give dataDir its own writable volume

The configuration mount is read-only and should stay that way. Call logs, recordings and voicemail need a volume of their own, owned by the uid the container runs as:

sudo mkdir -p /srv/lyno-data && sudo chown 65532:65532 /srv/lyno-data
configVersion: lyno/v1
kind: System
spec:
dataDir: /var/lib/lyno

Use an absolute path. dataDir is written back exactly as typed, so a relative one that works on your laptop follows the configuration into the container and resolves somewhere read-only.

Recordings are the one thing here that grows without bound — and nothing prunes them — so keeping them off the configuration mount is a size decision as much as a privacy one.

-validate prints whether the directory is writable by the uid it is running as, which is the quickest way to check a mount before starting:

docker run --rm -v /srv/lyno:/etc/lyno:ro -v /srv/lyno-data:/var/lib/lyno \
ghcr.io/telqo/lyno:main -config /etc/lyno/configs/lyno.yaml -validate
data: /var/lib/lyno (NOT writable by uid 65532)

See The data directory.

The management API and host networking

With network_mode: host the API's default loopback bind is genuinely reachable from the host, which is what makes it useful without exposing it. Under bridge networking a loopback bind is reachable only from inside the container; binding it wider requires api.tls, which validation enforces. See Management API.

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.

Shutdown releases queued callers, then gives active calls ten seconds to drain before dropping them, and finalises recordings afterwards. There is no graceful drain that waits for calls to end on their own.