Minimal Docker for GeoDjango + PostGIS
Posted on 2023-12-03, by Racum.
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:
Version | Distro | GDAL options | Size |
---|---|---|---|
alpine-small-latest | Alpine | Basic | 71.5 MB |
alpine-normal-latest | Alpine | Complete | 345 MB |
ubuntu-small-latest | Ubuntu | Basic | 367 MB |
ubuntu-full-latest | Ubuntu | Complete | 1.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:
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.