Deploy Django on Red Hat OpenShift
Deploy Django on OpenShift: build with Source-to-Image or a Dockerfile, run as an arbitrary non-root UID, connect to PostgreSQL, expose the app with a Route and TLS edge termination, run migrations as a Job, and fix permission errors under restricted SCCs.
Problem statement
Your company standardizes on Red Hat OpenShift, and the Django image that runs fine on plain Kubernetes crashes there with permission errors, or you are asked to use OpenShift's own build system. OpenShift is Kubernetes with stricter defaults: containers run as a random high UID instead of the user in the Dockerfile, Routes replace Ingress for most teams, and Source-to-Image (S2I) can build Python apps straight from Git.
- Build with S2I (
oc new-app python~https://github.com/acme/shop.git) or with your Dockerfile using a BuildConfig. - Make the image work under an arbitrary UID: files owned by group 0 with group write where needed (
chgrp -R 0 /app && chmod -R g=u /app). - Store settings in a Secret and ConfigMap; connect to PostgreSQL from an operator, a template, or an external service.
- Expose with a Route using edge TLS termination, and set
SECURE_PROXY_SSL_HEADER. - Run
migrateas a Job before rollouts, and keep Gunicorn on a port above 1024.
Step-by-step solution
1. Build the image
With Source-to-Image, OpenShift's Python builder installs requirements.txt and runs Gunicorn automatically if it finds it, using APP_MODULE:
oc new-project shop
oc new-app python:3.12-ubi9~https://github.com/acme/shop.git \
--name=web -e APP_MODULE=config.wsgi:application -e DISABLE_COLLECTSTATIC=0
S2I runs collectstatic during the build unless disabled, and honors an app.sh or .s2i/bin/assemble for custom steps. If you already have a Dockerfile, use it instead: oc new-build --strategy docker --binary --name web and oc start-build web --from-dir=. --follow.
2. Make the image run as any UID
OpenShift's default restricted-v2 SCC assigns each project a UID range and runs containers with a random UID from it, plus group 0 (root group, without root privileges). An image that expects user app (UID 1000) and writes to /app/media fails with PermissionError. Prepare the image:
FROM python:3.12-slim
WORKDIR /app
COPY --chown=1001:0 . .
RUN pip install --no-cache-dir -r requirements.txt \
&& python manage.py collectstatic --noinput \
&& chgrp -R 0 /app && chmod -R g=u /app
USER 1001
EXPOSE 8000
CMD ["gunicorn", "config.wsgi", "--bind", "0.0.0.0:8000", "--workers", "3"]
Group 0 ownership with group permissions equal to user permissions lets whatever UID OpenShift assigns read and write the same files.
3. Configure settings and the database
oc create secret generic django-env \
--from-literal=SECRET_KEY=... \
--from-literal=DATABASE_URL=postgres://shop:...@shop-db:5432/shop
oc set env deployment/web --from=secret/django-env
For PostgreSQL, use a database operator from OperatorHub (Crunchy Postgres for Kubernetes, CloudNativePG), a managed database outside the cluster, or a template-based instance for non-critical environments.
4. Expose with a Route
oc create route edge web --service=web --hostname=shop.example.com --insecure-policy=Redirect
The router terminates TLS (with the cluster's wildcard certificate or one you provide) and forwards plain HTTP to the pod. Tell Django:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
ALLOWED_HOSTS = ["shop.example.com"]
5. Migrations, probes, and media
Run migrations as a Job using the newly built image before the Deployment picks it up, for example from a pipeline (OpenShift Pipelines or external CI). Add readiness and liveness probes with a Host header, as on any Kubernetes cluster. Put media in object storage (S3-compatible, such as OpenShift Data Foundation's object gateway) rather than in the container.
Explanation
OpenShift enforces Security Context Constraints that prevent containers from running as root and assign UIDs per project, which limits damage if a container is compromised. Because the UID is unknown at build time, images rely on the root group for file access, a convention used by all Red Hat images. Routes are OpenShift's original ingress mechanism and integrate with the cluster router and certificates. Beyond these differences, Deployments, Services, Jobs, and probes behave as on any Kubernetes cluster.
Edge cases / notes
- UBI images. Red Hat's Universal Base Images (
ubi9/python-312) are built for arbitrary UIDs and supported on OpenShift. - Ingress objects. OpenShift converts standard Ingress resources into Routes, so manifests written for Kubernetes usually still work.
- Build resources. S2I builds run in the cluster and count against project quotas; large dependency installs may need higher build limits.
Next steps
- Compare with standard Kubernetes in Deploy Django on Kubernetes.
- Build non-root images with Run Django Docker Containers as Non-Root.
- Review cluster security with the Django Kubernetes Security Checklist.
FAQ
Why does my Django container fail with permission errors on OpenShift?
OpenShift runs it with a random UID that does not own the image files. Give group 0 the same permissions as the owner.
Should I use S2I or a Dockerfile?
S2I is convenient for straightforward apps. A Dockerfile gives full control and the same image can run elsewhere.
Can I use Ingress instead of Routes?
Yes. OpenShift translates Ingress objects into Routes.
Do I need the anyuid SCC?
No. Making the image work with an arbitrary UID is safer and is what OpenShift expects.