From 97a833f1e7c5330eb197e07c2e5c20d9953f8419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 31 Jul 2026 19:27:15 +0200 Subject: [PATCH] :whale: Migrate penpot images to DHI (#10734) Move Dockerfile.frontend, .backend, .exporter, .mcp and .storybook under docker/images/ from ubuntu:26.04 / nginx-unprivileged / a manual Node tarball install to Docker Hardened Images (Debian 13, or Alpine for storybook). storybook and mcp get a true non-dev runtime; frontend, backend and exporter keep the -dev tag as their final image, since each needs a shell and/or package manager at container runtime (nginx templating, fontforge/python3, and a headless-browser stack, respectively). --- .github/workflows/build-docker.yml | 15 +++++++ docker/images/Dockerfile.backend | 17 ++++---- docker/images/Dockerfile.exporter | 60 +++++++++----------------- docker/images/Dockerfile.frontend | 16 +++---- docker/images/Dockerfile.mcp | 67 +++++++----------------------- docker/images/Dockerfile.storybook | 25 +++-------- 6 files changed, 76 insertions(+), 124 deletions(-) diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index c0a813ad14..56940f4bff 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -107,6 +107,19 @@ jobs: username: ${{ secrets.PUB_DOCKER_USERNAME }} password: ${{ secrets.PUB_DOCKER_PASSWORD }} + # Frontend, backend, exporter, storybook and mcp now build FROM + # Docker Hardened Images (dhi.io). DHI is free (Apache 2.0, no + # subscription), but pulling from it still requires an + # authenticated login -- a separate `docker login` against a + # different registry host, even though it reuses the same + # PUB_DOCKER_* credentials as the DockerHub login above. + - name: Login to Docker Hardened Images registry (base image pull) + uses: docker/login-action@v4 + with: + registry: dhi.io + username: ${{ secrets.PUB_DOCKER_USERNAME }} + password: ${{ secrets.PUB_DOCKER_PASSWORD }} + # Every build pushes an immutable `build-` tag; if it already # exists in the registry, this exact image was already built and the # whole build can be skipped. Moving tags are handled by `promote`. @@ -164,6 +177,8 @@ jobs: file: ./docker/images/Dockerfile.${{ matrix.image }} platforms: linux/amd64,linux/arm64 push: true + provenance: mode=max + sbom: true # Immutable tag only; branch tags are moved atomically for the # whole image set by the `promote` job. tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:build-${{ needs.prepare.outputs.build_key }} diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index 03627611e1..98b3e02ab8 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -1,4 +1,4 @@ -FROM ubuntu:26.04 AS build +FROM dhi.io/debian-base:trixie-debian13-dev AS build LABEL maintainer="Penpot " ENV LANG='C.UTF-8' \ @@ -47,7 +47,8 @@ RUN set -eux; \ --add-modules java.base,jdk.net,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported,jdk.jfr,jdk.jcmd \ --output /opt/jre; -FROM ubuntu:26.04 AS image + +FROM dhi.io/debian-base:trixie-debian13-dev AS image LABEL maintainer="Penpot " ENV LANG='C.UTF-8' \ @@ -55,11 +56,13 @@ ENV LANG='C.UTF-8' \ JAVA_HOME="/opt/jre" \ PATH=/opt/jre/bin:/opt/imagick/bin:$PATH \ DEBIAN_FRONTEND=noninteractive \ - TZ=Etc/UTC + TZ=Etc/UTC \ + LD_LIBRARY_PATH=/opt/imagick/lib/deps RUN set -ex; \ - useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ apt-get -qq update; \ + apt-get -qqy --no-install-recommends install passwd; \ + useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ apt-get -qq dist-upgrade; \ apt-get -qqy --no-install-recommends install \ ca-certificates \ @@ -71,7 +74,7 @@ RUN set -ex; \ libglib2.0-0 \ libgomp1 \ libheif1 \ - libjpeg-turbo8 \ + libjpeg62-turbo \ liblcms2-2 \ libopenexr-3-1-30 \ libopenjp2-7 \ @@ -81,7 +84,7 @@ RUN set -ex; \ libwebp7 \ libwebpdemux2 \ libwebpmux3 \ - libxml2-16 \ + libxml2 \ libzip5 \ libzstd1 \ python3 \ @@ -100,7 +103,7 @@ RUN set -ex; \ chown -R penpot:penpot /opt/data; COPY --from=build /opt/jre /opt/jre -COPY --from=penpotapp/imagemagick:7.1.2-24 /opt/imagick /opt/imagick +COPY --from=penpotapp/imagemagick:7.1.2-27 /opt/imagick /opt/imagick COPY files/imagemagick-policy.xml /opt/imagick/etc/ImageMagick-7/policy.xml diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index fd5eee266c..7c0b1a14ff 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -1,18 +1,16 @@ -FROM ubuntu:26.04 +FROM dhi.io/node:24.18.1-debian13-dev LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v24.18.1 \ DEBIAN_FRONTEND=noninteractive \ - PATH=/opt/node/bin:/opt/imagick/bin:$PATH \ + PATH=/opt/imagick/bin:$PATH \ PLAYWRIGHT_BROWSERS_PATH=/opt/penpot/browsers RUN set -ex; \ - useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ - mkdir -p /etc/resolvconf/resolv.conf.d; \ - echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \ apt-get -qq update; \ + apt-get -qqy --no-install-recommends install passwd; \ + useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ apt-get -qq dist-upgrade; \ apt-get -qqy --no-install-recommends install \ curl \ @@ -44,16 +42,16 @@ RUN set -ex; \ fonts-freefont-ttf \ poppler-utils \ \ - libasound2t64 \ - libatk-bridge2.0-0t64 \ - libatk1.0-0t64 \ - libatspi2.0-0t64 \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libatspi2.0-0 \ libcairo2 \ - libcups2t64 \ + libcups2 \ libdbus-1-3 \ libdrm2 \ libgbm1 \ - libglib2.0-0t64 \ + libglib2.0-0 \ libnspr4 \ libnss3 \ libpango-1.0-0 \ @@ -68,7 +66,7 @@ RUN set -ex; \ \ libgomp1 \ libheif1 \ - libjpeg-turbo8 \ + libjpeg62-turbo \ liblcms2-2 \ libopenexr-3-1-30 \ libopenjp2-7 \ @@ -78,44 +76,26 @@ RUN set -ex; \ libwebp7 \ libwebpdemux2 \ libwebpmux3 \ - libxml2-16 \ + libxml2 \ libzip5 \ libzstd1 \ ; \ apt-get clean; \ - rm -rf /var/lib/apt/lists/*; - -RUN set -eux; \ - ARCH="$(dpkg --print-architecture)"; \ - case "${ARCH}" in \ - aarch64|arm64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ - ;; \ - amd64|x86_64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ - ;; \ - *) \ - echo "Unsupported arch: ${ARCH}"; \ - exit 1; \ - ;; \ - esac; \ - curl -LfsSo /tmp/nodejs.tar.gz ${BINARY_URL}; \ - mkdir -p /opt/node; \ - cd /opt/node; \ - tar -xf /tmp/nodejs.tar.gz --strip-components=1; \ - chown -R root /opt/node; \ - rm -rf /tmp/nodejs.tar.gz; \ - corepack enable; \ + rm -rf /var/lib/apt/lists/*; \ mkdir -p /opt/penpot; \ chown -R penpot:penpot /opt/penpot; ARG BUNDLE_PATH="./bundle-exporter/" COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/ -COPY --from=penpotapp/imagemagick:7.1.2-24 /opt/imagick /opt/imagick +COPY --from=penpotapp/imagemagick:7.1.2-27 /opt/imagick /opt/imagick WORKDIR /opt/penpot/exporter + +# DHI Node image installs Node at the system level (symlinked into +# /usr/bin), so `./setup`'s internal `corepack enable` needs root to write +# there. Ownership is fixed right after. +RUN ./setup && chown -R penpot:penpot /opt/penpot/exporter + USER penpot:penpot -RUN ./setup - CMD ["node", "app.js"] diff --git a/docker/images/Dockerfile.frontend b/docker/images/Dockerfile.frontend index 0772aab00c..306c42467d 100644 --- a/docker/images/Dockerfile.frontend +++ b/docker/images/Dockerfile.frontend @@ -1,15 +1,17 @@ -FROM nginxinc/nginx-unprivileged:1.31.1-alpine +FROM dhi.io/nginx:1.31.1-debian13-dev LABEL maintainer="Penpot " +ENV DEBIAN_FRONTEND=noninteractive + USER root RUN set -ex; \ - apk update; \ - apk upgrade; \ - apk add --no-cache bash gettext; \ - rm -rf /var/cache/apk/*; \ - addgroup -g 1001 penpot; \ - adduser -D -H -u 1001 -s /bin/false -h /opt/penpot -G penpot penpot; \ + apt-get -qq update; \ + apt-get -qq -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" dist-upgrade; \ + apt-get -qqy -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --no-install-recommends install bash gettext-base passwd; \ + rm -rf /var/lib/apt/lists/*; \ + groupadd -f -g 1001 penpot; \ + useradd -M -u 1001 -s /usr/sbin/nologin -d /opt/penpot -g penpot penpot; \ mkdir -p /opt/data/assets; \ chown -R penpot:penpot /opt/data; \ mkdir -p /etc/nginx/overrides/main.d/; \ diff --git a/docker/images/Dockerfile.mcp b/docker/images/Dockerfile.mcp index 5147ddbfa6..8d943c7e27 100644 --- a/docker/images/Dockerfile.mcp +++ b/docker/images/Dockerfile.mcp @@ -1,61 +1,26 @@ -FROM ubuntu:26.04 +FROM dhi.io/node:24.18.1-debian13-dev AS build +LABEL maintainer="Penpot " + +ENV DEBIAN_FRONTEND=noninteractive + +ARG BUNDLE_PATH="./bundle-mcp/" +COPY $BUNDLE_PATH /opt/penpot/mcp/ + +WORKDIR /opt/penpot/mcp + +RUN ./setup + + +FROM dhi.io/node:24.18.1-debian13 AS image LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v24.18.1 \ - DEBIAN_FRONTEND=noninteractive \ - PATH=/opt/node/bin:$PATH \ PENPOT_MCP_SERVER_HOST=0.0.0.0 -RUN set -ex; \ - useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \ - mkdir -p /etc/resolvconf/resolv.conf.d; \ - echo "nameserver 127.0.0.11" > /etc/resolvconf/resolv.conf.d/tail; \ - apt-get -qq update; \ - apt-get -qq dist-upgrade; \ - apt-get -qqy --no-install-recommends install \ - curl \ - tzdata \ - locales \ - ca-certificates \ - ; \ - apt-get clean; \ - rm -rf /var/lib/apt/lists/*; \ - echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen; \ - locale-gen; \ - find /usr/share/i18n/locales/ -type f ! -name "en_US" ! -name "POSIX" ! -name "C" -delete; - -RUN set -eux; \ - ARCH="$(dpkg --print-architecture)"; \ - case "${ARCH}" in \ - aarch64|arm64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ - ;; \ - amd64|x86_64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ - ;; \ - *) \ - echo "Unsupported arch: ${ARCH}"; \ - exit 1; \ - ;; \ - esac; \ - curl -LfsSo /tmp/nodejs.tar.gz ${BINARY_URL}; \ - mkdir -p /opt/node; \ - cd /opt/node; \ - tar -xf /tmp/nodejs.tar.gz --strip-components=1; \ - chown -R root /opt/node; \ - rm -rf /tmp/nodejs.tar.gz; \ - corepack enable; \ - mkdir -p /opt/penpot; \ - chown -R penpot:penpot /opt/penpot; - -ARG BUNDLE_PATH="./bundle-mcp/" -COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/mcp/ +COPY --from=build --chown=node:node /opt/penpot/mcp /opt/penpot/mcp WORKDIR /opt/penpot/mcp -USER penpot:penpot - -RUN ./setup +USER node CMD ["node", "index.js", "--multi-user"] diff --git a/docker/images/Dockerfile.storybook b/docker/images/Dockerfile.storybook index 26bb326383..3904604a25 100644 --- a/docker/images/Dockerfile.storybook +++ b/docker/images/Dockerfile.storybook @@ -1,24 +1,11 @@ -FROM nginxinc/nginx-unprivileged:1.31.1-alpine +FROM dhi.io/nginx:1.31.1-alpine3.22 LABEL maintainer="Penpot " -USER root - -RUN set -ex; \ - apk update; \ - apk upgrade; \ - rm -rf /var/cache/apk/*; \ - addgroup -g 1001 penpot; \ - adduser -D -H -u 1001 -s /bin/false -h /opt/penpot -G penpot penpot; - ARG BUNDLE_PATH="./bundle-storybook/" -COPY $BUNDLE_PATH /var/www/ -COPY ./files/nginx.storybook.conf /etc/nginx/conf.d/default.conf -RUN chown -R 1001:0 /var/cache/nginx; \ - chmod -R g+w /var/cache/nginx; \ - chown -R 1001:0 /etc/nginx; \ - chmod -R g+w /etc/nginx; \ - chown -R 1001:0 /var/www; \ - chmod -R g+w /var/www; +COPY --chown=65532:65532 $BUNDLE_PATH /var/www/ +COPY --chown=65532:65532 ./files/nginx.storybook.conf /etc/nginx/conf.d/default.conf -USER penpot:penpot +# Already the default user in the DHI runtime; explicit for clarity. Numeric +# UID, not by name -- this image has no "nonroot" entry in /etc/passwd. +USER 65532:65532