Creating a CUPS print server in Kubernetes

Kubernetes CUPS print server

Why did Reece want to do this?

Printing via CUPS is a very well trodden path. So much so that at Reece we are migrating away from VM’s running CUPS to Kubernetes pods. Adding, deleting and managing printers can be automated via git to allow a CUPS pod to support the thousands of print jobs we do at Reece every day.

Removing the overhead of managing VM’s and moving to a more structured pipeline as code build and deploy model allows more people to offer ideas and support for printing at Reece.

How are printers shared

The default behaviour of CUPS browsing is to broadcast printers using IPP on the CUPS instance layer 2 subnet. Whilst this has worked very well for Reece when using VM’s, this does not work when CUPS is moved to K8S as a Docker implementation.

Printers are shared across Layer 3 using the BrowsePoll directive in the CUPS configuration file required (RHEL 6 and RHEL 7 have different files for this directive).

CUPS uses a discovery protocol allowing CUPS clients to share printers from the CUPS server showing as local printers.

Using the new Kubernetes CUPS implementation as a print server is done from a CUPS client polling for remote printers. Remote CUPS printers are shared over the Internet Printing Protocol (IPP), clients can poll for printers by updating the required configuration file as shown below


Update the /etc/cups/cupsd.conf file to add required directive BrowsePoll <Remote CUPS server> setting host and port combination, port 631 is implied.



Update the /etc/cups/cups-browsed.conf file to add required directive BrowsePoll <Remote CUPS server> setting host and port combination, port 631 is implied.


Adding printers

Adding printers to this CUPS server is done using a git repo. Updating the git repo will trigger a pull from the CUPS pod to add the printer to the CUPS server instance using the below command

#> /usr/sbin/lpadmin -p <printer> -E -v socket://<printer>:9100 -P /usr/share/cups/model/dummy.ppd

The same git repo is used as a source of printers required on in the pod, if a printer exists in the pod but not the git file, the printer is removed with the below command

/usr/sbin/lpadmin -x <printer>

Print queues are kept clean by deleting print jobs queued on the CUPS server using the simple Linux one-liner below, looking for processes older than 60 seconds owned by the lp user and deleting the process.

  local SEC=60
  ps -eo pid=,euser=,etimes= | while read pid user sec; do  if [[ "${sec}" -gt "${SEC}" ]] && [[ "${user}" == "lp" ]]; then echo "${pid}" | xargs kill;  fi; done


Whist adding persistence sounds like a really good thing for a print server, this has not been decided as a useful function for Reece. The main issue with persistence is that we do not really care about in flight print jobs, the users will just re-print. If the add print queues script can add all the print queues at startup quickly, we do not need persistence using a PVC.

Persistence is easy to create and maintain in Kubernetes, but at the moment this is not required for our CUPS implementation.


Deployment at Reece is handled by Helm. The objects to deploy are shown below

Side car pods

This CUPS implementation in Kubernetes is a bit tricky. We need to add and delete printers when a git file is changed and we need to send print jobs through a third party template engine to parse XML into PCL. These complexities are handled by the use of init containers and side car pods. This is out of scope for this post.

Locking down printer sharing

Using the /etc/cups/cupsd.conf configuration file, we can lock down CIDR addresses that are allowed to print through these print servers. Using the below stanza in the configuration file, we can limit clients to the 10.99 supernet.

<Location />
  Order allow,deny
  Allow 10.99

What print jobs go to the third party template engine

This is handled using the interface / filter file (/usr/lib/cups/filter/custom). This file has logic to parse custom printing jobs from plain text.

Custom printing filter for DocOrigin

Using a third party template engine for print jobs makes CUPS a little harder to wrangle. A custom filter and ppd is required to send print jobs to the third party template engine.

To enable XML printing through a third party, the below files are required:

  1. Custom convs file to match text/plain
  2. Dummy PPD file to run print jobs through filters
  3. Custom filter

Custom files and print job flow

Adding a print.convs file to the /etc/cups/ directory allows for MIME types to be handled by custom filters. The below simple custom convs file takes all text/plain print jobs being converted to application/octet-stream and sends it to the custom filter. The 10 is a cost, the lowest cost gets used out of all convs files.

text/plain	application/octet-stream	10	custom

A custom dummy PPD file is used to point printers to application/octet-stream as an output and is placed into the /usr/share/cups/model/ directory. This ppd is used when creating printers.

*PPD-Adobe: "4.3"
*% Dummy PPD
*FormatVersion: "4.3"
*FileVersion:   "1.1"
*LanguageVersion: English
*LanguageEncoding: ISOLatin1
*PCFileName:    "dummy.ppd"
*Manufacturer:  "Dummy"
*Product:       "(SNMPOFF)"
*cupsFilter:    "application/octet-stream 0 -"
*cupsSNMPSupplies: False
*ModelName:     "Dummy"
*ShortNickName: "Dummy"
*NickName:      "Dummy"

Finally we need the custom filter which is placed into the /usr/lib/cups/filter/ directory with the filename matching the convs lines last column (custom).

These files combine to pass all print jobs destined to a printer added with the dummy ppd to be output as application/octet-stream, as all of our print jobs present as the MIME type text/plain we use the convs file to sent the print jobs to the custom filter.