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
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:
- set
sip.externalHostandmedia.externalIpto the address the outside world sees; - narrow
media.rtpPortMin/rtpPortMaxto a range small enough to publish, and publish exactly that range plus the SIP port.
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
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)" ...
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
| Tag | Meaning |
|---|---|
main | The latest commit on the default branch. |
vX.Y.Z | A 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
- Pull the new image.
- Run
-validateagainst your configuration with the new image — a schema change surfaces here rather than at restart. - 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.