Worker Configuration
Complete reference for all environment variables, SIP/media network settings, logging, token management, and performance tuning for CallMeter workers.
This page documents every configuration option available for user-owned CallMeter workers. All configuration is done through environment variables passed to the Docker container.
Required Environment Variables
These two must be set for the worker to start. Everything else, including the gateway address, is preconfigured in the image.
| Variable | Description | Example |
|---|---|---|
WORKER_TOKEN | Authentication token generated when creating the worker in CallMeter. Format: cmw_ prefix + 64 hex characters (68 characters total). | cmw_a1b2c3d4... |
SDP_IP | The address the worker advertises in the SDP c= line — where remote SIP endpoints will send media. Must be an address they can actually reach. | 203.0.113.10 |
WORKER_TOKEN Is Sensitive
The worker token is a secret credential. Never hardcode it in Dockerfiles, commit it to version control, or expose it in logs. Use Docker secrets, environment files with restricted permissions, or a secrets management system.
SDP_IP Has No Safe Default
The worker refuses to start without SDP_IP, deliberately. It is the only address the far end can send media to, and it cannot be inferred: a worker behind NAT would detect its own private address, register successfully, and then never receive any audio. Guessing here produces a healthy-looking test with no media, which is the hardest failure to diagnose from the outside.
Set it to whatever address your SIP infrastructure can reach the worker on — the host's own address on a flat network, or your public/NAT address when the worker sits behind NAT.
# Minimal configuration
environment:
WORKER_TOKEN: "${WORKER_TOKEN}"
SDP_IP: "203.0.113.10"Optional Environment Variables
Bind Addresses
SIP_IP and MEDIA_IP control which local interface the worker binds to. They are bind addresses only — unlike SDP_IP, they are never advertised to the far end. The default binds every interface, which is correct on a single-homed host.
| Variable | Description | Default | Example |
|---|---|---|---|
SIP_IP | Address the worker binds SIP signaling to. | 0.0.0.0 (all interfaces) | 10.0.1.50 |
MEDIA_IP | Address the worker binds RTP media to. Set this to put media on a different interface than signaling. | 0.0.0.0 (all interfaces) | 10.0.1.51 |
When to Set These
Leave both unset unless:
- The host has multiple interfaces and you need media or signaling pinned to a specific one. On a multi-homed host you should set
MEDIA_IPexplicitly — with the default, the kernel picks a source address per destination, and that address can differ from theSDP_IPyou advertise. - You are running several workers on one host and want each bound to a distinct address.
Behind NAT. The worker binds its private address and advertises the public one:
environment:
WORKER_TOKEN: "${WORKER_TOKEN}"
SIP_IP: "10.0.1.50" # bind: the worker's own address
MEDIA_IP: "10.0.1.50" # bind: where RTP sockets listen
SDP_IP: "203.0.113.10" # advertise: where your SIP server sends mediaOn a flat network where your SIP infrastructure reaches the worker directly, SDP_IP is simply the worker host's address and the two bind variables can be left unset.
Media Port Range
| Variable | Description | Default |
|---|---|---|
LOCAL_RTP_UDP_PORT_RANGE | UDP port range the worker draws media ports from, as start-end. | 37768-65535 |
Most deployments never need to set this. There are two reasons to:
1. You want a narrower range to open on your firewall. The default is wide because it does not need to be conservative; if your security policy requires a tighter hole, set an explicit range and open exactly that.
2. You run more than one worker on the same host. Each worker tracks its own port usage and cannot see its siblings, so two workers sharing a range will eventually try to bind the same port. Give each one its own:
# worker 1
LOCAL_RTP_UDP_PORT_RANGE: "10000-19999"
# worker 2
LOCAL_RTP_UDP_PORT_RANGE: "20000-29999"
# worker 3
LOCAL_RTP_UDP_PORT_RANGE: "30000-39999"Sizing: each active endpoint uses roughly 2 ports for an audio-only call (RTP + RTCP) and roughly 4 with video. A 100-endpoint audio worker therefore needs at least 200 ports. A 10,000-port range comfortably covers any single worker.
Whatever Range You Set, Open It
The range you configure and the range your firewall permits must match. If they diverge, SIP registration still succeeds and calls still set up — only the audio never arrives, because the media ports the worker chose are the ones being dropped.
Logging
The worker emits structured JSON logs — one object per line — suitable for direct consumption by log aggregation systems (ELK, Splunk, Datadog).
All logs go to stdout and stderr, following Docker logging conventions. Use Docker's logging drivers to route them:
# Docker Compose with logging driver
services:
callmeter-worker:
image: <callmeter-worker-image>
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"Worker Liveness
There is nothing to configure and no port to open. The worker reports liveness over the same outbound connection it already makes to the Worker Gateway: it sends a heartbeat every 30 seconds, and if the gateway receives nothing for 60 seconds it marks the worker stale and stops assigning it work. The heartbeat interval is set by the platform during the connection handshake, not by an environment variable.
To check a worker, open Workers in your project --- the status badge and the Last heartbeat timestamp reflect the live connection state. On the host, use docker ps and docker logs as you would for any container; the image intentionally defines no Docker HEALTHCHECK, and restart: unless-stopped handles restart-on-failure.
Token Management
Token Format
Worker tokens follow a fixed format:
- Prefix:
cmw_(4 characters) - Random hex string: 64 characters
- Total length: 68 characters
Example: cmw_a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890
Token Security
The token authenticates the worker to the CallMeter platform. It is hashed server-side and never stored in plaintext. Security best practices:
- Store in a secrets manager --- HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
- Use environment files with restricted permissions ---
chmod 600 .env - Never commit to version control --- Add
.envto.gitignore - Rotate periodically --- Regenerate tokens as part of your security rotation schedule
- One token per worker --- Never share tokens between workers; a token can only authenticate one simultaneous connection
Regenerating a Token
If a token is compromised or lost:
- Open your project in CallMeter
- Navigate to Workers
- Click on the worker
- Click Regenerate Token
- Copy the new token immediately (it will not be shown again)
- Update the
WORKER_TOKENenvironment variable on the Docker host - Restart the worker container
Regeneration Invalidates the Previous Token
When you regenerate a token, the previous token is immediately invalidated. The worker using the old token will disconnect and cannot reconnect until updated with the new token.
Token-Related Errors
| Error | Cause | Resolution |
|---|---|---|
authentication failed: invalid token | Token is wrong, expired, or regenerated | Verify the token matches what is shown in the UI, or regenerate |
authentication failed: worker disabled | The worker has been disabled by an admin | Re-enable the worker in the UI |
authentication failed: duplicate connection | Another worker or container is already connected with this token | Stop the other instance, or wait for its connection to time out |
Performance Tuning
Resource Allocation
The worker's capacity should match the resources allocated to the Docker container. General guidelines:
| Concurrent Endpoints | CPU Cores | Memory | Network |
|---|---|---|---|
| 10-25 | 1 core | 512 MB | 10 Mbps |
| 25-50 | 2 cores | 1 GB | 25 Mbps |
| 50-100 | 2-4 cores | 2 GB | 50 Mbps |
| 100-200 | 4 cores | 4 GB | 100 Mbps |
| 200-500 | 8 cores | 8 GB | 250 Mbps |
These are approximate values for audio-only calls. Video calls require approximately 3-5x the CPU and 5-10x the bandwidth per endpoint.
Do Not Set a CPU Limit
Size the host, not the container. Give the worker a machine matching the table above and let it use it. Do not add a deploy.resources.limits CPU cap.
A CPU Cap Silently Degrades the Measurement
CallMeter already decides how much work a worker accepts, in one place: the capacity assigned to it, weighted per media type, enforced at admission. A cgroup CPU cap is a second, independent ceiling that does not cooperate with that — it contradicts it.
When the two disagree, the kernel does not error and the container does not restart. It throttles. RTP pacing slips and reported MOS drops — which on a call-quality measurement product is the worst available failure, because it is indistinguishable from the finding you are paying us to report. You would be debugging your own carrier for a problem your compose file created.
This is why the worker image ships with no resource limits at all, deliberately. If a worker is running hot, lower its capacity in CallMeter or give it a bigger host.
The same reasoning applies to a memory cap: an OOM-kill mid-test destroys the run and any recordings not yet uploaded. Size the host and let the scheduler ration the work.
Network Mode
Run the worker with network_mode: host. This is the recommended configuration at every size, not only at high endpoint counts, and it is how CallMeter runs its own workers.
services:
callmeter-worker:
image: <callmeter-worker-image>
network_mode: host
environment:
WORKER_TOKEN: "${WORKER_TOKEN}"
SDP_IP: "203.0.113.10"Two reasons:
- Latency. Docker's default bridge network routes every packet through
docker-proxyand a NAT hop. On a platform whose entire output is latency, jitter and MOS measurements, that overhead lands directly in the numbers you are trying to trust. - Addressing. With host networking the container sees the host's real interfaces, so
SIP_IP,MEDIA_IPandSDP_IPmean exactly what they say. Under bridge networking the worker binds a private container address that bears no relation to the address your SIP infrastructure must reach.
Host Network Mode Trade-offs
Host networking removes port isolation between container and host: the worker's ports are used directly on the host. Make sure they do not collide with other services — and if you run several workers on one host, give each its own LOCAL_RTP_UDP_PORT_RANGE.
Complete Configuration Example
A production-ready docker-compose.yml with all common options:
version: "3.8"
services:
callmeter-worker:
image: <callmeter-worker-image>
container_name: callmeter-worker
restart: unless-stopped
network_mode: host
environment:
# Required
WORKER_TOKEN: "${WORKER_TOKEN}"
SDP_IP: "203.0.113.10" # where remote endpoints send media
# Bind addresses — only if the host is multi-homed
# SIP_IP: "10.0.1.50"
# MEDIA_IP: "10.0.1.50"
# Only to narrow your firewall, or when running several workers on one host
# LOCAL_RTP_UDP_PORT_RANGE: "10000-19999"
# No deploy.resources CPU cap, deliberately — see "Do Not Set a CPU Limit" above.
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "5"Next Steps
- Networking --- Deep dive into firewall rules, NAT, and bandwidth requirements
- Capacity and Scaling --- Size workers correctly and scale horizontally
- Troubleshooting --- Resolve configuration and connection issues
Recording storage
Recordings are staged in the container at /data/recordings before upload. This path is not
configurable — what you control is which host directory you mount there:
volumes:
- ./cm_data:/dataThe mounted directory contains recordings and nothing else, one sub-directory per test
sub-population. The worker's own machinery — its gateway socket, downloaded source media, GStreamer
scratch — stays inside the container at /app/sip, where it is disposable on restart and out of your
way.
The container runs as uid 65532, so the host directory must be owned by it:
mkdir -p ./cm_data && sudo chown 65532:65532 ./cm_dataMount a different host path — a larger disk, a separate volume — by changing the left-hand side. To relocate recordings, move that directory; nothing inside the container changes.
Without the mount, un-uploaded recordings are lost on upgrade
With no volumes: entry, /data/recordings lives on the container's writable layer. It grows unbounded
against your Docker storage, and anything not yet uploaded is destroyed by docker compose down —
which is the first step of the upgrade procedure.
How much disk
| Media | Per stream-hour |
|---|---|
| Audio | ~58 MB |
| Video (720p24) | ~675 MB |
Files are staged only until they upload, so steady-state usage is roughly one batch of concurrent recordings rather than a running total. Provision for the largest test you intend to run — 10 concurrent video streams for an hour is ~7 GB of transient staging — plus headroom for periods when the platform is unreachable and recordings queue up.
The worker reclaims space on its own: it deletes each file once the platform confirms the upload, discards anything the platform permanently refuses (a probe that succeeded, an organisation at its recording-storage ceiling, a file over the size cap), and sweeps abandoned files older than 24 hours.
Deploying Your Own Workers
Step-by-step guide to deploying a CallMeter worker as a Docker container in your infrastructure, including configuration, verification, and multi-worker setups.
Worker Networking
Comprehensive networking guide for CallMeter workers including firewall rules, port requirements, NAT traversal, proxy configuration, bandwidth planning, and connectivity troubleshooting.