Use this repo as a starting point for building your own NCOS containers with Kiro. The intended workflow is fork → clone → chat → (optionally) contribute back.
- Go to cradlepoint/container-samples on GitHub.
- Click Fork in the top-right corner.
- Choose your account or org as the destination and create the fork.
Working from your own fork keeps your container code separate from this upstream repo and gives you a remote to push to.
Kiro can clone the repo for you, no terminal needed:
- Open the Command Palette (
Cmd+Shift+Pon macOS,Ctrl+Shift+Pon Windows/Linux) and run Git: Clone. - Paste your fork's URL, e.g.
https://github.com/<your-username>/container-samples.git. - Pick a local folder to clone into.
- When prompted, choose Open to open the cloned repo as your Kiro workspace.
Kiro picks up the steering files in .kiro/steering/ automatically once the workspace is open, so it already knows the conventions, constraints, and reference samples in this repo.
Describe what you want to build in the chat panel, for example:
Build a container that reads GPS position from the router and publishes it over MQTT
Kiro will:
- Read the docs in
docs/(development guide, SDK reference, memory constraints) before writing code - Check whether NCOS already provides the capability natively, and ask clarifying questions about your target router model, networking, and Config Store access needs
- Scaffold the container under
containers/<your_sample>/following the existing conventions (Dockerfile, entrypoint script, Compose YAML, README) - Build and verify the image locally for both
linux/arm64andlinux/arm/v7before calling the work done
You can iterate conversationally — ask Kiro to add features, simplify something, or point it at an existing sample (like containers/edge_ai/) as a pattern to follow.
If you've built something worth sharing, use Kiro's Source Control view to commit and push, no terminal needed:
- Open the Source Control view in the sidebar (or
Cmd+Shift+G/Ctrl+Shift+G). - Stage your new container's files (e.g.
containers/<your_sample>/) by hovering over them and clicking the +, or stage everything with the + next to Changes. - Enter a commit message (e.g. "Add
<your_sample>container") in the message box and click the checkmark to commit. - Click Publish Branch (for a new branch) or Sync Changes / Push to push to your fork.
Then open a PR from your fork's branch to cradlepoint/container-samples on GitHub. Include what the container does, which router models/architectures you tested, and any measured image sizes in the PR description.
Every sample under containers/*/ is automatically built and published to the GitHub Container Registry on each change to master. Pull the image directly instead of building it yourself:
ghcr.io/cradlepoint/container-samples/<sample_name>:latest # linux/arm64
ghcr.io/cradlepoint/container-samples/<sample_name>:latest-armv7 # linux/arm/v7
Use the tag matching your router's architecture (see each sample's README for supported models). No login is required to pull.
Ericsson routers run containers on a Linux ARM64 (aarch64) platform using musl libc. Containers must be built for this target architecture and C library to function correctly.
- Architecture: ARM64 / aarch64
- C Library: musl libc (not glibc)
- OS: Linux
When building container images, ensure your base image and all compiled binaries target linux/arm64 with musl libc. Alpine Linux is a common base image choice since it uses musl natively.
Containers use bridge networking by default. Each container gets its own isolated network namespace with a virtual bridge interface. To expose services externally, use port mappings or assign the container an IP on a LAN network.
With the default bridge, the container receives an IP in the Docker internal subnet (starting at 172.17.0.2). Services are reached via port mappings on the router's IP.
To give a container its own IP address on a Local IP Network (LAN), define a custom Compose network that binds to an existing Local IP Network via its UUID. The container then appears as a distinct host on that LAN — reachable directly by IP without port mapping.
- Create a Local IP Network in NetCloud Manager (e.g.,
192.168.150.0/24) - Find the network's UUID (see below)
- Reference it in the Compose YAML under
driver_opts - Optionally assign a static IP to the service
From NetCloud Manager (Configuration):
In NCM, pull the device's configuration (JSON). The lan dictionary is keyed by UUID — each key is a Local IP Network UUID:
{
"lan": {
"00000002-0d93-319d-8220-4a1fb0372b51": {
"ip_address": "192.168.150.1",
"netmask": "255.255.255.0",
...
},
"00000000-0d93-319d-8220-4a1fb0372b51": {
"ip_address": "192.168.250.1",
"netmask": "255.255.255.0",
...
}
}
}From the local router NCOS CLI:
Connect to the router CLI (SSH or console) and query the Config Store:
get config/lan
This returns the list of Local IP Networks. Each network object is keyed by its UUID:
config/lan/00000002-0d93-319d-8220-4a1fb0372b51
config/lan/00000000-0d93-319d-8220-4a1fb0372b51
To inspect a specific network's details:
get config/lan/00000002-0d93-319d-8220-4a1fb0372b51
The output includes ip_address, netmask, and other settings you'll need to match in the Compose YAML's ipam config.
version: '2.4'
services:
my-service:
image: my-image:latest
networks:
container-lan:
ipv4_address: 192.168.150.10
networks:
container-lan:
driver: bridge
driver_opts:
com.cradlepoint.network.bridge.uuid: 00000002-0d93-319d-8220-4a1fb0372b51
ipam:
driver: default
config:
- subnet: 192.168.150.0/24
gateway: 192.168.150.1A service can attach to multiple LAN networks by listing them under its networks: key:
version: '2.4'
services:
my-service:
image: my-image:latest
networks:
lan1:
ipv4_address: 192.168.150.10
lan2:
ipv4_address: 192.168.250.10
networks:
lan1:
driver: bridge
driver_opts:
com.cradlepoint.network.bridge.uuid: 00000002-0d93-319d-8220-4a1fb0372b51
ipam:
driver: default
config:
- subnet: 192.168.150.0/24
gateway: 192.168.150.1
lan2:
driver: bridge
driver_opts:
com.cradlepoint.network.bridge.uuid: 00000000-0d93-319d-8220-4a1fb0372b51
ipam:
driver: default
config:
- subnet: 192.168.250.0/24
gateway: 192.168.250.1The recommended workflow is to let the Compose Builder in NetCloud Manager generate the network YAML after adding networks in the UI, rather than hand-writing UUIDs.
When using the default bridge network, ports must be explicitly mapped between the host and the container:
- Map specific TCP or UDP ports from the host to the container
- Both the host port and container port must be specified
- Multiple port mappings can be defined per container
Important: Mapped ports are exposed on all LAN and WAN interfaces, and the router firewall does not block them. This is not recommended for secure services — use a LAN IP network assignment instead to control which interfaces the service is reachable on.
services:
my-service:
image: my-image:latest
ports:
- "8080:80" # host:container TCP
- "8443:443" # HTTPS
- "5000:5000/udp" # UDP port
- "9090:9090/tcp" # Explicit TCPData persistence is achieved through named volumes. The router does not allow host filesystem mounts — only named volumes, Config Store, and USB storage are available.
- Named volumes are shared between containers in the same project
- Data in named volumes persists across container restarts
- Volume data is NOT updated when a new image is deployed (create a new project for fresh volumes)
- Be mindful of available storage space on the router
version: '2.4'
services:
my-service:
image: my-image:latest
volumes:
- shared-data:/var/tmp # Named volume
- $CONFIG_STORE # Config Store access (bare, no mount path)
volumes:
shared-data:
driver: localFor USB storage (requires NCOS 7.23.20+), add $USB_STORAGE as a volume. The device mounts at /var/media inside the container.
Host devices can be passed through to containers when hardware access is required:
- Serial ports (e.g.,
/dev/ttyUSB0) - USB devices
- Other character or block devices available on the host
Device passthrough gives the container direct access to the hardware, so appropriate permissions must be configured.
services:
my-service:
image: my-image:latest
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
- "/dev/ttyS0:/dev/ttyS0"
- "/dev/snd:/dev/snd"Environment variables can be set at container launch to configure application behavior without modifying the image. This is useful for:
- API endpoints and credentials
- Feature flags
- Runtime configuration that varies between deployments
services:
my-service:
image: my-image:latest
environment:
- API_ENDPOINT=https://api.example.com
- LOG_LEVEL=info
- FEATURE_FLAG_ENABLED=true
- TZ=UTCRouter hardware has limited CPU and memory compared to server environments. Consider:
- Memory limits — Set appropriate memory caps to prevent a container from exhausting system resources
- CPU limits — Restrict CPU usage to leave headroom for router operations
- Restart policies — Configure automatic restart behavior for fault tolerance
version: '2.4'
services:
my-service:
image: my-image:latest
mem_limit: 128M
restart: unless-stoppedservices:
my-service:
image: my-image:latest
restart: unless-stopped # Restart on failure, not on manual stop
my-critical-service:
image: my-image:latest
restart: always # Always restart regardless of exit status
my-oneshot-task:
image: my-image:latest
restart: on-failure # Only restart if exit code is non-zero- Always cross-compile or build on an ARM64 environment
- Verify that all dependencies are available for musl libc (some libraries assume glibc)
- Keep images minimal to conserve storage and reduce attack surface
- Statically linked binaries avoid C library compatibility issues entirely
- Multi-stage builds help reduce final image size
Build images locally or in CI targeting linux/arm64, then push to a registry. The router pulls pre-built images only — build: directives in Compose are not supported on the device.
Containers on routers should be designed to:
- Start automatically on boot
- Handle restarts gracefully
- Recover from unexpected shutdowns
- Log output to accessible locations for troubleshooting
services:
my-service:
image: my-image:latest
restart: unless-stopped
logging:
driver: json-fileA complete docker-compose.yml combining all concepts:
version: '2.4'
services:
my-service:
image: my-user/my-image:latest
networks:
container-lan:
ipv4_address: 192.168.150.10
volumes:
- app-storage:/app/storage
- $CONFIG_STORE
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
environment:
- API_ENDPOINT=https://api.example.com
- LOG_LEVEL=info
mem_limit: 128M
restart: unless-stopped
logging:
driver: json-file
networks:
container-lan:
driver: bridge
driver_opts:
com.cradlepoint.network.bridge.uuid: 00000002-0d93-319d-8220-4a1fb0372b51
ipam:
driver: default
config:
- subnet: 192.168.150.0/24
gateway: 192.168.150.1
volumes:
app-storage:
driver: local