OctoPrint and Kubernetes

I wanted to get OctoPrint running in my Kubernetes cluster to manage my 3D printer. Here is one way to accomplish that.

I wanted to get OctoPrint running in my Kubernetes cluster to manage my 3D printer. Here is one way to accomplish that.

I am all in on Kubernetes, my entire network runs inside of it. I needed to upgrade my OctoPi installation and decided to install OctoPrint in my Kubernetes cluster instead. To do this, I took my old OctoPi Raspberry Pi, which was a Pi4 with 8 gigs of memory, and turned it into a Kubernetes worker node and slapped a taint and a label on it. I do this because I needed OctoPrint to run on the same Pi that the printer is connected to. There are 4 manifests, the deployment, the service, the ingress and the persistent volume claim. I will be covering those 4 manifests in this post.

Before we do anything with the manifests, we need to taint and label your worker node that your printer is connected to. We do this so we can make sure there are enough resources available to run OctoPrint (the taint) and that OctoPrint will always run on the correct worker node (the label). OctoPrint does not consume much memory at all so you may not necessarily need to taint your node. I'll leave that decision up to you.

Lets taint and label your node now. I am using 3dprinter as the taint and label, but you can do whatever you want, just modify the deployment.yaml manifest accordingly.

kubectl taint nodes <nodename> dedicated=3dprinter:NoSchedule
kubectl label nodes <nodename> dedicated=3dprinter

Now that your worker node has been isolated, we will set up the OctoPrint installation.

The persistent volume claim

First, we will cover the persistent volume claim. We need a persistent volume so that your plugins, recent print files, configuration, etc. gets saved across pod restarts. It is a simple PVC that will only request 10 gigs of space.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: octoprint
  labels:
    app.kubernetes.io/name: octoprint
    app.kubernetes.io/instance: octoprint
    app.kubernetes.io/component: octoprint
    app.kubernetes.io/part-of: octoprint
spec:
  accessModes:
    - ReadWriteMany
  volumeMode: Filesystem
  resources:
    requests:
      storage: 10Gi

The deployment

Next, we will cover the deployment. This is where we create the pod, mount the volume and bring in the necessary devices like the serial port. If you need to bring in different devices take a look at how I did the volumeMount for ttyACM0 and adjust accordingly.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: octoprint
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: octoprint
      app.kubernetes.io/part-of: octoprint
  replicas: 1
  revisionHistoryLimit: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: octoprint
        app.kubernetes.io/instance: octoprint
        app.kubernetes.io/component: octoprint
        app.kubernetes.io/part-of: octoprint
    spec:
      nodeSelector:
        dedicated: 3dprinter
      tolerations:
        - key: "dedicated"
          value: "3dprinter"
          effect: "NoSchedule"
      containers:
      - name: octoprint
        image: octoprint/octoprint:1.7.2
        env:
          - name: ENABLE_MJPG_STREAMER
            value: 'false'
        securityContext:
          privileged: true
        resources:
          requests:
            memory: 2000Mi
            cpu: 1000m
          limits:
            memory: 3500Mi
            cpu: 2000m
        volumeMounts:
          - mountPath: /octoprint
            name: octoprint
            readOnly: false
          - mountPath: /dev/ttyACM0
            name: ttyacm0
            readOnly: false
          - mountPath: /dev/video0
            name: video0
            readOnly: false
      volumes:
      - name: octoprint
        persistentVolumeClaim:
          claimName: octoprint
      - name: ttyacm0
        hostPath:
          path: /dev/ttyACM0
      - name: video0
        hostPath:
          path: /dev/video0

When I get my camera plugged in and going, I will change the ENABLE_MJPG_STREAMER to true. Since I do not have the camera mounted or plugged in, I have that off.

The service

The service manifest is small and simple. It forwards port 80 on to the pod. You may notice the name is octoprint-svc instead of just octoprint. When it was octoprint I was getting errors about invalid number format for the port. Turns out, octoprint_port is the environment variable that tells OctoPrint what port to listen on. Kubernetes automatically brings in the services in the namespace as environment variables and sets them values pointing to the service, thus, breaking OctoPrint and causing a lot of head scratching.

apiVersion: v1
kind: Service
metadata:
# Can't use octoprint here because it sets OCTOPRINT_PORT to the ip/port of the service inside of the container
# That environment variable is a config environment variable that tells octoprint what port to start on.
  name: octoprint-svc
  labels:
    app.kubernetes.io/name: octoprint
    app.kubernetes.io/instance: octoprint
    app.kubernetes.io/component: octoprint
    app.kubernetes.io/part-of: octoprint
spec:
  selector:
    app.kubernetes.io/name: octoprint
    app.kubernetes.io/instance: octoprint
    app.kubernetes.io/component: octoprint
    app.kubernetes.io/part-of: octoprint
  ports:
    - protocol: TCP
      name: http
      port: 80
      targetPort: 80
  type: ClusterIP

The ingress

The ingress manifest has a couple of unique properties. First thing to watch out for is if you use a custom SSL cert that is not publicly trusted, you will want to make sure that your OctoPrint ingress is not doing SSL redirection. I only tested with Prusa Slicer and it looked like it does some validation on the SSL certificate. The second thing to watch out for is the body size of the requests can be quite large when uploading print gcode files. You will want to make sure that is not getting blocked either. I use NGINX for my ingress controller so my annotations are geared towards that. If you use something else you will need to figure those pieces out on your own.

Be sure to change the host name in the following manifest to whatever you want it to be.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: octoprint-c-lan
  annotations:
    nginx.org/redirect-to-https: "False"
    ingress.kubernetes.io/ssl-redirect: "False"
    nginx.ingress.kubernetes.io/ssl-redirect: "False"
    nginx.ingress.kubernetes.io/proxy-body-size: 800m
  labels:
    app.kubernetes.io/name: octoprint
    app.kubernetes.io/instance: octoprint
    app.kubernetes.io/component: octoprint
    app.kubernetes.io/part-of: octoprint
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - octoprint.example.com
  rules:
  - host: octoprint.example.com
    http:
      paths:
      - path: /
        pathType: ImplementationSpecific
        backend:
          service:
            name: octoprint-svc
            port:
              number: 80

Conclusion

This was a fun little project and completely overkill for what it is doing. But. I am a nerd, so, I did it.

Leave a comment if you have any comments or questions.