Dockerized Device Labs for Angular 20 CI: Kiosk, Scanner, and Printer Emulation That Reproduces Defects 10x Faster

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

Record 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

Related Resources

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.

Hire Matthew — Remote Angular Expert for Device-Heavy Apps Review Your Angular CI & Device Simulation 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
NG Wave Component Library

Related resources