---
title: "Deploy DreamFactory on OpenShift | DreamFactory Docs"
source: "https://docs.dreamfactory.com/getting-started/installing-dreamfactory/openshift-installation"
canonical_url: "https://docs.dreamfactory.com/getting-started/installing-dreamfactory/openshift-installation"
converted_at: "2026-09-25T00:36:29.246Z"
format: "markdown"
converted_by: "html-to-md-ai"
---
OpenShift runs pods under a stricter policy than stock Kubernetes. Under the default `restricted-v2` Security Context Constraint (SCC):

- the container runs as a **random high UID**, not root and not the UID in your Dockerfile

- that UID is always a member of **group 0** (root group)

- **no privileged ports** (below 1024)

- **no privilege escalation** and no `chown` at runtime

The standard `dreamfactorysoftware/df-docker` image runs nginx and PHP-FPM as root on port 80, so it is refused as-is. This guide gives you a derived image that satisfies the SCC, plus the Helm values and OpenShift objects to run it.

## What changes from the Docker image[​](#what-changes-from-the-docker-image)

Standard imageOpenShift imagenginx listens on 80listens on **8080**nginx pid and temp paths under `/run` and `/var/lib/nginx`under **`/tmp`**`user www-data;` in nginx.confremoved (non-root nginx cannot switch user)PHP-FPM pool `user = www-data`, socket in `/run/php`user/group removed, socket at **`/tmp/php-fpm.sock`**entrypoint runs `chown -R www-data` on storageremoved; ownership fixed at **build time** with `chgrp 0` + `chmod g=u`entrypoint starts PHP-FPM and cron via `service`PHP-FPM started directly; **cron disabled** (use a CronJob for df-scheduler)`USER root` (implicit)`USER 1001` (any non-root UID; OpenShift overrides it anyway)

Nothing about DreamFactory itself changes: same code, same system database, same license files, same admin UI.

## 1. Build the image[​](#1-build-the-image)

Start from a `df-docker` checkout with your three commercial composer files (`composer.json`, `composer.json-dist`, `composer.lock`) in place, as described in [Activating your license](/getting-started/installing-dreamfactory/activating-your-license).

Add this `Dockerfile.openshift` next to the standard `Dockerfile` (also in the [df-docker repo](https://github.com/dreamfactorysoftware/df-docker)):

```
# DreamFactory on OpenShift: non-root image and runtime configuration.## Extends the standard df-docker image so it runs under OpenShift's# `restricted` / `restricted-v2` SCC: arbitrary non-root UID, GID 0,# no privileged ports, no chown at runtime.## Build (from a df-docker checkout that already has your commercial# composer files in place, after building the standard image):#   docker build -f Dockerfile.openshift --build-arg BASE=df-docker-web:latest -t dreamfactory:7.7-openshift .#ARG BASE=dreamfactorysoftware/df-docker:latestFROM ${BASE}USER root# PHP version is discovered at build time; the 7.7 entrypoint defaults to 8.3 if unset.RUN PHPV=$(ls /etc/php | sort -V | tail -1) && echo "$PHPV" > /etc/php-version && echo "PHP $PHPV"ENV PHP_VERSION=8.5# 1. nginx: unprivileged port, writable pid/temp paths, no user directiveRUN sed -i 's/listen 80;/listen 8080;/' /etc/nginx/sites-available/dreamfactory.conf \ && sed -i 's|^pid /run/nginx.pid;|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf \ && sed -i 's/^user www-data;/# user directive removed: non-root/' /etc/nginx/nginx.conf \ && sed -i '/^http {/a \    client_body_temp_path /tmp/nginx-client-body;\n    proxy_temp_path /tmp/nginx-proxy;\n    fastcgi_temp_path /tmp/nginx-fastcgi;\n    uwsgi_temp_path /tmp/nginx-uwsgi;\n    scgi_temp_path /tmp/nginx-scgi;' /etc/nginx/nginx.conf# 2. php-fpm: cannot switch user when not root, so drop user/group and use a writable socket pathRUN PHPV=$(cat /etc/php-version) \ && sed -i 's/^user = www-data/; user = (non-root)/; s/^group = www-data/; group = (non-root)/' /etc/php/$PHPV/fpm/pool.d/www.conf \ && sed -i "s|^listen = /run/php/php$PHPV-fpm.sock|listen = /var/tmp/php-fpm.sock|" /etc/php/$PHPV/fpm/pool.d/www.conf \ && sed -i 's/^;\?listen.owner = .*/;listen.owner/; s/^;\?listen.group = .*/;listen.group/' /etc/php/$PHPV/fpm/pool.d/www.conf \ && sed -i 's|^pid = .*|pid = /tmp/php-fpm.pid|; s|^error_log = .*|error_log = /proc/self/fd/2|' /etc/php/$PHPV/fpm/php-fpm.conf \ && sed -i "s|unix:/var/run/php/php[^;]*-fpm.sock|unix:/var/tmp/php-fpm.sock|g; s|unix:/run/php/php[^;]*-fpm.sock|unix:/var/tmp/php-fpm.sock|g" /etc/nginx/sites-available/dreamfactory.conf \ && grep -q "unix:/var/tmp/php-fpm.sock" /etc/nginx/sites-available/dreamfactory.conf# 3. Group-0 ownership on everything written at runtime. OpenShift assigns a random UID#    that is always a member of GID 0, so g=u is the whole trick.RUN for d in /opt/dreamfactory/storage /opt/dreamfactory/bootstrap/cache /opt/dreamfactory/.env /opt/dreamfactory/public \             /var/lib/nginx /var/log/nginx /etc/nginx /etc/php /run /var/spool/cron /etc/cron.d /etc/ssmtp /var/tmp; do \      [ -e "$d" ] && chgrp -R 0 "$d" && chmod -R g=u "$d"; done; \    chgrp 0 /opt/dreamfactory && chmod g=u /opt/dreamfactory; \    touch /opt/dreamfactory/storage/logs/dreamfactory.log \ && chgrp 0 /opt/dreamfactory/storage/logs/dreamfactory.log && chmod g=u /opt/dreamfactory/storage/logs/dreamfactory.log# 4. Entrypoint: no runtime chown (fails as non-root), no `service` (needs root/init).#    Start php-fpm directly, skip cron (run df-scheduler as a CronJob instead).RUN sed -i 's/^\(\s*\)chown -R www-data:www-data .*$/\1: # chown removed for OpenShift/' /docker-entrypoint.sh \ && sed -i 's|^service "php${PHP_VERSION}-fpm" start|/usr/sbin/php-fpm${PHP_VERSION} --nodaemonize --fpm-config /etc/php/${PHP_VERSION}/fpm/php-fpm.conf \&|' /docker-entrypoint.sh \ && sed -i 's|^service "\$PHP_FPM_SERVICE" start|/usr/sbin/php-fpm${PHP_VERSION} --nodaemonize --fpm-config /etc/php/${PHP_VERSION}/fpm/php-fpm.conf \&|' /docker-entrypoint.sh \ && sed -i 's|^service cron start.*|: # cron disabled under non-root; run df-scheduler as a CronJob instead|' /docker-entrypoint.sh \ && sed -i 's|^\(\s*\)runuser -u www-data -- "\$@"|\1if [ "$(id -u)" = "0" ]; then runuser -u www-data -- "$@"; else "$@"; fi|' /docker-entrypoint.sh \ && sed -i 's|^\(\s*\)su -s /bin/bash www-data -c "\$(printf .%q . "\$@")"|\1"$@"|' /docker-entrypoint.sh \ && grep -qE "php-fpm\S* --nodaemonize" /docker-entrypoint.sh \ && grep -q 'id -u' /docker-entrypoint.shEXPOSE 8080USER 1001
```

Build and push to a registry your cluster can pull from (the internal OpenShift registry, Quay, or your own):

```
docker build -f Dockerfile.openshift --build-arg BASE=df-docker-web:latest -t <registry>/dreamfactory:7.7-openshift .docker push <registry>/dreamfactory:7.7-openshift
```

Test on plain Docker firstYou can prove the non-root part without a cluster. This simulates what OpenShift does:

```
docker run --rm -u 123456:0 -p 8080:8080 \  -e APP_KEY=base64:$(openssl rand -base64 32) \  -e DB_CONNECTION=mysql -e DB_HOST=<mysql> -e DB_DATABASE=dreamfactory -e DB_USERNAME=<u> -e DB_PASSWORD=<p> \  <registry>/dreamfactory:7.7-openshift
```

If the admin UI comes up at `http://localhost:8080` under UID 123456, the image is SCC-ready. `test-openshift.sh` in the df-docker repo does this end to end with a throwaway MySQL.

## 2. Deploy with Helm[​](#2-deploy-with-helm)

Use the [official Helm chart](https://github.com/dreamfactorysoftware/df-helm) with these overrides. Save as `values-openshift.yaml`:

```
dreamfactory:  image:    repository: <registry>/dreamfactory    tag: 7.7-openshift  service:    port: 8080    targetPort: 8080  securityContext:    runAsNonRoot: true    allowPrivilegeEscalation: false    capabilities:      drop: ["ALL"]    seccompProfile:      type: RuntimeDefault  # Do NOT set runAsUser or fsGroup. OpenShift assigns both from the namespace range.  env:    APP_KEY: "base64:<generate once with: openssl rand -base64 32>"    DF_LICENSE_KEY: "<your license key>"    DB_CONNECTION: mysql    DB_HOST: <your-mysql-host>    DB_PORT: "3306"    DB_DATABASE: dreamfactory    DB_USERNAME: dreamfactory    DB_PASSWORD: <from a Secret>    REDIS_HOST: <your-redis-host>    CACHE_DRIVER: redis    SESSION_DRIVER: redis# The chart's bundled MySQL and Redis also run as root. Disable them and# bring your own (an OpenShift-provided MySQL, Redis Operator, or external).mysql:  enabled: falseredis:  enabled: false# Ingress is not used on OpenShift; a Route is created in step 3.ingress:  enabled: false
```

APP_KEYSet `APP_KEY` explicitly and keep it in a Secret. On the Docker image the entrypoint generates one on first boot, but with several replicas each pod would generate its own and the encrypted credentials in the system database would not decrypt ("The MAC is invalid"). One key, set once, shared by every replica.

Install:

```
oc new-project dreamfactoryhelm install dreamfactory ./df-helm -f values-openshift.yaml -n dreamfactoryoc get pods -n dreamfactory -w
```

If the chart version you have does not expose a `securityContext` value, add it directly to `templates/dreamfactory-deployment.yaml` under the container spec, or apply a `PodSecurityContext` with `oc patch`.

## 3. Expose it with a Route[​](#3-expose-it-with-a-route)

OpenShift uses Routes instead of Ingress. Edge TLS termination at the router is the simplest and keeps certificates out of the container:

```
apiVersion: route.openshift.io/v1kind: Routemetadata:  name: dreamfactory  namespace: dreamfactoryspec:  host: dreamfactory.apps.<your-cluster-domain>  to:    kind: Service    name: dreamfactory  port:    targetPort: 8080  tls:    termination: edge    insecureEdgeTerminationPolicy: Redirect
```

```
oc apply -f route.yamloc get route dreamfactory -n dreamfactory
```

Because TLS terminates at the router, set `HTTPS_HEADER=on` in the DreamFactory environment (the entrypoint reads it) so generated links and CORS use `https`. See [CORS and SSL](/system-settings/config/cors-ssl).

## 4. First admin user and license check[​](#4-first-admin-user-and-license-check)

Browse to the Route host and create the first admin, exactly as in the [Docker guide](/getting-started/installing-dreamfactory/docker-installation). Then confirm the license took:

```
oc exec deploy/dreamfactory -n dreamfactory -- php artisan df:env 2>/dev/null | grep -i license
```

or in the admin UI under System Settings, the license level should read your tier, not `OPEN SOURCE`.

## 5. Scheduler (df-scheduler)[​](#5-scheduler-df-scheduler)

The standard image runs cron inside the container for the scheduler service. Cron needs root, so the OpenShift image disables it. Run the scheduler as a Kubernetes CronJob instead:

```
apiVersion: batch/v1kind: CronJobmetadata:  name: dreamfactory-scheduler  namespace: dreamfactoryspec:  schedule: "* * * * *"  concurrencyPolicy: Forbid  jobTemplate:    spec:      template:        spec:          restartPolicy: Never          containers:            - name: scheduler              image: <registry>/dreamfactory:7.7-openshift              command: ["php", "/opt/dreamfactory/artisan", "schedule:run"]              envFrom:                - secretRef:                    name: dreamfactory-env
```

Skip this if you do not use scheduled tasks.

## 6. Scaling[​](#6-scaling)

The DreamFactory container is stateless. The system database and Redis hold all shared state, so:

```
oc scale deploy/dreamfactory --replicas=3 -n dreamfactory
```

or add a HorizontalPodAutoscaler on CPU. Nothing else changes.

## Troubleshooting[​](#troubleshooting)

SymptomCauseFixPod `CrashLoopBackOff`, log says `nginx: [emerg] bind() to 0.0.0.0:80 failed (13: Permission denied)`still using the standard image or `listen 80`use the OpenShift image; check `listen 8080``nginx: [emerg] open() "/run/nginx.pid" failed (13)`pid path not moved to `/tmp`check the nginx.conf `pid` line`ERROR: [pool www] failed to open error_log` or `unable to bind listening socket`PHP-FPM socket or log path not writablesocket must be under `/tmp`; check `listen =` in `www.conf``chown: changing ownership ... Operation not permitted` on startupentrypoint still runs `chown`confirm the sed in the Dockerfile applied; `grep chown /docker-entrypoint.sh` should show only comments`The MAC is invalid` after scaling or restartreplicas have different `APP_KEY`set one `APP_KEY` in the Secret for all pods`Permission denied` writing `storage/logs/dreamfactory.log`storage not group-writablerebuild; the `chgrp 0 && chmod g=u` step must cover `/opt/dreamfactory/storage`Pod stays `Pending`, event says `unable to validate against any security context constraint``runAsUser` or `fsGroup` set in valuesremove them; let OpenShift assignAdmin UI loads over `http` links behind an `https` Route`HTTPS_HEADER` unsetset `HTTPS_HEADER=on`

If you need to confirm which SCC admitted the pod:

```
oc get pod <pod> -n dreamfactory -o jsonpath='{.metadata.annotations.openshift\.io/scc}'
```

`restricted-v2` is the goal. If you see `anyuid`, someone granted the service account a broader SCC; the image above does not need it.

## Related[​](#related)

- [Helm chart installation](/getting-started/installing-dreamfactory/helm-installation) for the base chart and commercial image build

- [Docker installation](/getting-started/installing-dreamfactory/docker-installation)

- [Activating your license](/getting-started/installing-dreamfactory/activating-your-license)

- [CORS and SSL](/system-settings/config/cors-ssl)