Minimal Docker for GeoDjango + PostGIS


Posted on 2023-12-03, by Racum. Tags: GIS Python Django PostgresSQL Docker

If you want to dockerize a GeoDjango project, the most common method is to use an oficial Python image as its base, and install all GIS dependencies over it, being GDAL the heaviest one, making your final image easily reach the mark of the multi-megabytes. But GDAL is very modular, and most GeoDjango projects can work fine with its basic compilation.

This article shows how to flip that dependency, and use the official minimal GDAL images as a basis, adding the Python/Django structure over it. Also, how to pick a leaner PostGIS image to save even more space on your development environment.

Choosing an Image

For the Application Server

The GDAL Docker images are not hosted on the main Docker Hub, but on GitHub Packages, under the tag ghcr.io/osgeo/gdal. Here is the list of variations available:

VersionDistroGDAL optionsSize
alpine-small-latestAlpineBasic71.5 MB
alpine-normal-latestAlpineComplete345 MB
ubuntu-small-latestUbuntuBasic367 MB
ubuntu-full-latestUbuntuComplete1.92 GB

The basic options contain a GDAL installation with basic raster and vector drivers, and basic PROJ projection definitions. This is enough for most of the functionality needed for GeoDjango; if you are only planning to use the geometry fields/operations, and you just need the World Geodetic grid (EPSG:4326/WGS84), you should consider this version.

Between Alpine and Ubuntu, it depends on your needs, specially if you have extra dependencies. My advice is to start with Alpine, if you have issues compiling or running your extra packages, consider Ubuntu than.

For the Database Server

There are many PostGIS images available on Docker Hub, unfortunately most of them are x86-64 only! If you need work with multiple architectures, I recommend the the image nickblah/postgis, it is not very small (636MB) but allows you to jump between x86 and ARM platforms without issues.

If x86-64 only is not an issue for your project, consider the Alpine variation of the officinal image postgis/postgis:9.5-alpine (430MB).

Also, be careful: the popular image mdillon/postgis, although very small (114MB on Alpine), is not being maintained, with the last version released 5 years before the writhing of this article. If you try this later on Docker Hub, and there is no new versions for it, please avoid it.

Setup Docker

Use the chosen GDAL image as the basis of your Dockerfile, in case of Alpine, use apk to install Python, the PostgreSQL client and GEOS (the libraries are already in the base image, but GeoDjango requires the binary geosop, available on this package):

FROM ghcr.io/osgeo/gdal:alpine-small-latest

ENV PYTHONUNBUFFERED 1
RUN mkdir /app
WORKDIR /app

RUN apk add --no-cache python3 py3-pip postgresql-client geos

COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt

COPY . /app

Add the minimal Django requirements on requirements.txt:

django
tzdata
psycopg2-binary

On your docker-compose.yaml, set the application service to build from the local Dockerfile, and the database service to use your chosen PostGIS image:

version: '3.9'

services:
  web:
    build:
      context: .
    volumes:
      - .:/app:cached  # For development only.
    ports:
      - '8001:8001'
    depends_on:
      - db
  db:
    image: nickblah/postgis
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust  # For development only.

Extras

It is very common for GIS projects to also require extra packages for geometry handling, projection transformations and map presentation. Some popular packages like Shapely, PyPROJ and Fiona require compilation; if you need them you have to add the following Alpine packages:

  • build-base: build tools.
  • python3-dev: Python headers.
  • geos-dev: GEOS headers.

Of course, that can drastically increase the size of your image, defusing the goal of this article; in that case please refer to Docker Multi-stage builds, using the packages above just for the builder

Setup Django

Make sure django.contrib.gis and your local app are listed on INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'django.contrib.gis',
    'geoapp',
]

Connect the database service:

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'postgres',
        'HOST': 'db',
        'POST': '5432',
        'USER': 'postgres',
        'PASSWORD': 'any',
    }
}

You can test the connection by running dbshell:

/app # ./manage.py dbshell
psql (15.5, server 16.1 (Debian 16.1-1.pgdg120+1))
Type "help" for help.

postgres=#

Geometry Fields on Models

Please refer to Spatial Field Types for a list of available model fields. If you are not sure about what to use, just pick the GeometryField, this can hold any geometry type.

In the example below I’m using a MultiPolygonField to hold multiple polygons of countries:

Contents of models.py:

from django.contrib.gis.db import models

class Country(models.Model):
    iso_code = models.CharField(max_length=2, primary_key=True)
    name = models.CharField(max_length=100)
    geometry = models.MultiPolygonField()

    class Meta:
        verbose_name_plural = 'Countries'

    def __str__(self):
        return self.name

Run makemigrations and migrate to create the database table for this model, and check it with dbshell:

postgres=# \d geoapp_country
                      Table "public.geoapp_country"
  Column  |            Type             | Collation | Nullable | Default
----------+-----------------------------+-----------+----------+---------
 iso_code | character varying(2)        |           | not null |
 name     | character varying(100)      |           | not null |
 geometry | geometry(MultiPolygon,4326) |           | not null |
Indexes:
    "geoapp_country_pkey" PRIMARY KEY, btree (iso_code)
    "geoapp_country_geometry_281cf55f_id" gist (geometry)
    "geoapp_country_iso_code_9a7637c2_like" btree (iso_code varchar_pattern_ops)

Notice that Django already created a GIST index for the geometry field.

Admin Support

Your project is already GIS-compliant! …but if you want to visually check your geometry data before having your frontend ready, you can see those fields via special widgets on Django Admin:

Here is a very basic admin.py that shows the Country model we defined above:

from django.contrib import admin
from .models import Country

admin.site.register(Country)

If this is your first Admin access, don’t forget to create a super-user via createsuperuser, and than run your web server locally:

$ ./manage.py runserver 0.0.0.0:8001

This is how the Country admin form should look like:

Screenshot of the Django Admin showing a map for the geometry field.

Conclusion

These are my Docker images after optimization:

$ docker images
REPOSITORY          TAG                  IMAGE ID      CREATED        SIZE
(project image)     latest               f9391bb285dc  2 minutes ago  189MB
ghcr.io/osgeo/gdal  alpine-small-latest  2379db429710  2 weeks ago    71.5MB
nickblah/postgis    latest               dd163015bd77  36 hours ago   636MB

There are many ways to go deeper, like hand-crafting your own base images with only the compilation flags you need; or make a chain of multi-stage builds to share a small GDAL between both the Pyhon application and the PostGIS images. The complexity of that exceeds the scope of this article, but I hope I could provide the first steps for that.