# 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, generates the `_ipps` certificate, 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. | | `CERT_EXTRA_NAMES` | *(empty)* | Comma-separated extra DNS names and/or IP addresses to put into the generated `_ipps` certificate. | | `CERT_DAYS` | `3650` | Validity of the generated certificate, in days. | 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)" ``` ### TLS certificate for `_ipps` CUPS advertises the queue twice over mDNS: `_ipp._tcp` (plain) and `_ipps._tcp` (TLS). macOS and iOS prefer the secure one, and *validate the certificate against the name they connected to*. The name clients see is Avahi's `.local`, which is not necessarily the container's host name — so the entrypoint installs its own self-signed certificate instead of leaving it to CUPS. Its SANs cover the container host name, `.local`, Avahi's FQDN, `localhost`, `127.0.0.1` and everything in `CERT_EXTRA_NAMES`: ```sh -e CERT_EXTRA_NAMES=192.168.100.20,print.example.org ``` The certificate lives in `/etc/cups/ssl` (so that directory must be writable: use a named volume, see the bind-mount caveat under *Notes and limitations*) and is only regenerated when one of the names it must cover is missing, so a restart normally logs `TLS certificate is up to date`. Inspect what the server actually presents: ```sh podman cp printserver:/etc/cups/ssl/server.crt . openssl x509 -in server.crt -noout -subject -ext subjectAltName echo | openssl s_client -connect localhost:631 -servername "$(hostname).local" \ | openssl x509 -noout -subject -ext subjectAltName ``` Because the certificate is self-signed, a fully validating client (macOS/iOS AirPrint, `curl` without `-k`) rejects it until you trust it. CUPS' own tools (`lpadmin`, `ipptool`) are lenient, so plain printing works without this step: - **macOS** (needs your password): ```sh sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain ./server.crt ``` - **iOS/iPadOS**: AirDrop or mail `server.crt` to the device, install the profile, then enable full trust under *Settings → General → About → Certificate Trust Settings*. Alternatively, keep using plain IPP (port 631 without TLS) — printing and duplex/colour options work there too. ## 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. If *Use:* shows *Generic PostScript Printer*, see [Troubleshooting](#macos-adds-the-printer-as-a-generic-postscript-printer) | | 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). The certificate lives in `/etc/cups/ssl`, so that directory must be writable — use a **writable (named) volume**; see the bind-mount caveat under *Notes and limitations*. If it is not writable, CUPS logs `Unable to encrypt connection: Unable to create server credentials` and the secure connection fails while plain IPP keeps working. The other cause is the certificate itself: clients only accept it when the name they connected to is in its SANs *and* they trust it (see *TLS certificate for `_ipps`*). Typical symptoms: `curl https://.local:631/` ends with `(60) SSL certificate problem`, or `ipptool -tv ipps://…` succeeds while macOS/iOS refuse. Add the missing name via `CERT_EXTRA_NAMES` or trust the certificate. After changing anything, remove the printer in macOS and add it again — macOS caches the failed attempt. ### macOS adds the printer as a Generic PostScript printer Symptom: the queue prints, but *Use:* shows **Generic PostScript Printer** and the print dialog offers no duplex, no colour and no tray 2 — while iOS, which queries the printer live, offers all of them. On the Mac, `lpoptions -p -l` lists only `APOptionalDuplexer` plus `Duplex`/`PageSize`/`ManualFeed`/`InputSlot`, and `/etc/cups/ppd/.ppd` contains `*PCFileName: "Generic.ppd"`. macOS only takes its driverless AirPrint path when it accepts the printer's secure `_ipps` service; if that fails it silently falls back to its bundled generic PostScript driver. Duplex then stays hidden, because the generic PPD gates it behind `APOptionalDuplexer False`. The usual cause is the certificate (see *TLS certificate for `_ipps`*), so check that first: ```sh podman exec printserver avahi-browse -rt _ipps._tcp # what is advertised podman cp printserver:/etc/cups/ssl/server.crt . && openssl x509 -in server.crt -noout -subject -ext subjectAltName ``` A verified way around it is to add the queue from the command line, driverless over the same secure service (`ipptool -tv ipp://:631/printers/` prints the UUID; drop the `urn:uuid:` prefix): ```sh sudo lpadmin -x sudo lpadmin -p -E -m everywhere \ -v "dnssd://%20%40%20._ipps._tcp.local./?uuid=" lpoptions -p -l # Duplex, ColorModel and tray 2 show up now ``` In the GUI, remove the printer and add it again, then pick **AirPrint** (not *Generic PostScript Printer*) under *Use*. If only the generic entry is offered, the secure connection is still being rejected — fix the certificate, or add the queue from the command line as above. ## 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.