CallMeter logoCallMeter Docs

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.

VariableDescriptionExample
WORKER_TOKENAuthentication token generated when creating the worker in CallMeter. Format: cmw_ prefix + 64 hex characters (68 characters total).cmw_a1b2c3d4...
SDP_IPThe 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.

VariableDescriptionDefaultExample
SIP_IPAddress the worker binds SIP signaling to.0.0.0.0 (all interfaces)10.0.1.50
MEDIA_IPAddress 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_IP explicitly — with the default, the kernel picks a source address per destination, and that address can differ from the SDP_IP you 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 media

On 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

VariableDescriptionDefault
LOCAL_RTP_UDP_PORT_RANGEUDP 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 .env to .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:

  1. Open your project in CallMeter
  2. Navigate to Workers
  3. Click on the worker
  4. Click Regenerate Token
  5. Copy the new token immediately (it will not be shown again)
  6. Update the WORKER_TOKEN environment variable on the Docker host
  7. 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.

ErrorCauseResolution
authentication failed: invalid tokenToken is wrong, expired, or regeneratedVerify the token matches what is shown in the UI, or regenerate
authentication failed: worker disabledThe worker has been disabled by an adminRe-enable the worker in the UI
authentication failed: duplicate connectionAnother worker or container is already connected with this tokenStop 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 EndpointsCPU CoresMemoryNetwork
10-251 core512 MB10 Mbps
25-502 cores1 GB25 Mbps
50-1002-4 cores2 GB50 Mbps
100-2004 cores4 GB100 Mbps
200-5008 cores8 GB250 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-proxy and 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_IP and SDP_IP mean 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

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:/data

The 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_data

Mount 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

MediaPer 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.

On this page