Command line
lyno [-config PATH] [-validate | -configure | -tts] [-headless] [-debug]
There are no subcommands and no positional arguments — everything is a flag.
| Flag | Default | Purpose |
|---|---|---|
-config | configs/lyno.yaml | Path to the main configuration file. |
-validate | off | Validate the configuration, print a summary and exit. |
-configure | off | Open the configuration editor and exit. |
-tts | off | Generate prompt audio and exit. |
-headless | off | Disable the dashboard and log to stderr. |
-debug | off | Enable debug logging. |
-tts brings its own flags — -text, -out, -manifest, -voice, -model,
-force and -voices — which do nothing without it. They are covered under
Prompt audio.
The five modes
# 1. Check the configuration without starting anything.
lyno -config configs/lyno.yaml -validate
# 2. Edit the configuration in a terminal editor.
lyno -config configs/lyno.yaml -configure
# 3. Generate the prompt set, before the PBX ever runs.
lyno -tts -manifest configs/prompts-en.yaml
# 4. Run with the live dashboard (interactive terminal).
lyno -config configs/lyno.yaml
# 5. Run with plain structured logs (systemd, Docker, CI).
lyno -config configs/lyno.yaml -headless
-validate, -configure and -tts all exit without starting the PBX.
-configure refuses to run when stdout is not an interactive terminal.
Precedence is -tts, then -configure, then -validate. -tts is evaluated
before the configuration is even loaded, so it works against a tree that does not
validate.
The dashboard is used only when stdout is a terminal, so a run under
systemd or in a container logs plainly even without -headless. Pass the flag
anyway — it makes the intent explicit and survives being run by hand.
Validating
A successful run prints a summary of what was loaded:
configuration OK
sip: udp/0.0.0.0:5060
codecs: [alaw ulaw]
timezone: Europe/Amsterdam
data: /var/lib/lyno (writable)
api: http://127.0.0.1:8080 (2 tokens)
trunks: 1
overrides: 0 inbound
outbound: 3 global routes (default trunk: "carrier")
tenants: 1
acme (acme.pbx.example.com)
accounts 2 · ring groups 1 · dids 1+0 pattern(s) · inbound 1
100 Alice 1 steps
200 Support follow-me 4 steps
The data: and api: lines only appear when those are configured. The data
line reports one of writable, does not exist yet; it will be created at startup, NOT a directory or NOT writable by uid N — informational rather
than fatal, so -validate still passes in CI and against a read-only container
mount that has no data volume.
-validate is not quite read-onlyReporting whether the data directory is writable means writing to it. The check
creates and deletes a .lyno-write-probe file inside dataDir. Nothing else
on disk is touched.
A failing run writes error: … to stderr and exits non-zero. Validation is a
whole-tree check that collects every problem rather than
stopping at the first, so one run usually tells you everything that is wrong:
trunk "carrier": registration.enabled requires auth
tenant acme extension 200 step 1: unknown ring_group "suport"
tenant acme extension 200 step 2: play requires 'file'
media: rtpPortMax must be greater than rtpPortMin
Wire it into CI or a pre-commit hook — the repository's make validate does
exactly this.
Running
Configuration is read once, at startup. Restart the process to apply a change. Startup returns once the listeners are ready.
There is exactly one exception. SIGHUP reloads the configuration and swaps the
management API's tokens — and nothing else. It exists because the
alternative, restarting to rotate a token, means nobody ever rotates one. A
configuration that no longer loads, or a token set that is refused, leaves the
running set alone and logs an error.
kill -HUP $(pidof lyno)
SIGHUP does nothing when the API is disabled.
Under systemd
[Unit]
Description=Lyno PBX
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStartPre=/usr/local/bin/lyno -config /etc/lyno/configs/lyno.yaml -validate
ExecStart=/usr/local/bin/lyno -config /etc/lyno/configs/lyno.yaml -headless
Restart=on-failure
RestartSec=5s
User=lyno
Group=lyno
# The configuration holds SIP passwords.
UMask=0077
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadOnlyPaths=/etc/lyno
[Install]
WantedBy=multi-user.target
ExecStartPre with -validate turns a bad edit into a failed start with a
readable reason, instead of a service that comes up in an unexpected state.
Logging
Without -headless, logs are a tab inside the dashboard. With -headless they
go to stderr as structured lines, which is what you want under systemd or
Docker.
-debug adds, among other things:
- one line per SIP message received — see the SIP trace;
- the outbound route that matched and the number after digit manipulation;
- the trunk source addresses discovered from DNS, and every change to them;
- each dialplan step as it runs, and each step skipped by a time condition;
- unhandled SIP responses (late or retransmitted provisionals on UDP) that would otherwise be noise at info level.
It is verbose enough that it is a debugging tool rather than a default.
The SIP trace
-debug switches on a trace of every SIP message the PBX receives. There is
no second flag to remember, and with -debug off the filter is not installed at
all, so it costs nothing per packet.
level=DEBUG msg="sip in" component=sip-trace transport=UDP source=198.51.100.7:5060
bytes=812 line="INVITE sip:+31201234567@pbx.example.com SIP/2.0"
from="\"+31612345678\" <sip:+31612345678@carrier.example>"
to="<sip:+31201234567@pbx.example.com>" call_id=... cseq="1 INVITE"
This is the only place that can answer "did the packet arrive at all", which is the first question when a carrier stops delivering calls. It sits below parsing, below the transaction layer and below every routing decision, so it reports messages that are malformed, addressed to an unknown DID, or dropped by the guard a moment later — none of which reach a handler that could log them.
Three things it deliberately does not do:
- It logs a summary, not the message. A raw SIP message is fifteen lines,
which the dashboard's log view renders as fifteen broken rows — and the
Authorizationheader inside one carries digest credentials that have no business in a log file somebody might paste into a ticket. The start line plusFrom,ToandCall-IDis what identifies a call. - It never changes or withholds a byte. Dropping is the guard's job, so this is safe to leave switched on.
- It skips keep-alives. A phone sends a bare CRLF every 30 seconds, and tracing those would bury the messages that matter.
On a stream transport the filter sees whatever the socket read returned rather
than a whole message, so a chunk with no blank line yet is reported with
partial=true instead of as a truncated message.
Exit behaviour
-validateexits non-zero when the configuration is invalid.- The dashboard quits on
qorctrl+c, stopping the PBX. - In headless mode,
SIGINTandSIGTERMshut down cleanly.
Shutdown, in order: the management listener closes first (a caller of an HTTP interface can retry, a caller on a trunk cannot), waiting callers are released from queues, then active calls are given ten seconds to drain, and only then are recordings finalised. Calls still up after ten seconds are dropped with a warning. There is no graceful drain that waits for a call to end on its own.