From c8113aa18870f13d79d50b39b2d0a0d051cdf330 Mon Sep 17 00:00:00 2001 From: Petre Ghita <564327+petre@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:10:27 +0200 Subject: [PATCH] fix: support OpenShift arbitrary UID/GID-0 in Docker image (#5136) * fix: support OpenShift arbitrary UID/GID-0 in Docker image Made-with: Cursor * move to community --------- Co-authored-by: Petre Ghita Co-authored-by: Timothy Carambat --- cloud-deployments/openshift/Dockerfile | 225 ++++++++++++++++++ cloud-deployments/openshift/README.md | 146 ++++++++++++ .../openshift/docker-entrypoint.sh | 39 +++ 3 files changed, 410 insertions(+) create mode 100644 cloud-deployments/openshift/Dockerfile create mode 100644 cloud-deployments/openshift/README.md create mode 100644 cloud-deployments/openshift/docker-entrypoint.sh diff --git a/cloud-deployments/openshift/Dockerfile b/cloud-deployments/openshift/Dockerfile new file mode 100644 index 00000000..14e42c26 --- /dev/null +++ b/cloud-deployments/openshift/Dockerfile @@ -0,0 +1,225 @@ +# OpenShift-compatible Dockerfile for AnythingLLM +# +# This Dockerfile is specifically designed for OpenShift deployments which use +# arbitrary UIDs with GID 0 (root group). Do NOT use this for standard Docker +# or Docker Compose deployments - use the main docker/Dockerfile instead. +# +# Key differences from the standard Dockerfile: +# - User is added to supplementary group 0 (root) for OpenShift compatibility +# - All files are owned by group 0 and are group-writable (chmod g+w) +# - /etc/passwd is made group-writable for dynamic UID injection +# - Uses a modified entrypoint that handles arbitrary UID scenarios + +# Setup base image +FROM ubuntu:noble-20251013 AS base + +# Build arguments +ARG ARG_UID=1000 +ARG ARG_GID=1000 + +FROM base AS build-arm64 +RUN echo "Preparing build of AnythingLLM image for arm64 architecture" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install system dependencies +# hadolint ignore=DL3008,DL3013 +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ + unzip curl gnupg libgfortran5 libgbm1 tzdata netcat-openbsd \ + libasound2t64 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 \ + libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \ + libxss1 libxtst6 ca-certificates fonts-liberation libappindicator3-1 libnss3 lsb-release \ + xdg-utils git build-essential ffmpeg && \ + mkdir -p /etc/apt/keyrings && \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ + apt-get update && \ + # Install node and yarn + apt-get install -yq --no-install-recommends nodejs && \ + curl -LO https://github.com/yarnpkg/yarn/releases/download/v1.22.19/yarn_1.22.19_all.deb \ + && dpkg -i yarn_1.22.19_all.deb \ + && rm yarn_1.22.19_all.deb && \ + # Install uvx (pinned to 0.6.10) for MCP support + curl -LsSf https://astral.sh/uv/0.6.10/install.sh | sh && \ + mv /root/.local/bin/uv /usr/local/bin/uv && \ + mv /root/.local/bin/uvx /usr/local/bin/uvx && \ + echo "Installed uvx! $(uv --version)" && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create a group and user with specific UID and GID. +# The user's primary group is ARG_GID (default 1000) for Docker/Compose backward compatibility. +# The user is also added to the root group (GID 0) as a supplementary group so that +# OpenShift's arbitrary-UID-with-GID-0 model works via group-writable permissions. +RUN (getent passwd "$ARG_UID" && userdel -f "$(getent passwd "$ARG_UID" | cut -d: -f1)") || true && \ + if [ "$ARG_GID" != "0" ]; then \ + (getent group "$ARG_GID" && groupdel "$(getent group "$ARG_GID" | cut -d: -f1)") || true && \ + groupadd -g "$ARG_GID" anythingllm && \ + useradd -l -u "$ARG_UID" -m -d /app -s /bin/bash -g anythingllm -G 0 anythingllm; \ + else \ + useradd -l -u "$ARG_UID" -m -d /app -s /bin/bash -g 0 anythingllm; \ + fi && \ + mkdir -p /app/frontend/ /app/server/ /app/collector/ /app/.cache /app/.yarn && \ + chown -R anythingllm:0 /app && \ + chmod -R g+w /app && \ + chmod g=u /etc/passwd + +# Copy docker helper scripts (use OpenShift-specific entrypoint) +COPY ./cloud-deployments/openshift/docker-entrypoint.sh /usr/local/bin/ +COPY ./docker/docker-healthcheck.sh /usr/local/bin/ +COPY --chown=anythingllm:0 ./docker/.env.example /app/server/.env + +# Ensure the scripts are executable +RUN chmod +x /usr/local/bin/docker-entrypoint.sh && \ + chmod +x /usr/local/bin/docker-healthcheck.sh + +USER anythingllm +WORKDIR /app + +# Puppeteer does not ship with an ARM86 compatible build for Chromium +# so web-scraping would be broken in arm docker containers unless we patch it +# by manually installing a compatible chromedriver. +RUN echo "Need to patch Puppeteer x Chromium support for ARM86 - installing dep!" && \ + curl -fSL https://webassets.anythingllm.com/chromium-1088-linux-arm64.zip -o chrome-linux.zip && \ + unzip chrome-linux.zip && \ + rm -rf chrome-linux.zip + +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV CHROME_PATH=/app/chrome-linux/chrome +ENV PUPPETEER_EXECUTABLE_PATH=/app/chrome-linux/chrome + +RUN echo "Done running arm64 specific installation steps" + +############################################# + +# amd64-specific stage +FROM base AS build-amd64 +RUN echo "Preparing build of AnythingLLM image for non-ARM architecture" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install system dependencies +# hadolint ignore=DL3008,DL3013 +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ + curl gnupg libgfortran5 libgbm1 tzdata netcat-openbsd \ + libasound2t64 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 \ + libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \ + libxss1 libxtst6 ca-certificates fonts-liberation libappindicator3-1 libnss3 lsb-release \ + xdg-utils git build-essential ffmpeg && \ + mkdir -p /etc/apt/keyrings && \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ + apt-get update && \ + # Install node and yarn + apt-get install -yq --no-install-recommends nodejs && \ + curl -LO https://github.com/yarnpkg/yarn/releases/download/v1.22.19/yarn_1.22.19_all.deb \ + && dpkg -i yarn_1.22.19_all.deb \ + && rm yarn_1.22.19_all.deb && \ + # Install uvx (pinned to 0.6.10) for MCP support + curl -LsSf https://astral.sh/uv/0.6.10/install.sh | sh && \ + mv /root/.local/bin/uv /usr/local/bin/uv && \ + mv /root/.local/bin/uvx /usr/local/bin/uvx && \ + echo "Installed uvx! $(uv --version)" && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create a group and user with specific UID and GID. +# The user's primary group is ARG_GID (default 1000) for Docker/Compose backward compatibility. +# The user is also added to the root group (GID 0) as a supplementary group so that +# OpenShift's arbitrary-UID-with-GID-0 model works via group-writable permissions. +RUN (getent passwd "$ARG_UID" && userdel -f "$(getent passwd "$ARG_UID" | cut -d: -f1)") || true && \ + if [ "$ARG_GID" != "0" ]; then \ + (getent group "$ARG_GID" && groupdel "$(getent group "$ARG_GID" | cut -d: -f1)") || true && \ + groupadd -g "$ARG_GID" anythingllm && \ + useradd -l -u "$ARG_UID" -m -d /app -s /bin/bash -g anythingllm -G 0 anythingllm; \ + else \ + useradd -l -u "$ARG_UID" -m -d /app -s /bin/bash -g 0 anythingllm; \ + fi && \ + mkdir -p /app/frontend/ /app/server/ /app/collector/ /app/.cache /app/.yarn && \ + chown -R anythingllm:0 /app && \ + chmod -R g+w /app && \ + chmod g=u /etc/passwd + +# Copy docker helper scripts (use OpenShift-specific entrypoint) +COPY ./cloud-deployments/openshift/docker-entrypoint.sh /usr/local/bin/ +COPY ./docker/docker-healthcheck.sh /usr/local/bin/ +COPY --chown=anythingllm:0 ./docker/.env.example /app/server/.env + +# Ensure the scripts are executable +RUN chmod +x /usr/local/bin/docker-entrypoint.sh && \ + chmod +x /usr/local/bin/docker-healthcheck.sh + +############################################# +# COMMON BUILD FLOW FOR ALL ARCHS +############################################# + +# hadolint ignore=DL3006 +FROM build-${TARGETARCH} AS build +RUN echo "Running common build flow of AnythingLLM image for all architectures" + +USER anythingllm +WORKDIR /app + +# Install & Build frontend layer +# Use BUILDPLATFORM to run on the native host architecture (not emulated). +# This avoids esbuild crashing under QEMU when cross-compiling. +# The output (static HTML/CSS/JS) is platform-independent. +FROM --platform=$BUILDPLATFORM node:18-slim AS frontend-build +WORKDIR /app/frontend +COPY ./frontend/package.json ./frontend/yarn.lock ./ +RUN yarn install --network-timeout 100000 && yarn cache clean +COPY ./frontend/ ./ +RUN yarn build +WORKDIR /app + +# Install server layer +# Also pull and build collector deps (chromium issues prevent bad bindings) +FROM build AS backend-build +COPY --chown=anythingllm:0 ./server /app/server/ +WORKDIR /app/server +RUN yarn install --production --network-timeout 100000 && yarn cache clean +WORKDIR /app + +# Install collector dependencies +COPY --chown=anythingllm:0 ./collector/ ./collector/ +WORKDIR /app/collector +ENV PUPPETEER_DOWNLOAD_BASE_URL=https://storage.googleapis.com/chrome-for-testing-public +RUN yarn install --production --network-timeout 100000 && yarn cache clean + +WORKDIR /app +USER anythingllm + +# Since we are building from backend-build we just need to move built frontend into server/public +FROM backend-build AS production-build +WORKDIR /app +COPY --chown=anythingllm:0 --from=frontend-build /app/frontend/dist /app/server/public + +# Ensure all app files are owned by group 0 (root) and group-writable so that: +# - Docker/Compose: access works via user ownership (UID match) +# - OpenShift: access works via group ownership (GID 0 match + g+w) +# - Generic Kubernetes: access works via user ownership or fsGroup +# This must run after all COPY and yarn install steps to cover node_modules, etc. +USER root +RUN chown -R anythingllm:0 /app && \ + chmod -R g+w /app && \ + mkdir -p /app/server/storage + +# Setup the environment +ENV NODE_ENV=production +ENV ANYTHING_LLM_RUNTIME=docker +ENV DEPLOYMENT_VERSION=1.12.1 +ENV HOME=/app + +# Setup the healthcheck +HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \ + CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1 + +USER anythingllm + +# Run the server +# CMD ["sh", "-c", "tail -f /dev/null"] # For development: keep container open +ENTRYPOINT ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh"] diff --git a/cloud-deployments/openshift/README.md b/cloud-deployments/openshift/README.md new file mode 100644 index 00000000..b0be7685 --- /dev/null +++ b/cloud-deployments/openshift/README.md @@ -0,0 +1,146 @@ +> [!IMPORTANT] +> This is a community-maintained template and is not officially supported by the AnythingLLM team. You could encounter issues or even deployment failures in future versions of AnythingLLM. We do our best to keep this template and all community contributions backwards compatible, but we cannot guarantee it. + +# OpenShift Deployment Template for AnythingLLM + +This directory contains a specialized Dockerfile and entrypoint script for deploying AnythingLLM on **Red Hat OpenShift** clusters. + +## Why This Template Exists + +OpenShift has a unique security model that differs from standard Docker/Kubernetes deployments: + +1. **Arbitrary UIDs**: OpenShift runs containers with randomly assigned user IDs (UIDs) that don't exist in `/etc/passwd` +2. **GID 0 Requirement**: All containers run with GID 0 (root group) as the primary group +3. **Restricted SCCs**: The default Security Context Constraints (SCCs) prevent containers from running as specific users + +These requirements are incompatible with the standard AnythingLLM Docker image, which uses a fixed `anythingllm` user with UID/GID 1000. + +## Key Differences from Standard Dockerfile + +| Feature | Standard Docker | OpenShift Template | +|---------|-----------------|-------------------| +| File ownership | `anythingllm:anythingllm` | `anythingllm:0` (root group) | +| File permissions | Standard | Group-writable (`g+w`) | +| `/etc/passwd` | Read-only | Group-writable for UID injection | +| Supplementary groups | None | Added to group 0 | +| Entrypoint | Standard | Handles arbitrary UID scenarios | + +## When to Use This Template + +Use this template **only** if you are deploying to: +- Red Hat OpenShift (any version) +- OKD (OpenShift Origin) +- Any Kubernetes cluster with OpenShift-style restricted SCCs + +**Do NOT use this for:** +- Standard Docker deployments +- Docker Compose +- Generic Kubernetes (use the standard image with appropriate `securityContext`) +- Cloud container services (AWS ECS, Google Cloud Run, Azure Container Instances) + +## Building the Image + +From the repository root: + +```bash +docker build -f cloud-deployments/openshift/Dockerfile -t anythingllm:openshift . +``` + +For multi-architecture builds: + +```bash +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -f cloud-deployments/openshift/Dockerfile \ + -t your-registry/anythingllm:openshift \ + --push . +``` + +## Deploying to OpenShift + +### Using `oc` CLI + +```bash +# Create a new project (namespace) +oc new-project anythingllm + +# Create a deployment +oc new-app your-registry/anythingllm:openshift + +# Expose the service +oc expose svc/anythingllm --port=3001 + +# Set required environment variables +oc set env deployment/anythingllm \ + STORAGE_DIR=/app/server/storage \ + JWT_SECRET=$(openssl rand -hex 32) +``` + +### Using a DeploymentConfig YAML + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: anythingllm +spec: + replicas: 1 + selector: + matchLabels: + app: anythingllm + template: + metadata: + labels: + app: anythingllm + spec: + containers: + - name: anythingllm + image: your-registry/anythingllm:openshift + ports: + - containerPort: 3001 + env: + - name: STORAGE_DIR + value: /app/server/storage + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: anythingllm-secrets + key: jwt-secret + volumeMounts: + - name: storage + mountPath: /app/server/storage + volumes: + - name: storage + persistentVolumeClaim: + claimName: anythingllm-storage +``` + +## Persistent Storage + +OpenShift PersistentVolumeClaims work with this image. Ensure the PVC is created before deployment: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: anythingllm-storage +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +``` + +## Troubleshooting + +### Permission Denied Errors + +If you see permission errors, verify: +1. You're using this OpenShift-specific image, not the standard one +2. The PVC has correct access modes +3. No custom SCCs are overriding the default behavior + +### User Not Found in passwd + +The entrypoint script automatically handles this by injecting a passwd entry at runtime. If issues persist, check that `/etc/passwd` is group-writable in your image. diff --git a/cloud-deployments/openshift/docker-entrypoint.sh b/cloud-deployments/openshift/docker-entrypoint.sh new file mode 100644 index 00000000..3b6b5fd3 --- /dev/null +++ b/cloud-deployments/openshift/docker-entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# OpenShift runs containers with an arbitrary UID that may not exist in /etc/passwd. +# Many tools (npm, prisma, git, etc.) expect a passwd entry for the running user. +# If the current UID has no entry, dynamically add one using nss_wrapper-style injection. +if ! whoami &> /dev/null 2>&1; then + if [ -w /etc/passwd ]; then + echo "anythingllm:x:$(id -u):0:AnythingLLM User:/app:/bin/bash" >> /etc/passwd + fi +fi +export HOME=/app + +# Check if STORAGE_DIR is set +if [ -z "$STORAGE_DIR" ]; then + echo "================================================================" + echo "⚠️ ⚠️ ⚠️ WARNING: STORAGE_DIR environment variable is not set! ⚠️ ⚠️ ⚠️" + echo "" + echo "Not setting this will result in data loss on container restart since" + echo "the application will not have a persistent storage location." + echo "It can also result in weird errors in various parts of the application." + echo "" + echo "Please run the container with the official docker command at" + echo "https://docs.anythingllm.com/installation-docker/quickstart" + echo "" + echo "⚠️ ⚠️ ⚠️ WARNING: STORAGE_DIR environment variable is not set! ⚠️ ⚠️ ⚠️" + echo "================================================================" +fi + +{ + cd /app/server/ && + # Disable Prisma CLI telemetry (https://www.prisma.io/docs/orm/tools/prisma-cli#how-to-opt-out-of-data-collection) + export CHECKPOINT_DISABLE=1 && + npx prisma generate --schema=./prisma/schema.prisma && + npx prisma migrate deploy --schema=./prisma/schema.prisma && + node /app/server/index.js +} & +{ node /app/collector/index.js; } & +wait -n +exit $?