The shipped Dockerfile produces a small distroless image (gcr.io/distroless/static-debian12:nonroot) with the cpe-sim binary at /usr/local/bin/cpe-sim and the reference profiles bundled at /profiles/. No shell, no package manager, no extras.
Build¶
docker build -t cpe-sim:dev .
Build args (optional, baked into --version output):
docker build \
--build-arg VERSION=v0.5.0 \
--build-arg COMMIT=$(git rev-parse --short HEAD) \
-t cpe-sim:v0.5.0 .
The build uses BuildKit cache mounts for /go/pkg/mod and /root/.cache/go-build so repeat builds are fast.
Run, one-shot Inform¶
docker run --rm \
-v "$PWD/profiles/example-tr181-gateway/:/profile.yaml:ro" \
cpe-sim:dev \
--profile=/profile.yaml \
--acs-url=http://acs.local:7547/cwmp
Exits 0 on a clean session close, 1 on any error. Useful in CI as a smoke test.
Run, daemon mode with CR listener (TR-069)¶
docker run --rm \
-p 7547:7547 \
-v "$PWD/my-fleet.yaml:/profile.yaml:ro" \
cpe-sim:dev \
--profile=/profile.yaml \
--acs-url=http://acs.local:7547/cwmp \
--cr-bind-addr=0.0.0.0:7547 \
--cr-publish-path=Device.ManagementServer.ConnectionRequestURL \
--seed=1
-p 7547:7547 exposes the CR listener so the ACS can call back. With fleet.count > 1, all CPEs share that one port; the listener routes by URL path (/cr/cpe-1, /cr/cpe-2, …).
Run, TR-369 USP Agent mode¶
USP Agents connect outbound to a Message Transfer Protocol endpoint (MQTT broker, WebSocket server, or STOMP broker). No inbound port is exposed; the Agent dials out and the Controller talks back over the established session.
docker run --rm \
-v "$PWD/my-fleet.yaml:/profile.yaml:ro" \
cpe-sim:dev \
--profile=/profile.yaml \
--usp-mtp=mqtt \
--usp-mqtt-addr=broker.example.com:1883 \
--seed=1
See USP Agent for the full set of MTP flags.
Bundled reference profiles¶
Skip the volume mount when using one of the shipped profiles:
| Path inside image | Vendor shape |
|---|---|
/profiles/example-tr181-gateway/ |
Generic TR-181 gateway (Device.*) with WAN, LAN, WiFi, EasyMesh extenders, hosts |
/profiles/example-tr098-gateway/ |
Generic TR-098 gateway (InternetGatewayDevice.*) with WAN, LAN, WLAN, vendor-extension extenders, hosts |
docker run --rm cpe-sim:dev \
--profile=/profiles/example-tr098-gateway/ \
--acs-url=http://acs.local:7547/cwmp
Compose¶
The repo ships docker-compose.example.yml as a copy-paste starting point:
cp docker-compose.example.yml docker-compose.yml
ACS_URL=http://acs.local:7547/cwmp docker compose up --build
The example wires:
cpe-simfrom the localDockerfile.- ACS URL via the
ACS_URLenvironment variable (defaults tohttp://acs.local:7547/). - CR listener bound on
0.0.0.0:7547and exposed on the host. --seed=1for deterministic runs.
Edit the command: block to point at a custom profile, change log level, or add your own CLI flags.
Compose with a sibling ACS service¶
services:
acs:
image: your-acs:dev
ports:
- "7547:7547"
cpe-sim:
build: .
depends_on:
- acs
command:
- --profile=/profiles/example-tr181-gateway/
- --acs-url=http://acs:7547/cwmp
- --seed=1
- --log-level=info
# No host port mapping needed unless the ACS pushes CRs back.
# Add --cr-bind-addr / --cr-publish-path and expose 7547 if so.
The simulator dials the ACS by container name (acs); both containers share the compose network.
Image hygiene¶
- Distroless base: no
/bin/sh, no debugger.docker exec -it ... shwill fail; that's intentional. - Non-root user: runs as
nonroot:nonroot. If you bind-mount profiles, make them world-readable or chown them to UID 65532. - No CMD:
ENTRYPOINTiscpe-sim, so flags pass through directly. There is no default--profile; supply one or the simulator exits with a usage error.
Resource budgeting¶
Per-CPE memory is in the order of tens of KiB plus the parameter tree size. A 1000-CPE fleet on a typical TR-181 profile fits comfortably in 200 MiB. The shared HTTP transport pool reuses connections across CPEs so file-descriptor counts scale sub-linearly with fleet size.
If you're pushing past 5000 CPEs in one container, raise the file descriptor limit:
docker run --ulimit nofile=65536:65536 ...