// Hardware + Flask

Offline Container Label Printer

2026 Live

A Raspberry Pi kiosk that prints sequentially-numbered container labels to a Zebra ZP505 over CUPS. Runs a hardened Flask app with a WAL-mode SQLite counter, a barcode-scanner-driven admin recovery page, and a redeploy-from-zip rebuild path that takes a bare SD card to a working printer in minutes.

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 commit
    The counter increments inside a transaction, commits before the print job goes out, and uses synchronous=FULL so 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 path
    Even 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 server
    The 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 directly
    The 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 recovery
    If 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 imaging
    I 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

Hardware
Raspberry Pi + Zebra ZP505
OS
Pi OS Bookworm
App framework
Flask
WSGI server
Waitress
Database
SQLite (WAL, FULL)
Print stack
CUPS Raw queue + ZPL
UI shell
Chromium kiosk on labwc
Service mgmt
systemd
Input
USB barcode scanner
Storage
SanDisk High Endurance 32GB

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.