# Printserver A containerized CUPS print server that shares a **Brother HL-4150CDN** network printer over IPP and advertises it via AirPrint (mDNS/DNS-SD). It prints by sending **PostScript (BR-Script3)** straight to the printer, so no vendor driver and no 32-bit libraries run in the container. The container bundles: - CUPS (IPP + web admin on port 631) with cups-filters for format conversion - Avahi / DNS-SD for AirPrint discovery (UDP 5353) - Brother's official HL-4150CDN **BR-Script3 PPD** for device options ## How it works The HL-4150CDN emulates PCL6 and BR-Script3 (PostScript 3), and accepts PostScript over raw AppSocket (port 9100). CUPS converts whatever a client sends into PostScript and the BR-Script3 PPD turns the selected queue options into PostScript `setpagedevice` instructions: ``` macOS / iOS / Windows / Linux container printer ┌─────────────────┐ IPP/AirPrint ┌──────────────────────────┐ raw ┌────────────┐ │ AirPrint (URF) │ ───────────────▶ │ CUPS │ ──────▶ │ HL-4150CDN │ │ PDF / PostScript│ │ URF/PWG/PDF → PostScript│ :9100 │ BR-Script3 │ └─────────────────┘ │ pstops → setpagedevice │ └────────────┘ └──────────────────────────┘ ``` For example, `Duplex=DuplexNoTumble BRPrintQuality=Color InputSlot=Tray2` becomes: ```postscript <>setpagedevice <>setpagedevice <> setpagedevice ``` ## Layout | Path | Purpose | | --- | --- | | `Containerfile` | Image definition (Debian base, CUPS + cups-filters + Avahi, BR-Script3 PPD) | | `cupsd.conf` | Container CUPS configuration (listen on all interfaces, share printers) | | `entrypoint.sh` | Starts D-Bus, Avahi and CUPS, then registers the printer queue | | `assets/Brother HL-4150CDN/ppd/` | Official BR-Script3 PPD used for the queue | | `assets/Brother HL-4150CDN/Windows/` | Vendor Windows driver the PPD was extracted from | ## Prerequisites - Podman (or a compatible builder such as Docker/Buildah) - The printer reachable on the network, with a known IP address or hostname ## Build Run from the repository root: ```sh podman build -t printserver:latest . ``` The image is architecture-independent (no i386 or other 32-bit binaries), so it builds natively on x86_64 and arm64 hosts alike. ## Run mDNS does not work through a normal container network, so use **host networking**: ```sh podman run -d --name printserver \ --network host \ -e PRINTER_IP=192.168.100.7 \ -e PRINTER_NAME=HL-4150CDN \ printserver:latest ``` ### Environment variables | Variable | Default | Description | | --- | --- | --- | | `PRINTER_IP` | *(empty)* | IP address / host name of the network printer. A device URI is derived from it (see `PRINTER_PROTOCOL`). | | `PRINTER_URI` | *(empty)* | Full device URI. Overrides `PRINTER_IP` when set. | | `PRINTER_PROTOCOL` | `socket` | Protocol used with `PRINTER_IP`: `socket`, `lpd` or `ipp`. | | `PRINTER_NAME` | `HL4150CDN` | CUPS queue name. Also used as the name advertised via AirPrint/DNS-SD. | | `PRINTER_DESCRIPTION` | *(empty)* | CUPS printer description (`lpadmin -D`), shown in the CUPS web UI. | | `PPD_FILE` | `/usr/share/cups/model/BR4150_2.PPD` | PPD used for the queue. | No queue is created when neither `PRINTER_IP` nor `PRINTER_URI` is set. ### Choosing the connection The HL-4150CDN accepts PostScript directly, so the default is raw AppSocket. Set `PRINTER_IP` and let the entrypoint build the URI: | `PRINTER_PROTOCOL` | Resulting URI | | --- | --- | | `socket` (default) | `socket://:9100` — raw AppSocket/JetDirect | | `lpd` | `lpd:///binary_p1` — LPR/LPD | | `ipp` | `ipp:///ipp/print` | For example, a printer at `192.168.100.7`: ```sh -e PRINTER_IP=192.168.100.7 ``` Or specify the URI explicitly for full control: ```sh -e PRINTER_URI=socket://192.168.100.7:9100 ``` ### Advertised name `PRINTER_NAME` is used as the CUPS queue name and as the name clients see in AirPrint discovery. To show a descriptive label in the CUPS web interface, set `PRINTER_DESCRIPTION` (this does not change the advertised name): ```sh -e PRINTER_NAME=HL4150CDN -e PRINTER_DESCRIPTION="Brother HL-4150CDN (Office)" ``` ## Printing options The BR-Script3 PPD exposes the HL-4150CDN's hardware options. Common ones: | Option | PPD keyword | Values | | --- | --- | --- | | Paper source | `InputSlot` | `AutoSelect`, `Tray1`, `Tray2`, `MPTray` | | Two-sided printing | `Duplex` | `None`, `DuplexNoTumble` (long edge), `DuplexTumble` (short edge) | | Colour / mono | `BRPrintQuality` | `Auto`, `Color`, `Black` | | Colour mode | `BRColorMode` | `Normal`, `Vivid`, `None` | | Media type | `BRMediaType` | `Plain`, `Thin`, `Thick`, `Recycled`, `Glossy`, `Label`, envelopes, … | | Print quality | `CAPT` | `Fine` (600×600), `SuperFine` (2400 dpi class) | | Toner save | `TonerSaveMode` | `True`, `False` | | Secure print | `BRJobHold` | `None`, `Private` (+ `BRJobPIN`) | They can be selected per job, e.g.: ```sh podman exec printserver lp -d HL4150CDN \ -o Duplex=DuplexNoTumble -o BRPrintQuality=Color -o InputSlot=Tray1 report.pdf ``` ## Client support | Platform | Path | Notes | | --- | --- | --- | | macOS | AirPrint / IPP Everywhere | Discovered via mDNS; no driver needed | | iOS / iPadOS | AirPrint | Discovered via mDNS; no driver needed | | Linux | IPP / IPP Everywhere | Native or with the shared PPD | | Windows | IPP or raw 9100 | Install Brother's **BR-Script3** Windows driver, pointed at the printer or the shared queue | Windows uses a client-side Brother driver; the server does not share via SMB. ## Verify Browse the CUPS web interface: ```sh open http://localhost:631/ # printers open http://localhost:631/admin # administration ``` From the host, or inside the container: ```sh podman exec printserver lpstat -t # queues and status podman exec printserver lpstat -v HL4150CDN # device URI podman exec printserver lpoptions -p HL4150CDN -l # available options podman exec printserver avahi-browse -rt _ipp._tcp # mDNS advertisement ``` A print job can be submitted with: ```sh podman exec printserver lp -d HL4150CDN /etc/hostname ``` On macOS/iOS the shared queue should appear in the printer list via AirPrint. ## Notes and limitations - **Host networking is required** for Avahi to send and receive mDNS traffic. `-p 631:631` alone is enough for direct IPP access but not for AirPrint discovery. - **Printer state is not persisted by default.** The queue is recreated on every start from `PRINTER_IP`/`PRINTER_URI`, so nothing has to survive a restart. To keep CUPS state (extra queues, subscriptions) across container replacement, mount a volume over `/etc/cups` — **use a named volume**: ```sh -v printserver-cups:/etc/cups ``` Podman seeds a *new named volume* with the image's `/etc/cups`, so `cupsd.conf` is present. A **bind mount to an empty host directory does not**: it hides the image's configuration, `cupsd` then exits immediately (status 1, silently — its default `ErrorLog` is syslog), the container stops, and the entrypoint's `lpadmin` fails with a misleading `lpadmin: Unable to connect to server: Bad file descriptor`. This is easiest to hit from a quadlet that bind-mounts a share, e.g. `Volume=/nfs/share/print-server/:/etc/cups/:Z`. If you must bind-mount, make sure the directory already contains a `cupsd.conf`; also note that a read-only share, NFS `root_squash` or SELinux relabeling can still prevent `cupsd` from writing its state there. For a named volume in a quadlet, use `Volume=printserver-cups:/etc/cups` (a bare name, no leading `/`). - Colour handling is done by the printer's own PostScript RIP. Output may differ slightly from Brother's host-side (rasterising) driver. - The queue advertises AirPrint capabilities (`image/urf`, `image/pwg-raster`, `sides-supported`, `color-supported`). macOS and iOS printing is converted by cups-filters; verify end to end on your network. ## Troubleshooting ### The container runs but nothing answers on 631 The queue can be advertised over mDNS while nothing is actually listening, so clients discover a printer they cannot reach. Check inside the container: ```sh podman exec printserver curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:631/ podman exec printserver tail -20 /var/log/cups/error_log ``` **`Unable to open listen socket for address 0.0.0.0:631 - Permission denied`** — `cupsd` never bound port 631. This is the normal outcome of **rootless Podman with `Network=host`**: port 631 is privileged, and a rootless container does not hold `CAP_NET_BIND_SERVICE` in the host network namespace (adding the capability does not help, because the capability must belong to the *host* user namespace). `cupsd` keeps running on its domain socket, so `lpadmin`/`lpstat` inside the container still work — which makes the failure easy to miss. Fix one of: - allow unprivileged binding of port 631 on the host, persistently (needs root): ```sh echo 'net.ipv4.ip_unprivileged_port_start=631' | sudo tee /etc/sysctl.d/90-unprivileged-ports.conf sudo sysctl --system ``` - run the container as a rootful (system) service rather than a user quadlet; - or serve on an unprivileged port (CUPS advertises the actual port in its SRV record, but be aware some AirPrint clients assume 631). On SELinux systems, verify there is no port-labelling denial as well (`sudo ausearch -m avc -ts recent`). Also ensure 631/tcp and 5353/udp are open in the firewall. ### macOS/iOS sees the printer but reports it cannot connect If plain IPP works but the *secure* connection fails, this is the `_ipps` service: macOS prefers `_ipps._tcp`, which CUPS advertises unconditionally (also when no certificate exists). CUPS creates its TLS certificate lazily on the first TLS connection, in `/etc/cups/ssl` — so that directory must be writable. If it is not, CUPS logs `Unable to encrypt connection: Unable to create server credentials` and the secure connection fails while plain IPP keeps working. Use a **writable (named) volume** for `/etc/cups`; see the bind-mount caveat under *Notes and limitations*. After changing anything, remove the printer in macOS and add it again — macOS caches the failed attempt. ## CUPS 3 and the future of PPDs Adding the queue with a PPD makes `lpadmin` print: ``` lpadmin: Printer drivers are deprecated and will stop working in a future version of CUPS. ``` This is an advance notice, not an error. OpenPrinting deprecated PPDs, printer drivers and filter executables during the CUPS 2.x series, and **CUPS 3.x drops them entirely** in favour of an all-IPP architecture (a *local* server for temporary driverless queues plus a *sharing* server for persistent ones, with filtering limited to PDF, PWG Raster, Apple Raster and PCLm). Any queue created with a PPD (`-P`) or a driver model (`-m `) triggers the message — it is not specific to Brother, and there is no way to keep the PPD and silence it. A raw queue and `-m everywhere` (driverless) do *not* print it. The driverless replacement is a **Printer Application**: a PAPPL-based service that emulates a driverless IPP printer and does the format/option conversion on the other side. For a PostScript device like the HL-4150CDN that role is filled by the **PostScript Printer Application** (`ps-printer-app`), which takes over exactly what our BR-Script3 PPD does today — the PPD does not disappear, it just moves out of CUPS and into the printer application. Note the HL-4150CDN is itself *not* IPP Everywhere / AirPrint capable (Brother lists it as such), so the plain `-m everywhere` shortcut is not available here. This container instead advertises AirPrint to its *clients* and performs the PostScript conversion itself. Nothing needs to change right now: the distro images used here ship CUPS 2.4.x and PPD-based drivers keep working for years. When you eventually move to CUPS 3.x, run `ps-printer-app` (or `legacy-printer-app`) alongside CUPS and create the queue driverlessly against it (`ipp://…`), leaving the PPD inside the printer application.