Other Blog Posts

Running UMN MapServer on Kubernetes

2024-09-15

UMN MapServer is a well known Open Source platform for publishing spatial data and interactive mapping applications to the web. Going back to the mid-1990's it is still widely used and of course you can run it in a container on Kubernetes.

Because there are binary packages for many Linux distributions, creating a MapServer container image is really simple. We build our image as a FastCGI service using spawn-fcgi.

Using spawn-fcgi to run a standalone MapServer process

If we start the mapserv binary using spawn-fcgi from a login shell on any Linux system like this:

spawn-fcgi -a 0.0.0.0 -p 9990 -n -- /usr/bin/mapserv

and then look at the process tree with ps -ef --forest we will find the following lines in the output:

root        1631       1  0 07:13 tty1     00:00:00 /bin/login -p --
thomas     54480    1631  0 14:09 tty1     00:00:00  \_ -bash
thomas     55007   54480  0 14:10 tty1     00:00:00      \_ /usr/bin/mapserv

We can see one process of our mapserv executable. The -n switch will prevent spawn-fcgi from forking a mapserv process and instead run in foreground. This is exactly what we want to have in our container.

The MapServer Dockerfile

Let's put this in a Dockerfile:

FROM alpine:latest
RUN apk upgrade -U
RUN apk add -U --update --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing mapserver
RUN apk add -U --update spawn-fcgi
RUN mkdir /opt/mapserver
RUN chown 1000:1000 /opt/mapserver
USER 1000
ENV MAPSERVER_CONFIG_FILE="/opt/mapserver/mapserver.conf"
CMD ["/usr/bin/spawn-fcgi", "-a", "0.0.0.0", "-p", "9990", "-n", "--", "/usr/bin/mapserv"]

The result is a simply structured and lightweight container image that solves one concern. This allows us to handle horizontal scaling and also reverse proxy configuration independently. Arguably putting a reverse proxy in the container image would slightly simplify the communication given that HTTP is more commonly available than FastCGI. However any increase in load will typically affect MapServer most. Thus we will need to scale up MapServer more quickly than the reverse proxy, and we avoid some overhead by keeping the reverse proxy software outside of our container image.

We're using Alpine here, but a lot of Linux distros have Mapserver packages. By using a MapServer package from the distribution we rely on the distro's package maintainers to provide the latest security patches.

Where to put mapfiles and geodata

In the Dockerfile we created an /opt/mapserver mount point where we will provide application specific MapServer configuration (mapserver.conf and mapfiles) and optional geodata, e.g. shapefiles.

Assuming we pushed our MapServer image to a container registry and created a persistent volume claim containing the map data we can use the following Kubernetes deployment:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mapserver
spec:
  selector:
    matchLabels:
      app: mapserver
  replicas: 3
  template:
    metadata:
      labels:
        app: mapserver
    spec:
      containers:
      - name: mapserver
        image: <your.container.registry>/mapserver:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 9990
        volumeMounts:
        - mountPath: /opt/mapserver
          name: mapdata
      volumes:
      - name: mapdata
        persistentVolumeClaim:
          claimName: mapdata-pvc
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: mapserver
  name: mapserver
spec:
  ports:
  - port: 9990
    protocol: TCP
    targetPort: 9990
  selector:
    app: mapserver
  type: ClusterIP

In the deployment we scale MapServer to 3 replicas. In addition we create a service for our pod for communicating with the reverse proxy.

Reverse Proxy Configuration for the MapServer FastCGI service

Next we configure a reverse proxy that communicates with our container using the FastCGI protocol. In particular we look at how to configure Nginx and Apache.

Nginx

For Nginx we can define a location:

  location /map/ {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass   mapserver:9990;
    fastcgi_param  SCRIPT_FILENAME  /usr/lib/cgi-bin/mapserv$fastcgi_script_name;
  }

Apache

For Apache, you will need to enable mod_proxy_fcgi and then add a configuration line like this:

ProxyPass "/app/" "fcgi://mapserver:9990/";

Traefik

At the time of writing this post a FastCGI implementation for Traefik is not yet available. Currently the progress is tracked here.