Skip to content

ast/wjmasterisk

Repository files navigation

Asterisk notes

About

A self-hosted PBX for a small private network, packaged as a Docker image. Built on Asterisk 23.3.0 (compiled from the upstream tarball inside the Dockerfile; multistage build on Debian 13 Trixie, ~370 MB final image).

Intended use case is a handful of phones at home, mixing:

  • softphones on laptops/desktops (Linphone, MicroSIP, Zoiper, pjsua …),
  • analogue phones via SIP-to-FXS adapters (Grandstream HT812, Cisco SPA122, Cisco ATA191-MPP — see HARDWARE.org for per-device setup),
  • a mobile SIP client reaching the PBX over TLS+SRTP.

The [softphone], [ata] and [mobile] endpoint templates in etc-asterisk/pjsip.conf cover those three cases; adding a new phone is a ~6-line copy-paste. All configuration lives in etc-asterisk/ and is bind-mounted live — edit a file, reload (pjsip reload / dialplan reload / module reload from the Asterisk console), no image rebuild needed.

See IDEAS.org for a brainstorm of directions to take this further — feature codes, IVR, conferencing, home-automation hooks, hardware to add, ops & security.

Using

Build and run

Build and run via the justfile:

just            # list recipes
just build      # build the docker image
just run        # run with ./etc-asterisk bind-mounted into the container
just lint       # hadolint the Dockerfile
just config-test # boot Asterisk briefly and fail on ERROR lines
just push       # push to the registry
just all        # build + push

Alternatively use docker compose (adds persistent volumes for /var/lib/asterisk and /var/log/asterisk):

docker compose up --build

docker compose

Why compose rather than just run for the persistent deployment:

  • restart: unless-stopped brings Asterisk back after a reboot or a crash, without needing a systemd unit.
  • stop_signal: SIGINT — Asterisk only shuts down cleanly on SIGINT. Docker’s default SIGTERM cuts in-flight calls and half-writes voicemail recordings; compose handles this so docker compose down is safe.
  • Named volumes for /var/spool/asterisk (voicemail, MoH), /var/lib/asterisk (CDRs, astdb) and /var/log/asterisk survive container recreation — just run drops all of those on exit.
  • Healthcheck via core waitfullybooted, so docker ps shows healthy once the dialplan is loaded and the AMI/CLI socket is up.

The foreground PID 1 inside the container is asterisk -cvvv, whose stdin is owned by compose. Don’t docker attach: a stray Ctrl-C would SIGINT PID 1 and shut Asterisk down. Open a remote console over the CLI socket instead — it’s a separate connection, safe to disconnect:

docker compose exec asterisk asterisk -rvvv

Exit with exit or Ctrl-D. For one-shot commands (same mechanism the healthcheck uses):

docker compose exec asterisk asterisk -rx "pjsip show registrations"
docker compose exec asterisk asterisk -rx "pjsip set logger on"

TLS certificates

The SIP-TLS transport (port 5061, configured in etc-asterisk/pjsip.conf under [transport-tls]) expects a cert and key under etc-asterisk/keys/. Generate a fresh self-signed pair with:

just gen-keys                       # CN=asterisk.local, valid 10 years
just gen-keys pbx.example.lan 365   # custom CN and validity (days)

This writes two files into etc-asterisk/keys/ (both gitignored):

FileRoleWhere it lives
asterisk.keyPrivate keyServer only — never copy off the box
asterisk.pemPublic certDistribute to TLS clients that need to trust it

A self-signed cert is not in any client’s default trust store, so each SIP-TLS client must either:

  1. Import asterisk.pem as a trusted root, or
  2. Disable certificate verification (fine for a homelab; not for prod).

In pjsua, --use-tls turns on TLS; trust-store flags vary by build — see pjsua --help for --ca-list.

Tailscale setup

To reach the PBX over Tailscale from remote clients, install and authenticate Tailscale on the host machine with DNS interception disabled:

tailscale up --accept-dns=false

The --accept-dns=false flag is critical for SIP: Tailscale’s DNS rewriting can interfere with SIP’s use of DNS SRV records and host lookups, causing registration and routing failures. Disable it to let the system use its configured resolvers.

Important: The Asterisk container uses network_mode: host (in compose) or --network host (via just run) rather than Docker bridge networking. This is required for SIP — the protocol embeds the server’s IP address in SDP payloads, and container-to-host address translation breaks SIP signaling between remote clients and the PBX. With host networking, Asterisk binds directly on the VM’s interfaces, where Tailscale routes to it transparently.

Remote clients on the same Tailscale network can then register to the PBX at its Tailscale IP (e.g., sip:6001@100.x.x.x;transport=tls) with no additional firewall configuration.

Asterisk CLI commands

Run these inside the running Asterisk console (the -cvvv foreground process). To get a console against a container started detached or via compose:

docker exec -it asterisk asterisk -rvvv          # compose (container_name: asterisk)
docker exec -it $(docker ps -qf ancestor=ghcr.io/ast/wjmasterisk) asterisk -rvvv   # just run

You can also fire a single command without an interactive console using asterisk -rx "<command>".

Reload config (no restart, no rebuild)

etc-asterisk/ is bind-mounted live — edit a file, then reload the relevant subsystem:

dialplan reload        # after editing extensions.conf
pjsip reload           # after editing pjsip.conf (endpoints, transports)
module reload          # reload everything that supports it
voicemail reload       # after editing voicemail.conf
moh reload             # after editing musiconhold.conf
core reload            # broad reload — most .conf files at once

Inspect registrations and endpoints

pjsip show endpoints           # every endpoint + its current state
pjsip show endpoint 6010       # one endpoint in full detail
pjsip show aors                # AORs and their configured contacts
pjsip show contacts            # who is actually registered right now
pjsip show registrations       # outbound registrations (if any)
pjsip show transports          # the UDP/TCP/TLS listeners

Live calls

core show channels             # active channels (one row per call leg)
core show channels verbose     # same, with more columns
core show channel <name>       # deep detail on one channel
hangup request <name>          # drop a stuck call

Debugging SIP and audio

pjsip set logger on            # dump every SIP packet to the console
pjsip set logger off
rtp set debug on               # log RTP flow — use for one-way-audio bugs
rtp show settings              # the configured RTP port range
core set verbose 5             # more chatty console
core set debug 3               # internal debug logging

General

core show codecs               # codecs this build knows about
core show uptime               # how long Asterisk has been up
core show settings             # version, paths, channel counts
core waitfullybooted           # blocks until startup is complete (used by the healthcheck)

Hardware (FXS / ATA configuration)

Configuration notes for the analogue-phone adapters live in HARDWARE.org — common ATA settings plus per-device SIP field tables for the Cisco SPA122 and ATA191-MPP. The primary adapter, the Grandstream HT812, has its own full walkthrough in HT812.org (network discovery, web-UI fields, rotary-phone support, gotchas).

Softphones

Reference: https://docs.pjsip.org/en/latest/specific-guides/other/cli_cmd.html

pjsua (pjproject)

Building pjproject

sudo apt install build-essential libasound2-dev libssl-dev libncurses5-dev \
     libreadline-dev libavcodec-dev libavformat-dev libswscale-dev libopus-dev

git clone https://github.com/pjsip/pjproject.git
cd pjproject
./configure --enable-shared
make dep
make -j$(nproc)

The compiled apps land in pjproject/pjsip-apps/bin/.

Running pjsua

PBX points at the Asterisk host. Default 127.0.0.1 assumes pjsua runs on the same machine as the PBX; override for other hosts, e.g. PBX=192.168.1.16 or PBX=asterisk.local.

To actually call between two extensions you need **two pjsua processes running side by side**, one registered as each extension. They use different --local-port values so they don’t fight for the same UDP port. Open two terminals and run one block in each:

Terminal 1 — register as 6002:

PBX=${PBX:-127.0.0.1}
pjsua --id sip:6002@$PBX \
      --registrar sip:$PBX \
      --realm '*' \
      --username 6002 \
      --password 6002 \
      --local-port=5063
      # --use-tls   # add for SIP-TLS on 5061

Terminal 2 — register as 6001:

PBX=${PBX:-127.0.0.1}
pjsua --id sip:6001@$PBX \
      --registrar sip:$PBX \
      --realm '*' \
      --username 6001 \
      --password 6001 \
      --local-port=5062

Verify both are registered (in a third terminal, against the running Asterisk container) — both endpoints should show Available with a contact listed:

docker exec -it $(docker ps -qf ancestor=ghcr.io/ast/wjmasterisk) \
    asterisk -rx "pjsip show endpoints"

Useful pjsua commands

Once pjsua is running you get an interactive prompt. The most useful single-letter commands:

KeyWhat it does
mMake a call (prompts for the destination URI)
aAnswer the incoming call with 200 OK
gAnswer with a specific code (200 answer, 486 busy, 603 decline)
hHang up the current call (ha hangs up all)
HHold the call
vre-inVite — release hold / resume
] [Switch to next / previous call (when more than one is active)
xBlind transfer the current call
#Send DTMF (e.g. to enter a voicemail PIN, or dial *97, *43)
dqDump current call quality (jitter, loss, RTT) — best audio-debug tool
dDump status (accounts, registrations, active calls)
CpShow / change codec priorities
VAdjust audio volume
rrRe-register the account (useful when toggling network)
qQuit

The full menu is printed any time you press ? at the prompt.

Making calls

Press m, then type the destination URI at the Make call: prompt. This is pjsua’s own CLI — not a shell — so the $PBX variable used above is NOT expanded here. Substitute the actual address (or 127.0.0.1 when pjsua is on the same machine as Asterisk):

sip:6001@127.0.0.1;transport=udp     ; another softphone
sip:6010@127.0.0.1;transport=udp     ; an ATA line
sip:*43@127.0.0.1;transport=udp      ; echo test — repeat your voice
sip:*44@127.0.0.1;transport=udp      ; music on hold — one-client audio test
sip:*97@127.0.0.1;transport=udp      ; check your own voicemail

(If you set PBX=192.168.1.16 when starting pjsua, use that IP in the URIs above. The variable substitution happens only in the shell command that launches pjsua, not inside pjsua’s prompt.)

To answer an incoming call, press a. To reject with a specific status, press g and pick:

200 → answer
486 → busy
603 → decline

Things to try

  • *43 from any phone — the echo test will repeat your voice back. If you hear yourself, the audio path is healthy in both directions.
  • Park / unpark via x (transfer) to a parking lot extension (not configured by default — would need res_parking.so loaded and a parkext in features.conf).
  • dq during a call: watch the jitter and packet-loss counters when moving the mobile client between WiFi and cellular.
  • Change codec priority with Cp and observe negotiation in the Asterisk CLI with pjsip set logger on → look for the m= line in the SDP.

Linphone

Configuration

FieldValue
Username6002 (or any configured extension)
Display name6002 (optional)
SIP Domainasterisk (Tailscale hostname)
Passwordsame as username (default)
TransportUDP

The SIP domain should be the Tailscale hostname of the Asterisk VM. If the short hostname does not resolve, use the full Tailscale domain (asterisk.<tailnet>.ts.net).

Making calls

Before dialling, confirm the 6002 account is selected as the active account in Linphone (shown in the top bar or account switcher). If another account or the system identity is active, Asterisk will reject the call with “No matching endpoint found”.

Dial by typing the extension directly (Linphone fills in the domain from the active account):

DestinationWhat it does
*43Echo test — repeats your voice back
*44Music on hold — one-way audio test
*97Voicemail
6001Another softphone (must be registered)

Zoiper

Configuration

FieldValue
Username6003 (or any configured extension)
Display name6003 (optional)
Domainasterisk.local (or IP/Tailscale address)
Passwordsame as username (default)
TransportUDP (or TLS for 5061)

Launch Zoiper and select “Add account”. Enter the SIP credentials above. For remote access via Tailscale, use the Tailscale IP or hostname (e.g., 100.x.x.x) as the domain.

Making calls

Dial directly in the keypad or contact list. The same test extensions work:

DestinationWhat it does
*43Echo test — repeats your voice back
*44Music on hold — one-way audio test
*97Voicemail
6001Another softphone (must be registered)

MicroSIP (Windows only)

⚠️ Note: MicroSIP runs on Windows only.

Configuration

Download from microsip.org. Launch and go to SettingsAccounts:

FieldValue
Display name6004
User name6004 (or any configured extension)
Serverasterisk.local (or IP/Tailscale address)
Passwordsame as username (default)
TransportUDP (leave blank) or TLS for 5061

Apply, and MicroSIP will register automatically.

Making calls

Double-click a contact or type directly in the dial field. Same test extensions as other softphones:

DestinationWhat it does
*43Echo test — repeats your voice back
*44Music on hold — one-way audio test
*97Voicemail
6001Another softphone (must be registered)

About

My docker for Asterisk

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors