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.

Django on OpenShiftBuildS2I or DockerfileRandom UIDrestricted SCCDeploymentGunicorn podsRoute + TLSedge terminationOpenShift runs images as an arbitrary UID, so files must be group-writable by GID 0.
  1. Build with S2I (oc new-app python~https://github.com/acme/shop.git) or with your Dockerfile using a BuildConfig.
  2. 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).
  3. Store settings in a Secret and ConfigMap; connect to PostgreSQL from an operator, a template, or an external service.
  4. Expose with a Route using edge TLS termination, and set SECURE_PROXY_SSL_HEADER.
  5. Run migrate as 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.

running under a random UIDFixed user app (1000)✕files owned by 1000 only✕random UID cannot write✕PermissionError at startGroup 0, g=u permissions✓any UID in group 0 works✓no root privileges✓runs on Kubernetes tooGroup-based permissions make one image work on both platformsdjango-deployment.com
Avoid writing to the image at runtime; use volumes or object storage for media.

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.

symptom and fixSymptomCause and fixPermissionError writing filesrandom UID; chgrp 0 and chmod g=ubind to port 80 deniednon-root; listen on 8000 or 8080redirect loop over HTTPSedge TLS; set SECURE_PROXY_SSL_HEADERS2I does not start Gunicornset APP_MODULE or app.shMost issues come from OpenShift’s non-root defaultsdjango-deployment.com
Never ask for the anyuid SCC just to make an image start.

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

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.

Keep going