
Docker for Hardware Simulation in Dev/CI: Emulating Kiosks, Scanners, and Printers to Reproduce Angular 20+ Defects Fast
Spin up a repeatable device lab in Docker—barcode scanner WebSockets, ESC/POS printer mocks, and kiosk states—wired into Nx + Cypress in minutes.
Make hardware boring. When a kiosk or scanner is “just another container,” bugs stop hiding in the field and start failing fast in CI.Back to all posts
I’ve shipped airport kiosks that had to keep printing boarding passes when Wi‑Fi hiccuped, and retail scanners that swallowed barcodes faster than the UI could paint. The common thread: defects were “field only.” The fix: Dockerized device labs I could spin up locally or in CI to reproduce issues in minutes.
This is a focused playbook for Angular 20+ teams. We’ll emulate scanners over WebSockets, printers over HTTP/IPP with ESC/POS output, and kiosk device states, then wire all of it into an Nx + Cypress + GitHub Actions pipeline. Along the way, we’ll use Signals + SignalStore to make device state explicit and testable.
The airport kiosk that wouldn’t print—and why Docker solved it
A real outage, reproduced in 6 minutes
For a major airline, a kiosk intermittently “printed” without actually cutting paper. In the field, logs were noisy and time-limited. I stood up a Docker printer emulator that dropped ACKs 1 in 10 times. In six minutes, we reproduced it, fixed the ack/timeout logic, and added a deterministic Cypress spec.
Printer accepted jobs but never cut paper
UI showed “Printed” without verifying device acks
Wi‑Fi packet loss masked error handling
Why now matters (2025 roadmaps)
If you plan to upgrade to Angular 20+ (or 21 soon), flaky device flows will block you. A Docker device lab makes upgrades and incident response safer. If you need an Angular consultant to set this up, I’m available for remote engagements.
Q1 is hiring season for Angular talent
Angular 21 beta is near; CI must be green to upgrade safely
Why Docker hardware simulation matters for Angular 20+ teams
Measurable outcomes I’ve seen
When your “hardware” is a container with log history and togglable behaviors, every bug becomes reproducible. You can also stress test: burst 50 scans/second, inject 3% packet loss, or delay print acks by 2 seconds.
10x faster defect reproduction (hours → minutes)
40–60% drop in flaky E2E tests
Fewer hotfixes; easier canary rollbacks
Enterprise constraints it solves
We avoid privileged runners and USB/IP hacks by standardizing on WS/HTTP interfaces that mirror production gateway services. You get portability and deterministic builds.
No USB passthrough needed in CI
Works on GitHub Actions, Jenkins, and Azure DevOps
Repeatable across Nx apps and preview stacks
Reference architecture: device simulators, Angular adapter, and CI harness
// device.store.ts — SignalStore for device state (Angular 20, @ngrx/signals)
import { Injectable, inject } from '@angular/core';
import { SignalStore, withState, withMethods } from '@ngrx/signals';
import { toSignal } from '@angular/core/rxjs-interop';
import { webSocket } from 'rxjs/webSocket';
import { retryBackoff } from 'backoff-rxjs';
interface DeviceState {
scannerConnected: boolean;
lastScan?: { value: string; ts: number };
printerConnected: boolean;
printQueue: number;
errors: string[];
}
@Injectable({ providedIn: 'root' })
export class DeviceStore extends SignalStore(
withState<DeviceState>({ scannerConnected: false, printerConnected: false, printQueue: 0, errors: [] }),
withMethods((store) => {
const scanner$ = webSocket<any>({ url: 'ws://localhost:9001' }).pipe(
retryBackoff({ initialInterval: 500, maxInterval: 5000, randomizationFactor: 0.25 })
);
const msgs = toSignal(scanner$, { initialValue: null });
function connectScanner() {
const m = msgs();
if (!m) return;
if (m.type === 'connected') store.patch({ scannerConnected: true });
if (m.type === 'scan') store.patch({ lastScan: m.payload });
if (m.type === 'error') store.patch({ errors: [...store.state().errors, m.message] });
}
async function print(payload: Blob) {
const res = await fetch('http://localhost:9100/print', { method: 'POST', body: payload });
const json = await res.json();
if (json.status === 'queued') store.patch({ printQueue: store.state().printQueue + 1 });
}
return { connectScanner, print };
})
) {}# simulators/scanner/scanner.js — WS + REST for deterministic tests
const { WebSocketServer } = require('ws');
const express = require('express');
const app = express();
app.use(express.json());
const wss = new WebSocketServer({ port: 9001 });
let sockets = [];
wss.on('connection', ws => {
sockets.push(ws);
ws.send(JSON.stringify({ type: 'connected' }));
ws.on('close', () => (sockets = sockets.filter(s => s !== ws)));
});
app.post('/emit', (req, res) => {
const value = req.body.value || 'TEST-123';
const msg = JSON.stringify({ type: 'scan', payload: { value, ts: Date.now() } });
sockets.forEach(s => s.readyState === 1 && s.send(msg));
res.json({ ok: true });
});
app.listen(9002, () => console.log('scanner REST on :9002'));# simulators/printer/printer.js — HTTP endpoint capturing print jobs + flaky acks
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.post('/print', (req, res) => {
const id = Date.now();
const file = fs.createWriteStream(path.join('/data', `${id}.bin`));
req.pipe(file);
file.on('finish', () => {
// 10% of the time, drop the ack to reproduce field bugs
if (Math.random() < 0.1) return res.destroy();
res.json({ status: 'queued', id });
});
});
app.get('/health', (_, r) => r.json({ ok: true }));
app.listen(9100, () => console.log('printer on :9100'));1) Device simulators (Node + WS/HTTP)
Simulators mimic real protocols, not DOM hacks. The scanner pushes JSON events; the printer accepts buffered jobs and emits acks/errors; kiosk state flips through a REST API so Cypress can script failures.
Scanner over WebSocket with server-push
Printer over HTTP/IPP-like endpoint storing ESC/POS/PDF
Kiosk state via REST: online/offline, paper-low, jam
2) Angular adapter with Signals + SignalStore
Keep device logic in a dedicated DeviceStore. The UI reads signals: connected(), lastScan(), printQueue(), error(). This keeps templates declarative and E2E tests stable.
Centralize device state
Typed events and error taxonomy
Exponential backoff + jitter
3) CI harness (Nx + Cypress + Actions)
Compose brings up services at known ports, Cypress drives scenarios by hitting REST endpoints, and Actions/Jenkins collects logs for triage.
Docker Compose starts simulators
Cypress posts events to simulators
Artifacts include logs and captured prints
Docker Compose and GitHub Actions pipeline
# docker-compose.yml
version: '3.9'
services:
scanner:
image: node:20-alpine
working_dir: /app
volumes:
- ./simulators/scanner:/app
command: sh -c "npm i ws express && node scanner.js"
ports: ["9001:9001", "9002:9002"]
printer:
image: node:20-alpine
working_dir: /app
volumes:
- ./simulators/printer:/app
- ./artifacts/prints:/data
command: sh -c "npm i express && node printer.js"
ports: ["9100:9100"]# .github/workflows/e2e.yml
name: e2e
on: [push, pull_request]
jobs:
cypress:
runs-on: ubuntu-latest
services:
scanner:
image: node:20-alpine
options: >-
--health-cmd "wget -qO- localhost:9002 || exit 1" --health-interval 3s --health-timeout 2s --health-retries 20
ports:
- 9001:9001
- 9002:9002
volumes:
- ${{ github.workspace }}/simulators/scanner:/app
command: sh -c "cd /app && npm i ws express && node scanner.js"
printer:
image: node:20-alpine
options: >-
--health-cmd "wget -qO- localhost:9100/health || exit 1" --health-interval 3s --health-timeout 2s --health-retries 20
ports:
- 9100:9100
volumes:
- ${{ github.workspace }}/simulators/printer:/app
- ${{ github.workspace }}/artifacts/prints:/data
command: sh -c "cd /app && npm i express && node printer.js"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx nx run web:build
- run: npx http-server dist/apps/web -p 4200 &
- run: npx wait-on http://localhost:4200
- run: npx cypress run --browser chrome --record false
- uses: actions/upload-artifact@v4
with: { name: print-artifacts, path: artifacts/prints }// cypress/e2e/scanner.cy.ts
it('accepts a barcode and routes to order page', () => {
cy.visit('/scan');
// inject deterministic scan
cy.request('POST', 'http://localhost:9002/emit', { value: 'ORDER-ABC-123' });
// assert UI reacts via DeviceStore (Signals)
cy.findByRole('status', { name: /scanner connected/i }).should('exist');
cy.findByText(/ORDER-ABC-123/).should('be.visible');
});Compose file for local dev and CI
One file powers both local dev and CI. Volumes capture artifacts (scans, prints) for triage.
GitHub Actions with Nx + Cypress
This is the pattern I drop into Jenkins and Azure DevOps as well—identical steps, different YAML.
Boot simulators as services
Wait for health endpoints
Run Cypress headless in parallel
UI states, accessibility, and telemetry
<!-- status-banner.component.html -->
<p-toast></p-toast>
<div role="status" aria-live="polite" class="status-banner" [class.error]="device.errors().length">
<ng-container *ngIf="device.scannerConnected(); else off">
<i class="pi pi-barcode"></i> Scanner connected
</ng-container>
<ng-template #off>
<i class="pi pi-ban"></i> Scanner offline — retrying…
</ng-template>
</div>.status-banner {
padding: var(--space-2);
background: var(--surface-50);
&.error { background: var(--red-50); color: var(--red-900); }
}Accessible device indicators
I pair PrimeNG components with design tokens for success/warn/error and density. Live regions announce device changes to screen readers—critical for ADA compliance in kiosks.
role=status for live updates
High-contrast tokens for error states
Keyboard-first retry flows
Typed events + Firebase logs
I log scan and print events to Firebase/GA4 with a correlation ID so we can chart percentiles on dashboards. That’s how we proved a 42% reduction in failed print acks after shipping retry logic.
Event schema versioning
Sample-rate noisy events
Correlate scan→print latency
When to hire an Angular developer for legacy rescue
Signals migration meets devices
If your app is mid-upgrade (12–15 → 20) and device flows are flaky, bring in an Angular expert who has stabilized kiosks and scanners. I’ll stand up the Docker lab, add a SignalStore, and leave you with deterministic tests.
Zoneless + Signals can surface race conditions
Device gateways hide USB/driver variance
Typical timeline
For a telecom ads platform, we reproduced barcode latency spikes and cut mean scan→render to <120ms via WebSocket + Signals. For an airline kiosk, we removed phantom success toasts and added printer acks with retries and backoff.
2–3 days to baseline simulators and Compose
1–2 weeks to add DeviceStore + tests
Parallel track to upgrade Angular safely
What to measure next
Reliability and UX KPIs
Hook metrics into Angular DevTools profiling, GA4, and your logs. If you use Firebase, stream logs with correlation IDs. For dashboards, I use D3/Highcharts with WebSocket updates and data virtualization for long sessions.
MTTR for device incidents
Scan→route p95
Print ack p99 and error taxonomy coverage
Key takeaways
- Dockerized device simulators make field-only bugs reproducible in minutes, not days.
- Model device state in Angular with Signals + SignalStore to drive offline, retry, and UI indicators.
- Use WebSocket for scanner input and HTTP/IPP for print to mirror real protocols without USB passthrough.
- Wire simulators into Nx + Cypress and GitHub Actions for deterministic, parallelizable CI.
- Instrument everything—typed event schemas, Firebase logs, and GA4—to prove reliability gains.
Implementation checklist
- Define typed event schemas for scanner, printer, and kiosk states.
- Create Node-based simulators: WS for scanner, HTTP/IPP for print, REST for device state flips.
- Expose simulators via Docker Compose; tag and version images.
- Add an Angular DeviceStore (Signals + SignalStore) to track connection, last events, and errors.
- Implement exponential backoff, jitter, and offline banners with accessible ARIA states.
- Write Cypress tests that post events to simulators and assert UI updates.
- Run simulators as GitHub Actions/Jenkins services; wait-on health endpoints.
- Collect metrics: mean time-to-reproduce, test flake rate, print render size, and Lighthouse/UX deltas.
Questions we hear from teams
- What does an Angular consultant do for Docker hardware simulation?
- Stand up scanner/printer/kiosk simulators in Docker, add a Signals-based DeviceStore, wire Nx + Cypress tests, and integrate logs/metrics. The outcome: reproducible defects, stable CI, and measurable reliability gains.
- How long does it take to set up a device lab for Angular 20+?
- Baseline simulators and Compose typically take 2–3 days. Adding a DeviceStore, retries, and 8–12 deterministic Cypress specs usually takes 1–2 weeks, depending on legacy complexity and CI platform.
- How much does it cost to hire an Angular developer for this work?
- Most teams invest 2–4 weeks of senior Angular engineering. I offer fixed-scope packages for setup plus training, or flexible retainers. Book a discovery call for a tailored estimate within 48 hours.
- Can we run this on GitHub Actions, Jenkins, or Azure DevOps?
- Yes. The same Docker images and health checks work across GitHub Actions, Jenkins, and Azure DevOps. I provide drop-in YAML and scripts plus artifact uploads for print/scan logs.
- Will this work with Firebase Hosting or SSR?
- Yes. The simulators run as sidecar services; your Angular app can be SSR or static on Firebase Hosting. The DeviceStore remains framework-agnostic and works with Signals and zoneless setups.
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