The Problem
The floor needed a way to print labels with a monotonically-incrementing container number, on demand, without depending on the network. The previous setup was a desktop PC running a vendor utility — fine until the PC needed a reboot, the operator needed a login, or someone wandered off with the keyboard.
Constraints that drove the design:
No network dependency
The label printer has to keep working through outages. That ruled out anything cloud-hosted or anything that called home for a sequence number.
Counter integrity is non-negotiable
Skipping a number is annoying. Duplicating one is a real problem downstream. The counter has to survive crashes, yanked power cords, and SD card hiccups without losing or repeating a value.
Recoverable by a non-engineer
If the Pi dies on a Saturday, the rebuild path can't require me to fly in. It needs to be an SD card and a zip file.
Architecture
A single Raspberry Pi running Pi OS Bookworm boots into a labwc session, autostarts Chromium pointed at localhost:5000, and serves a Flask app behind Waitress. The Flask app talks to a local SQLite database for the counter, and pipes ZPL straight into CUPS for the Zebra ZP505.
┌─────────────────────────────────────────────────┐
│ Raspberry Pi (Bookworm, labwc autostart) │
│ │
│ Chromium kiosk ─► http://localhost:5000 │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Flask + Waitress │ │
│ │ (label printer app) │ │
│ └──────┬───────────┬───┘ │
│ │ │ │
│ ┌──────▼─────┐ ┌──▼───────────┐ │
│ │ SQLite │ │ CUPS │ │
│ │ (WAL, │ │ ZebraZP505 │ │
│ │ FULL) │ │ Raw queue │ │
│ └────────────┘ └──────┬───────┘ │
│ │ │
└─────────────────────────────────────┼───────────┘
│ USB
▼
Zebra ZP505 printer
Everything lives on the Pi. Power-cycle the device, log out, log in — the kiosk reloads and the next label is ready.
Key Decisions
-
SQLite in WAL mode, synchronous=FULL, per-label commitThe counter increments inside a transaction, commits before the print job goes out, and uses
synchronous=FULLso the value hits disk before the call returns. Combined with WAL mode for concurrent readers, this means a yanked power cord at the worst possible moment loses at most the print attempt — never the counter. -
Threading lock around the print pathEven though Waitress is conservative with concurrency, a single in-process lock around the read-increment-print sequence rules out any chance of two simultaneous requests landing on the same number. Belt and suspenders — the SQLite transaction would catch it anyway, but the lock makes the code obvious.
-
Waitress instead of the Flask dev serverThe Flask dev server is fine for laptops, not for a kiosk that has to run for months. Waitress is a single pip install, no Nginx in front, and handles the load (one operator, one label at a time) without complaint.
-
CUPS Raw queue, ZPL piped directlyThe ZP505 only speaks ZPL. CUPS with a Raw queue gets out of the way — the app generates ZPL strings and sends them to
lp -d ZebraZP505. A CUPS test page sends PostScript, which is silently dropped by the printer; verification has to be done with a real ZPL job. -
Barcode-scanner-driven admin recoveryIf the counter ever drifts (a print job fails after the increment, or a bad restore brings back an old DB), the admin page lets the operator scan a recovery barcode to reset the counter to a known value. The USB scanner is the intended input device — much faster and more reliable than an on-screen keyboard on a touch-only kiosk.
-
Redeploy-from-zip over SD card imagingI considered keeping a golden image. In practice, it's about the same time investment to flash a stock Pi OS and run a setup script that drops in the systemd unit, labwc autostart, and the app itself — and you get a clean, current OS at the end. The whole recovery payload is a single zip file.
Lessons Learned
apt autoremove is destructive on Pi OS
During an early hardening pass I ran apt autoremove on the desktop session and watched lightdm refuse to start on the next boot — it had pulled labwc session files as “unused.” The fix on Pi OS for services you don't want is systemctl mask, not removal. Anything you uninstall on a Pi-OS desktop image, you should verify still leaves the session bootable.
Pi Imager mangles passwords with special characters
The pre-imaging configuration step in Raspberry Pi Imager silently mangles passwords containing certain symbols, leaving you with a Pi that won't accept the password you set. The recovery path is editing cmdline.txt to boot with init=/bin/sh and resetting the password manually. Lesson: keep the initial imaging password alphanumeric and rotate it after first boot.
CUPS Raw-queue deprecation is on the horizon
CUPS prints a deprecation warning for Raw queues these days. It still works, but the long-term options are a Zebra ZPL pass-through driver package or skipping CUPS entirely and writing ZPL directly to /dev/usb/lp0. Going direct-to-device removes a whole layer of moving parts — that's the likely next iteration.
Chromium is called chromium on Bookworm
Small thing, but worth writing down: the binary name changed from chromium-browser to chromium on Bookworm. Any autostart script copied from an older guide will silently fail because the binary doesn't exist under the old name.
Stack
Status & Outcomes
Currently in production. The Pi was recently rebuilt from scratch after a power outage corrupted the SD card, and the redeploy-from-zip path proved itself end-to-end: stock Pi OS image, run the setup script, drop the deploy zip, reboot, working printer. Total time from blank card to first printed label was well under what a desktop-PC rebuild would have been.
The counter survived the rebuild because the database file came back from a recent backup, and the admin recovery page closed the gap between the backup point and the last-printed label using a scanned barcode.
What's next
Two items on the horizon: replacing the CUPS Raw queue with a direct write to /dev/usb/lp0 to get ahead of the deprecation, and packaging the setup script + deploy zip into a tagged release so the recovery payload is versioned alongside the app instead of living on a flash drive in my desk.