
Dockerized Device Labs for Angular 20 CI: Kiosk, Scanner, and Printer Emulation That Reproduces Defects 10x Faster
How I use Docker to emulate kiosks, scanners, and printers so Angular teams can reproduce hardware defects reliably in dev and CI—without a lab full of devices.
We turned a ‘printer jam’ ghost bug into an 8‑minute repro script and cut CI flakes by ~70%—no lab time, no guesswork.Back to all posts
I’ve shipped airport kiosks, device portals, and telematics dashboards where the worst bugs only show up when a scanner misreads, a printer jams, or Wi‑Fi blips. Early on, my team and I were blocked by “works on my machine” and flaky labs. The fix: Dockerized device emulators wired into Angular 20+ CI so we can deterministically replay the chaos.
In the airline kiosk project, we containerized a barcode scanner, ESC/POS ticket printer, and a “device-bus” gateway. QA could trigger scan/print/timeout scenarios from Cypress during GitHub Actions runs. Result: defect reproduction went from hours to minutes, and CI flakes dropped by ~70%.
These notes summarize the device simulation pattern I now roll into Nx monorepos: a typed event schema, a WebSocket/REST device bus, Docker images for each peripheral, and Angular services powered by Signals + SignalStore.
Why Angular Teams Need Dockerized Peripherals in 2025
The real-world pain
Kiosk and device-heavy products fail in ways mocks never cover: subtle timing, partial reads, buffer overflows, paper-low states. If your CI can’t emulate these, you ship blind.
Field-only bugs
Flaky device labs
Non-deterministic CI
What changes with Docker
Containerized emulators produce repeatable sequences across dev, QA, and CI. You can scale runners and run 20 kiosk flows in parallel without renting hardware benches.
Deterministic inputs
Parallelizable CI
Portable labs
Measured outcomes from my projects
On a major airline kiosk, we cut ‘can’t reproduce’ tickets drastically and widened test coverage to include jams, retries, and power-loss recovery—scenarios impossible to schedule on shared hardware.
10x faster reproduction
~70% flake reduction
Broader coverage
Reference Architecture: Docker Device Bus + Emulators + Angular 20
// libs/device-events/src/lib/schema.ts
export type DeviceEventType = 'scan' | 'print' | 'status' | 'error';
export interface DeviceEventBase {
id: string; // uuid
ts: number; // epoch ms
type: DeviceEventType;
source: 'scanner' | 'printer' | 'kiosk-os';
corr?: string; // correlation id for test runs
v: 1; // schema version
}
export interface ScanEvent extends DeviceEventBase {
type: 'scan';
payload: { symbology: 'QR' | 'CODE128'; data: string; quality: number };
}
export interface PrintEvent extends DeviceEventBase {
type: 'print';
payload: { template: 'boarding-pass' | 'receipt'; bytes: string /* base64 ESC/POS */ };
}
export interface StatusEvent extends DeviceEventBase {
type: 'status';
payload: { device: 'scanner' | 'printer'; state: 'ok' | 'paper-low' | 'offline' };
}
export type DeviceEvent = ScanEvent | PrintEvent | StatusEvent;Core pieces
The device-bus normalizes events (REST/WebSocket) and broadcasts to Angular clients. Emulators implement the typed schema and expose control endpoints (e.g., /emit/scan, /printer/status/paper-low).
device-bus (Node.js)
scanner-emulator
printer-emulator
network failures
Event schema first
Define a versioned event schema for scans, prints, and device status. Emit the same payloads in dev, CI, and production so traces are portable.
Typed contracts
Replayable
Versioned
docker-compose for Scanner and Printer Emulation
# tools/docker/docker-compose.devices.yml
version: '3.9'
services:
device-bus:
image: ghcr.io/your-org/device-bus:1.2.0
ports: ["8081:8081"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
interval: 5s
timeout: 2s
retries: 10
scanner-emulator:
image: ghcr.io/your-org/scanner-emulator:0.9.0
environment:
- BUS_URL=http://device-bus:8081
depends_on: { device-bus: { condition: service_healthy } }
printer-emulator:
image: ghcr.io/your-org/printer-emulator:0.9.0
environment:
- BUS_URL=http://device-bus:8081
depends_on: { device-bus: { condition: service_healthy } }
app:
build:
context: ../../
dockerfile: tools/docker/Dockerfile.web
environment:
- DEVICE_BUS_WS=ws://device-bus:8081
ports: ["4200:4200"]
depends_on: { device-bus: { condition: service_healthy } }Compose services
Each emulator runs its own container and registers with the device-bus. The Angular app connects to the bus via WebSocket for live events.
device-bus
scanner
printer
app under test
Local parity with CI
Use the same compose in dev and CI to avoid “works locally” gaps. Healthchecks make test startup deterministic.
Same compose file
Env flags
Healthchecks
Angular 20 Integration: Signals + SignalStore for Device State
// libs/device-client/src/lib/device.store.ts
import { signalStore, withState, withMethods } from '@ngrx/signals';
import { signal } from '@angular/core';
import type { DeviceEvent } from '@device-events/schema';
interface DeviceState {
status: Record<string, 'ok' | 'paper-low' | 'offline'>;
lastScan?: string;
}
export const DeviceStore = signalStore(
withState<DeviceState>({ status: {} }),
withMethods((state) => {
let ws: WebSocket | undefined;
const endpoint = (globalThis as any).DEVICE_BUS_WS ?? 'ws://localhost:8081';
const connect = () => {
ws?.close();
ws = new WebSocket(endpoint + '/ws');
ws.onmessage = (e) => {
const ev: DeviceEvent = JSON.parse(e.data);
if (ev.type === 'scan') state.lastScan = ev.payload.data;
if (ev.type === 'status') state.status[ev.payload.device] = ev.payload.state;
};
ws.onclose = () => setTimeout(connect, 1000); // naive backoff for brevity
};
const send = (ev: DeviceEvent) => ws?.readyState === 1 && ws.send(JSON.stringify(ev));
return { connect, send, status: signal(state.status), lastScan: signal(state.lastScan) };
})
);DeviceStore
A small SignalStore tracks device status/events and exposes actions to send commands during tests. PrimeNG toasts and badges surface status to operators.
Signals for status
Actions for emit
Derived selectors
WebSocket client
Use exponential backoff and a typed message map. Feature-flags can switch between emulators and live peripherals.
Reconnect with backoff
Typed messages
Test-friendly
Cypress: Driving Devices and Asserting Prints
// apps/e2e/support/commands.ts
Cypress.Commands.add('emitScan', (data: string) => {
cy.request('POST', 'http://localhost:8081/emit/scan', { data, symbology: 'QR', corr: Cypress.env('RUN_ID') });
});
Cypress.Commands.add('setPaperLow', () => {
cy.request('POST', 'http://localhost:8081/printer/state', { state: 'paper-low' });
});
Cypress.Commands.add('getLastPrint', () => {
return cy.request('GET', 'http://localhost:8081/printer/last');
});// apps/e2e/integration/kiosk.spec.ts
it('prints boarding pass after scan, handles paper-low gracefully', () => {
cy.visit('/check-in');
cy.emitScan('PAX:MATTHEW;FL:1234;SEAT:12A');
cy.findByRole('button', { name: /print/i }).click();
cy.setPaperLow();
cy.contains('Printer paper is low').should('be.visible');
cy.getLastPrint().then(({ body }) => {
expect(body.template).to.eq('boarding-pass');
expect(body.bytes).to.match(/^JVBERi0xLjc/); // base64 prefix for PDF or ESC/POS capture
});
});Custom commands
Drive the emulator through REST and capture printed bytes to assert templates, barcodes, and totals.
emit scan
paper-low
print capture
Deterministic flows
Tag each run with a corr id to correlate UI and device logs.
Reset device state
Seed data
Correlate runs
CI Integration: GitHub Actions, Jenkins, Azure DevOps
# .github/workflows/e2e.yml
name: e2e-device-lab
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
docker:
image: docker:stable-dind
privileged: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- name: Start device lab
run: |
docker compose -f tools/docker/docker-compose.devices.yml up -d --build
for i in {1..30}; do curl -fsS http://localhost:8081/health && break || sleep 2; done
- name: Run Cypress
run: npx nx run kiosk-e2e:e2e --configuration=ci
env:
RUN_ID: ${{ github.run_id }}
- name: Collect artifacts
if: always()
run: |
docker logs $(docker ps -q --filter name=device-bus) > device-bus.log || true
- uses: actions/upload-artifact@v4
if: always()
with:
name: device-logs
path: |
device-bus.log
**/printer-outputs/**Spin up emulators
Run docker-compose in a service step, wait for health, then execute Cypress. Matrix your flows by device state.
Compose up
Healthcheck wait
Parallel shards
Artifacts and logs
Upload print bytes and device-bus logs for debugging. Keep the corr id consistent.
Print captures
Device logs
HAR files
Replay Field Defects with Typed Event Traces
# tools/replay.sh
jq -c '.events[]' traces/flight-checkin-2025-01-12.json > traces/run.jsonl
while read -r line; do
curl -sS -X POST http://localhost:8081/replay --data "$line" \
-H 'Content-Type: application/json'
sleep 0.2
done < traces/run.jsonlRecord once, replay everywhere
Record DeviceEvent sequences in production via Firebase Logs/GA4 debug views. Sanitize PII, then replay in CI against the emulator.
GA4/Firebase logs
JSONL traces
CI harness
Error taxonomy
Classify errors so you can measure test coverage by failure mode. Did we replay ‘paper-low + reconnect’ yet?
status vs. transport vs. ui
Tags for triage
Coverage heatmap
Device Safety, Accessibility, and UX Polish
Safety in CI
Lock emulators to an internal Docker network; never ship emulators in production images. Feature flags choose emulator vs. HID/USB drivers.
No real ports
Sandboxed images
Config flags
Accessibility in kiosk UI
Surface device state via PrimeNG badges and ARIA live regions; audit with Lighthouse and Angular DevTools. Offline/low-paper must be perceivable and operable.
ARIA for device status
Color contrast
Focus traps
Performance budgets
Track hydration (<1500ms on kiosk hardware) and ensure event bursts don’t starve UI threads; Signals + SignalStore helps keep reactivity tight.
Hydration time
Bundle size
WebSocket pressure
When to Hire an Angular Developer for Legacy Rescue
Common starting points
If you’re sitting on a legacy kiosk app, or CI is red due to device flake, bring in an Angular consultant who has shipped Dockerized device labs and zoneless upgrades.
AngularJS migration
Zone.js flakiness
CI red on E2E
Typical timeline
I usually deliver a PoC device lab in 1–2 weeks inside an Nx monorepo, wire Cypress, and hand off a reproducible CI setup with dashboards for coverage.
Assessment: 3–5 days
PoC: 1–2 weeks
Rollout: 2–4 weeks
What to Instrument Next
Metrics to track
Add GA4/Firebase counters for device error classes, wire CI to publish coverage per scenario, and report week-over-week improvements to leadership.
Reproduction time
Flake rate
Scenario coverage
Expand the lab
Add chaos proxies to simulate latency/jitter and card reader emulators with L2/L3 error injections.
Card readers
Signature pads
Network chaos
Key takeaways
- Emulate kiosks, scanners, and printers with Docker to remove lab dependencies and flakiness.
- Standardize device I/O behind a typed event schema and replay traces to reproduce field defects.
- Run Cypress against emulators in CI (GitHub Actions/Jenkins) for deterministic E2E.
- Wire Angular 20 apps to a device bus via WebSocket/REST using Signals + SignalStore.
- Expect 5–10x faster defect reproduction and a sharp drop in “can’t reproduce” tickets.
Implementation checklist
- Define a typed event schema for all device interactions.
- Create Docker images for scanner, printer, and a device-bus gateway.
- Expose REST/WebSocket control endpoints for test orchestration.
- Add Cypress commands to push device events and assert UI/print output.
- Record production traces and build a replay harness in CI.
- Gate merges on emulator-backed E2E and lighthouse/perf budgets.
- Track metrics: reproduction time, flake rate, error taxonomy coverage.
Questions we hear from teams
- How long does it take to implement Docker device simulation for an Angular app?
- Assessment in 3–5 days, a PoC lab in 1–2 weeks, and rollout in 2–4 weeks. Timelines vary by peripherals and CI (GitHub Actions, Jenkins, Azure DevOps). I prioritize reproducibility, typed schemas, and CI gates.
- What does an Angular consultant deliver for hardware simulation?
- A Dockerized device lab (scanner, printer, device-bus), Angular 20 services using Signals + SignalStore, Cypress E2E, and CI integration with artifacts and logs. Plus a replay harness for field traces and documentation your team can maintain.
- Will this work on Windows and macOS developer machines?
- Yes. Everything runs in Docker. Developers use the same docker-compose as CI, so dev parity is high. HID/USB passthrough isn’t required because emulators communicate over WebSocket/HTTP.
- How much does it cost to hire an Angular developer for this setup?
- Engagements typically start at a two‑week PoC. Fixed‑price or retainer options are available. See rates on AngularUX or request a scoped quote based on devices, CI provider, and coverage goals.
- What’s the difference between mocks and emulators here?
- Mocks run in-process and can miss timing and state issues. Emulators are external processes that model real devices and timing, exposing control endpoints. They produce more realistic failure modes and reproducible CI results.
Ready to level up your Angular experience?
Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.
NG Wave
Angular Component Library
A comprehensive collection of 110+ animated, interactive, and customizable Angular components. Converted from React Bits with full feature parity, built with Angular Signals, GSAP animations, and Three.js for stunning visual effects.
Explore Components