Ever since I was at Search Discovery I’d been trying to figure out a way to add basic authentication to mkdocs. This proved to be way more challenging than I thought it might be.

At one point I considered just signing up for ReadTheDocs or Gitbook, but I persisted. What I ended up with may be over-engineered, but it works.

What I tried

Asked ChatGPT what it would do

Recommended deploying via VM instance or Caddy (frontend proxy)

Googled around

Found an article that walked through deploying from GitLab CI

What worked

Building as a Cloud Run service, deploying an nginx frontend proxy as the “ingress” container (one that handles incoming requests), then the main mkdocs container as a “sidecar”

Cloud Run service

Cloud Run has several advantages over Compute Engine VM instances:

  1. Cheaper — because the site will get so little traffic, it makes sense to only spin it up when it’s needed. VM instances are always on, Cloud Run services are not
  2. Easier — rather than trying to build your own CI/CD pipeline from scratch, Cloud Run lets you compose

Suppose you could also use App Engine

Build docs container using Cloud Run CD to match the platform architecture and avoid “exec format error”

Containers

  1. An nginx frontend proxy serving on port 80, proxying traffic to/from port 8080
  2. The mkdocs container serving on port 8080

Secrets

You can’t edit nginx.conf directly but you can add an nginx.conf file as a secret using Secrets Manager and mount it as a volume

Deployment

  • Working YAML
    apiVersion: serving.knative.dev/v1
    kind: Service
    metadata:
      name: mkdocs-service
      namespace: '194919427391'
      selfLink: /apis/serving.knative.dev/v1/namespaces/153919444396/services/mkdocs-service
      uid: 393f453a-6a41-40ab-9814-8633d2950f00
      resourceVersion: AAYY9UUGwkY
      generation: 10
      creationTimestamp: '2024-05-21T10:20:53.007737Z'
      labels:
        cloud.googleapis.com/location: us-central1
      annotations:
        run.googleapis.com/client-name: cloud-console
        serving.knative.dev/creator: [email protected]
        serving.knative.dev/lastModifier: [email protected]
        run.googleapis.com/description: MkDocs site
        run.googleapis.com/operation-id: 4ede382b-2808-4f84-a35f-1004761f6bd7
        run.googleapis.com/ingress: all
        run.googleapis.com/ingress-status: all
    spec:
      template:
        metadata:
          labels:
            client.knative.dev/nonce: 0ec7f9fb-87aa-4a20-8623-6d9eed070849
            run.googleapis.com/startupProbeType: Default
          annotations:
            run.googleapis.com/client-name: cloud-console
            autoscaling.knative.dev/maxScale: '5'
            run.googleapis.com/startup-cpu-boost: 'true'
            run.googleapis.com/container-dependencies: '{"nginx":["mkdocs-site"]}'
        spec:
          containerConcurrency: 80
          timeoutSeconds: 300
          serviceAccountName: [email protected]
          containers:
          - name: nginx
            image: nginx
            ports:
            - name: http1
              containerPort: 80
            resources:
              limits:
                cpu: 500m
                memory: 256Mi
            volumeMounts:
            - name: nginx-conf-secret
              readOnly: true
              mountPath: /etc/nginx/conf.d/
            - name: nginx-htpasswd
              mountPath: /etc/nginx
            startupProbe:
              timeoutSeconds: 240
              periodSeconds: 240
              failureThreshold: 1
              tcpSocket:
                port: 80
          - name: mkdocs-site
            image: gcr.io/acme-explosives-423614/github.com/somedomain/client-data-layer-spec@sha256:425fa276d0fc9df49c9b727785a27ed3373952e51e5792b6a68dca821e85b289
            env:
            - name: PORT
              value: '8080'
            resources:
              limits:
                cpu: 1000m
                memory: 512Mi
            startupProbe:
              timeoutSeconds: 240
              periodSeconds: 240
              failureThreshold: 1
              tcpSocket:
                port: 8080
          volumes:
          - name: nginx-conf-secret
            secret:
              secretName: nginx_config
              items:
              - key: latest
                path: default.conf
          - name: nginx-htpasswd
            secret:
              secretName: nginx_htpasswd
              items:
              - key: latest
                path: .htpasswd
      traffic:
      - percent: 100
        latestRevision: true
    status:
      observedGeneration: 10
      conditions:
      - type: Ready
        status: 'True'
        lastTransitionTime: '2024-05-21T11:39:24.388934Z'
      - type: ConfigurationsReady
        status: 'True'
        lastTransitionTime: '2024-05-21T10:20:53.131621Z'
      - type: RoutesReady
        status: 'True'
        lastTransitionTime: '2024-05-21T11:39:24.347405Z'
      latestReadyRevisionName: mkdocs-service-00005-4nj
      latestCreatedRevisionName: mkdocs-service-00005-4nj
      traffic:
      - revisionName: mkdocs-service-00005-4nj
        percent: 100
        latestRevision: true
      url: https://mkdocs-service-vwuoluxl6a-uc.a.run.app
      address:
        url: https://mkdocs-service-vwuoluxl6a-uc.a.run.app
     

Deploy using CLI

gcloud run services replace service.yaml

Docs site must have Dockerfile. This worked:

FROM python:3.9-slim
 
# git required for mkdocs
RUN apt-get update && apt-get install -y git
 
# install mkdocs-material and plugins
RUN pip install mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin
 
COPY . .
 
EXPOSE 8080
 
CMD ["mkdocs","serve"]

Challenges

Permissions

Building the container

Cloud Run

Next steps

Auth using Google account

Right now using nginx basic auth. Should be fine for all users to share a password. Might be cool/more professional if they could log in via Google oauth using e.g. oauth2-proxy

Custom domain

https://console.cloud.google.com/run/domains?project=acme-explosives-423614 You can add a custom domain so you don’t have to hand off an ugly URL like https://mkdocs-service-vwuoluxl6a-uc.a.run.app/ to the client

Optimize build

Currently runs using mkdocs serve, which is intended for development (python server, includes live reloading, etc.) — maybe use mkdocs build in the dockerfile and then serve using nginx or caddy?