NC

Static files with Nginx, Docker & Kubernetes

I wanted a way to serve up a set of .well-known paths, as easily as I could get away with, but keep it all alongside some other Kubernetes manifests.

I hoped it’d be something I could trick the Nginx Ingress controller into doing (as that already exists), perhaps with just a ConfigMap but it’s not really designed to do that. I didn’t think it should be necessary to build a custom container image either. But we could do it with a Deployment and an Nginx container. There’s a bit to it, but perhaps it’s helpful for others.

I’m going to use Matrix’s .well-known paths here, but the same principle could apply to anything static.

A quick experiment with Docker

To start with, we need something to serve:

# .well-known/matrix/server
{
  "m.server": "matrix.nickcharlton.net:443"
}

We can then serve it by mounting the current directory in the right place, using the Nginx Docker image:

docker run --rm -p 8080:80 -v .:/usr/share/nginx/html:ro nginx

And test it with curl:

$ curl http://localhost:8080/.well-known/matrix/server
{
  "m.server": "matrix.nickcharlton.net:443"
}

That works quite nicely, which is just enough experimenting to turn it into Kubernetes manifests.

Running on Kubernetes

To start with, a ConfigMap to hold the data:

# configmap-well-known.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: well-known
  namespace: default
  labels:
    app.kubernetes.io/name: well-known
data:
  server: |
    {
      "m.server": "matrix.nickcharlton.net:443"
    }

Then a Deployment to run Nginx:

# deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: well-known
  namespace: default
  labels:
    app.kubernetes.io/name: well-known
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: well-known
  template:
    metadata:
      labels:
        app.kubernetes.io/name: well-known
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          volumeMounts:
            - name: data
              mountPath: /usr/share/nginx/html/.well-known/matrix/server
              subPath: server
      volumes:
        - name: data
          configMap:
            name: well-known

Finally, a Service and Ingress:

# service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: well-known
  namespace: default
  labels:
    app.kubernetes.io/name: well-known
spec:
  # type: LoadBalancer
  clusterIP: None
  ports:
    - name: http
      port: 80
      targetPort: http
  selector:
    app.kubernetes.io/name: well-known
# ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: well-known
spec:
  ingressClassName: nginx
  rules:
    - host: nickcharlton.net
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: well-known
                port:
                  name: http

I use kube-vip on my clusters, so I could use a LoadBalancer service here to get an IP outside of the cluster and use that (a pattern you can use elsewhere too). But, it’s also possible to use a headless service with an Ingress, which is what’s done here.

We can test via the Ingress directly by forwarding a port:

$ kubectl port-forward --namespace=ingress-nginx service/ingress-nginx-controller 8080:80
$ curl --resolve nickcharlton.net:8080:127.0.0.1 http://nickcharlton.net:8080/.well-known/matrix/server
{
  "m.server": "matrix.nickcharlton.net:443"
}

Or, if we can access outside the cluster (assuming the domain resolves):

$ curl http://nickcharlton.net:8080/.well-known/matrix/server
{
  "m.server": "matrix.nickcharlton.net:443"
}