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:

<</Duplex true /Tumble false>>setpagedevice
<</BRProcessColor 4 /BRHrc 0>>setpagedevice
<</ManualFeed false /BRTraysw false /BRFeeder 1>> 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:

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:

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://<ip>:9100 — raw AppSocket/JetDirect
lpd lpd://<ip>/binary_p1 — LPR/LPD
ipp ipp://<ip>/ipp/print

For example, a printer at 192.168.100.7:

-e PRINTER_IP=192.168.100.7

Or specify the URI explicitly for full control:

-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):

-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 <host>.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, <host name>.local, Avahi's FQDN, localhost, 127.0.0.1 and everything in CERT_EXTRA_NAMES:

-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:

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):

    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.:

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
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:

open http://localhost:631/            # printers
open http://localhost:631/admin       # administration

From the host, or inside the container:

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:

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:

    -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:

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):

    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://<host>.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 <queue> -l lists only APOptionalDuplexer plus Duplex/PageSize/ManualFeed/InputSlot, and /etc/cups/ppd/<queue>.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:

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://<host>:631/printers/<queue> prints the UUID; drop the urn:uuid: prefix):

sudo lpadmin -x <queue>
sudo lpadmin -p <queue> -E -m everywhere \
  -v "dnssd://<queue>%20%40%20<host>._ipps._tcp.local./?uuid=<printer-uuid>"
lpoptions -p <queue> -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 <driver>) 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.

S
Description
A containerized CUPS print server that shares a Brother HL-4150CDN network printer over IPP and advertises it via AirPrint (mDNS/DNS-SD).
Readme MIT
1.2 MiB
0 Stars 1 Watchers 0 Forks
Languages
Shell 90%
Dockerfile 10%