Fix docker bloat (#21948)

* Refactor Docker build/runtime stages for slimmer images

Split runtime and build dependencies into separate stages and build the app in a dedicated stage before runtime copy.

Add a slim prune stage that removes non-runtime source trees, source maps, duplicate enterprise source videos, module test/doc folders, and extra vendored gem artifacts.

This ensures bytes are removed before the final slim copy, so layer size actually decreases while keeping runtime behavior intact.

* Add target-specific Docker image validation in CI

Introduce script/ci/docker_validate_image.sh with validations for slim, slim-bim, and all-in-one images.

Checks include runtime binary presence/absence, plugin asset/module integrity, slim pruning expectations, BIM tooling, and all-in-one API startup/embedded services.

Update docker workflow to run the validator for every matrix target before push.

* fix

* Generate YAML-safe auto Hocuspocus secret

All-in-one startup auto-generates OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET in the entrypoint.

Environment overrides are parsed through YAML, so leading punctuation in the previous charset (e.g. %) could trigger Psych parsing errors and abort boot.

Restrict generated secret characters to alphanumeric to keep parsing stable while preserving high entropy.

* Fix all-in-one hocuspocus runtime and validation

* Fix all-in-one memcached startup handover
This commit is contained in:
Cyril Rohr
2026-02-11 10:40:54 +01:00
committed by GitHub
parent 93044028f1
commit d482f1f708
10 changed files with 540 additions and 86 deletions
+2
View File
@@ -41,5 +41,7 @@ frontend/node_modules
node_modules
# travis
vendor/bundle
# Local checkout; all-in-one copies hocuspocus from its dedicated image.
vendor/hocuspocus
/public/assets
/config/frontend_assets.manifest.json
+5 -16
View File
@@ -255,23 +255,12 @@ jobs:
if [ -d vendor/bundle.bak ]; then
mv vendor/bundle.bak vendor/bundle
fi
- name: Test
# We only test the native container. If that fails the builds for the others
# will be cancelled as well.
if: matrix.platform == 'linux/amd64' && matrix.target == 'all-in-one'
- name: Validate image
run: |
docker run \
--name openproject \
-d -p 8080:80 --platform ${{ matrix.platform }} \
-e SUPERVISORD_LOG_LEVEL=debug \
-e OPENPROJECT_LOGIN__REQUIRED=false \
-e OPENPROJECT_HTTPS=false \
${{ steps.build.outputs.imageid }}
sleep 60
docker logs openproject --tail 300
wget -O- --retry-on-http-error=503,502 --retry-connrefused http://localhost:8080/api/v3
./script/ci/docker_validate_image.sh \
--image "${{ steps.build.outputs.imageid }}" \
--target "${{ matrix.target }}" \
--platform "${{ matrix.platform }}"
- name: Push image
id: push
uses: docker/build-push-action@v6
+41 -7
View File
@@ -3,7 +3,7 @@ ARG DEBIAN_BASE="trixie"
# Add SBOM scan context for intermediate steps
ARG BUILDKIT_SBOM_SCAN_CONTEXT=true
ARG BUILDKIT_SBOM_SCAN_STAGE=true
FROM ruby:${RUBY_VERSION}-slim-${DEBIAN_BASE} AS base
FROM ruby:${RUBY_VERSION}-slim-${DEBIAN_BASE} AS runtime-base
LABEL maintainer="operations@openproject.com"
ARG NODE_VERSION="22.21.0"
@@ -19,6 +19,8 @@ ENV DOCKER=1
ENV APP_USER=app
ENV APP_PATH=/app
ENV APP_DATA_PATH=/var/openproject/assets
ENV BUNDLE_PATH="$APP_PATH/vendor/bundle"
ENV BUNDLE_APP_CONFIG="$APP_PATH/.bundle"
ENV PGVERSION="17"
ENV PGVERSION_CHOICES="13 15 17"
ENV PGBIN="/usr/lib/postgresql/$PGVERSION/bin"
@@ -54,10 +56,19 @@ WORKDIR $APP_PATH
# upgrade bundler
RUN gem install bundler --no-document
# system dependencies, nodejs
# runtime dependencies
COPY ./docker/prod/setup/preinstall-common.sh ./docker/prod/setup/preinstall-common.sh
RUN ./docker/prod/setup/preinstall-common.sh
FROM runtime-base AS build-base
ARG NODE_VERSION="22.21.0"
# build-only dependencies
COPY ./docker/prod/setup/preinstall-build.sh ./docker/prod/setup/preinstall-build.sh
RUN ./docker/prod/setup/preinstall-build.sh
FROM build-base AS app-build
# stuff required for gems
COPY Gemfile Gemfile.* .ruby-version ./
COPY modules ./modules
@@ -73,15 +84,32 @@ COPY . .
# Copy lock file again as the updated version was overriden by COPY just now
RUN cp Gemfile.lock.bak Gemfile.lock && rm Gemfile.lock.bak && \
./docker/prod/setup/precompile-assets.sh && \
./docker/prod/setup/postinstall-common.sh && \
./docker/prod/setup/precompile-assets.sh
FROM app-build AS app-build-slim
COPY ./docker/prod/setup/prune-slim-runtime.sh ./docker/prod/setup/prune-slim-runtime.sh
RUN ./docker/prod/setup/prune-slim-runtime.sh
FROM runtime-base AS app-runtime
COPY --chown=$APP_USER:$APP_USER --from=app-build /app /app
RUN ./docker/prod/setup/postinstall-common.sh && \
cp ./config/database.production.yml config/database.yml && \
ln -s $APP_PATH/docker/prod/setup/.irbrc /home/$APP_USER/
FROM runtime-base AS app-runtime-slim
COPY --chown=$APP_USER:$APP_USER --from=app-build-slim /app /app
RUN ./docker/prod/setup/postinstall-common.sh && \
cp ./config/database.production.yml config/database.yml && \
ln -s $APP_PATH/docker/prod/setup/.irbrc /home/$APP_USER/
# -------------------------------------
# slim (public)
# -------------------------------------
FROM base AS slim
FROM app-runtime-slim AS slim
USER $APP_USER
EXPOSE 8080
@@ -93,7 +121,7 @@ VOLUME ["$APP_DATA_PATH"]
# slim-bim (public)
# same as slim but with BIM support enabled
# -------------------------------------
FROM base AS slim-bim
FROM app-runtime-slim AS slim-bim
USER $APP_USER
EXPOSE 8080
@@ -104,7 +132,7 @@ ENV OPENPROJECT_EDITION=bim
# -------------------------------------
# all-in-one (public)
# -------------------------------------
FROM base AS all-in-one
FROM app-runtime AS all-in-one
ENV OPENPROJECT_RAILS__CACHE__STORE=memcache
ENV DATABASE_URL=postgres://openproject:openproject@127.0.0.1/openproject
@@ -114,8 +142,14 @@ COPY --from=openproject/gosu /go/bin/gosu /usr/local/bin/gosu
RUN chmod +x /usr/local/bin/gosu && gosu nobody true
COPY --from=openproject/hocuspocus:17.0.3 --chown=$APP_USER:$APP_USER /app /opt/hocuspocus
# Keep node/npm in all-in-one for bundled hocuspocus even when BIM support is disabled.
COPY --from=build-base /usr/local/bin/node /usr/local/bin/node
COPY --from=build-base /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ./docker/prod/setup/postinstall-onprem.sh && \
ln -sf ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
ln -sf ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \
ln -sf ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack && \
ln -s /app/docker/prod/setup/.irbrc /root/
# Expose ports for apache and postgres
+2 -1
View File
@@ -97,7 +97,8 @@ if [ "$(id -u)" = '0' ]; then
HP_HOST=${OPENPROJECT_HOST__NAME:="localhost"}
export OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__URL="${HP_PROTOCOL}://${HP_HOST}/hocuspocus"
export OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET="$(tr -dc 'A-Za-z0-9!?%=' < /dev/urandom | head -c 32)"
# Use a YAML-safe secret charset because environment values are parsed via YAML.
export OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET="$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)"
fi
exec "$@"
+5
View File
@@ -10,3 +10,8 @@ cp Gemfile.lock Gemfile.lock.bak
rm -rf vendor/bundle/ruby/*/cache
rm -rf vendor/bundle/ruby/*/gems/*/spec
rm -rf vendor/bundle/ruby/*/gems/*/test
rm -rf vendor/bundle/ruby/*/gems/*/tests
rm -rf vendor/bundle/ruby/*/gems/*/{doc,docs,example,examples,benchmark,benchmarks}
rm -rf vendor/bundle/ruby/*/bundler/gems/*/.git
rm -rf vendor/bundle/ruby/*/bundler/gems/*/{spec,test,tests,doc,docs,example,examples,benchmark,benchmarks}
find vendor/bundle -type f \( -name '*.a' -o -name '*.o' \) -delete
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -euxo pipefail
get_architecture() {
if command -v uname > /dev/null; then
ARCHITECTURE=$(uname -m)
case $ARCHITECTURE in
aarch64|arm64)
echo "arm64"
return 0
;;
ppc64le)
echo "ppc64le"
return 0
;;
esac
fi
echo "x64"
return 0
}
ARCHITECTURE=$(get_architecture)
apt-get update -qq
apt-get install -yq --no-install-recommends \
ca-certificates \
curl \
git \
build-essential \
libyaml-dev \
libpq-dev \
libclang-dev
if ! command -v node > /dev/null || ! command -v npm > /dev/null; then
curl -s https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCHITECTURE}.tar.gz | tar xzf - -C /usr/local --strip-components=1
fi
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
truncate -s 0 /var/log/*log
+66 -60
View File
@@ -2,25 +2,24 @@
set -euxo pipefail
get_architecture() {
if command -v uname > /dev/null; then
ARCHITECTURE=$(uname -m)
case $ARCHITECTURE in
aarch64|arm64)
echo "arm64"
return 0
;;
ppc64le)
echo "ppc64le"
return 0
;;
esac
fi
if command -v uname > /dev/null; then
ARCHITECTURE=$(uname -m)
case $ARCHITECTURE in
aarch64|arm64)
echo "arm64"
return 0
;;
ppc64le)
echo "ppc64le"
return 0
;;
esac
fi
echo "x64"
return 0
echo "x64"
return 0
}
set -exo pipefail
ARCHITECTURE=$(get_architecture)
apt-get update -qq
@@ -28,69 +27,76 @@ apt-get update -qq
apt-get upgrade -y
apt-get install -yq --no-install-recommends \
curl \
wget \
file \
gnupg2 \
lsb-release
ca-certificates \
curl \
wget \
file \
gnupg2 \
lsb-release
# install node + npm
curl -s https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCHITECTURE}.tar.gz | tar xzf - -C /usr/local --strip-components=1
wget --quiet -O- https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgrsql.gpg -
echo "deb [signed-by=/usr/share/keyrings/postgrsql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
wget --quiet -O- https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg -
echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
apt-get update -qq
apt-get install -yq --no-install-recommends \
libpq-dev \
libpq5 \
libffi8 \
unrtf \
tesseract-ocr \
poppler-utils \
catdoc \
imagemagick \
libclang-dev \
libjemalloc2 \
git \
build-essential \
libyaml-dev \
libpq5 \
libffi8 \
unrtf \
tesseract-ocr \
poppler-utils \
catdoc \
imagemagick \
libjemalloc2
for version in $PGVERSION_CHOICES ; do
apt-get install -yq --no-install-recommends postgresql-client-$version
apt-get install -yq --no-install-recommends postgresql-client-$version
done
# Specifics for BIM edition
if [ ! "$BIM_SUPPORT" = "false" ]; then
apt-get install -y wget unzip
apt-get install -yq --no-install-recommends unzip
tmpdir=$(mktemp -d)
cd $tmpdir
# Install node + npm for BIM runtime tools.
curl -s https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCHITECTURE}.tar.gz | tar xzf - -C /usr/local --strip-components=1
# Install XKT converter
npm install -g @xeokit/xeokit-gltf-to-xkt@1.3.1
tmpdir=$(mktemp -d)
cd $tmpdir
# Install COLLADA2GLTF
wget --no-verbose --tries 3 https://github.com/KhronosGroup/COLLADA2GLTF/releases/download/v2.1.5/COLLADA2GLTF-v2.1.5-linux.zip
unzip -q COLLADA2GLTF-v2.1.5-linux.zip
mv COLLADA2GLTF-bin "/usr/local/bin/COLLADA2GLTF"
# Install XKT converter
npm install -g @xeokit/xeokit-gltf-to-xkt@1.3.1
# IFCconvert
wget --no-verbose --tries 3 https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-fea8e3a-linux64.zip
unzip -q IfcConvert-v0.7.11-fea8e3a-linux64.zip
mv IfcConvert "/usr/local/bin/IfcConvert"
# Install COLLADA2GLTF
wget --no-verbose --tries 3 https://github.com/KhronosGroup/COLLADA2GLTF/releases/download/v2.1.5/COLLADA2GLTF-v2.1.5-linux.zip
unzip -q COLLADA2GLTF-v2.1.5-linux.zip
mv COLLADA2GLTF-bin "/usr/local/bin/COLLADA2GLTF"
wget --no-verbose --tries 3 https://github.com/opf/xeokit-metadata/releases/download/v1.1.0/xeokit-metadata-linux-x64.tar.gz
tar -zxvf xeokit-metadata-linux-x64.tar.gz
chmod +x xeokit-metadata-linux-x64/xeokit-metadata
cp -r xeokit-metadata-linux-x64/ "/usr/lib/xeokit-metadata"
ln -s /usr/lib/xeokit-metadata/xeokit-metadata /usr/local/bin/xeokit-metadata
# IFCconvert
wget --no-verbose --tries 3 https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-fea8e3a-linux64.zip
unzip -q IfcConvert-v0.7.11-fea8e3a-linux64.zip
mv IfcConvert "/usr/local/bin/IfcConvert"
cd /
rm -rf $tmpdir
wget --no-verbose --tries 3 https://github.com/opf/xeokit-metadata/releases/download/v1.1.0/xeokit-metadata-linux-x64.tar.gz
tar -zxvf xeokit-metadata-linux-x64.tar.gz
chmod +x xeokit-metadata-linux-x64/xeokit-metadata
cp -r xeokit-metadata-linux-x64/ "/usr/lib/xeokit-metadata"
ln -s /usr/lib/xeokit-metadata/xeokit-metadata /usr/local/bin/xeokit-metadata
cd /
rm -rf $tmpdir
fi
id $APP_USER || useradd -d /home/$APP_USER -m $APP_USER
# Purge helper packages used only while building this stage.
apt-get purge -yq --auto-remove \
file \
gnupg2 \
lsb-release
# curl/wget are only needed during installation in this stage.
apt-get purge -yq --auto-remove \
curl \
wget
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
truncate -s 0 /var/log/*log
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
set -euxo pipefail
APP_PATH=${APP_PATH:-/app}
# Remove source-only trees that are not needed for slim runtime images.
rm -rf \
"$APP_PATH/spec" \
"$APP_PATH/screenshots" \
"$APP_PATH/lookbook" \
"$APP_PATH/frontend"
# Keep precompiled enterprise media in public/assets and remove duplicate source videos.
if [ -d "$APP_PATH/public/assets/enterprise" ]; then
rm -rf "$APP_PATH/app/assets/videos/enterprise"
fi
# Source maps are useful during development, but unnecessary in slim runtime images.
find "$APP_PATH/public/assets" -type f -name '*.map' -delete
# Lookbook source is removed above, so its compiled static assets are unnecessary too.
rm -rf "$APP_PATH/public/assets/lookbook"
# Module test and documentation folders are not used at runtime.
find "$APP_PATH/modules" -mindepth 2 -maxdepth 2 -type d \
\( -name spec -o -name test -o -name tests -o -name doc -o -name docs \) \
-prune -exec rm -rf '{}' +
# Remove leftover git metadata and common non-runtime folders from vendored git gems.
for gem_root in "$APP_PATH/vendor/bundle"/ruby/*/gems "$APP_PATH/vendor/bundle"/ruby/*/bundler/gems; do
[ -d "$gem_root" ] || continue
rm -rf "$gem_root"/*/.git
rm -rf "$gem_root"/*/{doc,docs,example,examples,benchmark,benchmarks}
done
# Remove static/object files left by native builds.
find "$APP_PATH/vendor/bundle" -type f \( -name '*.a' -o -name '*.o' \) -delete
+18 -2
View File
@@ -57,16 +57,29 @@ install_plugins() {
popd >/dev/null
}
stop_memcached_daemon() {
/etc/init.d/memcached stop >/dev/null 2>&1 || true
if command -v pkill >/dev/null 2>&1; then
pkill -x memcached >/dev/null 2>&1 || true
fi
rm -f /var/run/memcached/memcached.pid
}
start_memcached_daemon() {
stop_memcached_daemon
/etc/init.d/memcached start
}
migrate() {
wait_for_postgres
pushd $APP_PATH >/dev/null
/etc/init.d/memcached start
start_memcached_daemon
echo "-----> Running migrations..."
bundle exec rake db:migrate
# run seed as app user so created attachments (and folder) belong to app, not root
echo "-----> Seeding database..."
su app -c 'bundle exec rake db:seed'
/etc/init.d/memcached stop
stop_memcached_daemon
popd >/dev/null
}
@@ -141,6 +154,9 @@ fi
echo "-----> Database setup finished."
echo " On first installation, the default admin credentials are login: admin, password: admin"
# Ensure supervisord can manage memcached itself without a stale daemon keeping port 11211 busy.
stop_memcached_daemon
echo "-----> Launching supervisord..."
erb -r uri $APP_PATH/docker/prod/supervisord.conf.erb > /etc/supervisor/supervisord.conf
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf -e ${SUPERVISORD_LOG_LEVEL}
+323
View File
@@ -0,0 +1,323 @@
#!/usr/bin/env bash
set -euo pipefail
set -x
usage() {
cat <<'USAGE'
Usage: script/ci/docker_validate_image.sh --image <image-ref> --target <slim|slim-bim|all-in-one> [--platform <docker-platform>]
Validates target-specific runtime behavior of a built docker image.
USAGE
}
log() {
printf '[docker-validate] %s\n' "$*"
}
die() {
printf '[docker-validate] ERROR: %s\n' "$*" >&2
exit 1
}
IMAGE=""
TARGET=""
PLATFORM=""
VALIDATION_PORT="${VALIDATION_PORT:-18080}"
VALIDATION_TIMEOUT_SECONDS="${VALIDATION_TIMEOUT_SECONDS:-300}"
VALIDATION_CONTAINER_NAME=""
cleanup() {
if [[ -n "${VALIDATION_CONTAINER_NAME}" ]]; then
docker rm -f "${VALIDATION_CONTAINER_NAME}" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do
case "$1" in
--image)
IMAGE="${2:-}"
shift 2
;;
--target)
TARGET="${2:-}"
shift 2
;;
--platform)
PLATFORM="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
usage
die "Unknown argument: $1"
;;
esac
done
[[ -n "${IMAGE}" ]] || { usage; die "--image is required"; }
[[ -n "${TARGET}" ]] || { usage; die "--target is required"; }
command -v docker >/dev/null 2>&1 || die "docker is required"
command -v curl >/dev/null 2>&1 || die "curl is required"
run_in_image_shell() {
local shell_script="$1"
docker run --rm --entrypoint sh "${IMAGE}" -lc "${shell_script}"
}
validate_plugin_and_runtime_basics() {
run_in_image_shell "$(cat <<'SH'
set -eu
check_present() {
if ! command -v -- "$1" >/dev/null 2>&1; then
echo "Expected command '$1' to be present"
exit 1
fi
}
check_file() {
[ -f "$1" ] || {
echo "Expected file '$1' to exist"
exit 1
}
}
check_present bin/rails
bin/rails --version >/dev/null
[ "${BUNDLE_APP_CONFIG:-}" = "/app/.bundle" ] || {
echo "Expected BUNDLE_APP_CONFIG=/app/.bundle, got '${BUNDLE_APP_CONFIG:-}'"
exit 1
}
check_file /app/.bundle/config
grep -q 'BUNDLE_PATH: "vendor/bundle"' /app/.bundle/config || {
echo "Missing BUNDLE_PATH in /app/.bundle/config"
exit 1
}
grep -q 'BUNDLE_DEPLOYMENT: "true"' /app/.bundle/config || {
echo "Missing BUNDLE_DEPLOYMENT in /app/.bundle/config"
exit 1
}
check_file /app/config/frontend_assets.manifest.json
ls /app/public/assets/frontend/*.js >/dev/null 2>&1 || {
echo "Expected compiled frontend javascript assets to exist"
exit 1
}
for plugin in budgets costs openproject-avatars openproject-documents \
openproject-github_integration openproject-gitlab_integration openproject-meeting; do
grep -q -- "$plugin" /app/public/assets/frontend/*.js || {
echo "Expected plugin '${plugin}' to be present in compiled frontend assets"
exit 1
}
done
for plugin_dir in budgets costs avatars documents github_integration gitlab_integration meeting; do
[ -d "/app/modules/${plugin_dir}/frontend/module" ] || {
echo "Expected plugin frontend module directory '/app/modules/${plugin_dir}/frontend/module'"
exit 1
}
done
check_present convert
check_present tesseract
SH
)"
}
validate_slim_pruning() {
run_in_image_shell "$(cat <<'SH'
set -eu
check_absent_dir() {
[ ! -d "$1" ] || {
echo "Expected directory '$1' to be removed from slim image"
exit 1
}
}
check_present_dir() {
[ -d "$1" ] || {
echo "Expected directory '$1' to exist"
exit 1
}
}
check_absent_dir /app/frontend
check_absent_dir /app/spec
check_absent_dir /app/screenshots
check_absent_dir /app/lookbook
check_absent_dir /app/public/assets/lookbook
check_absent_dir /app/app/assets/videos/enterprise
check_present_dir /app/public/assets/enterprise
if find /app/public/assets -type f -name '*.map' | grep -q .; then
echo "Expected source maps to be removed from slim runtime assets"
exit 1
fi
if find /app/modules -mindepth 2 -maxdepth 2 -type d \
\( -name spec -o -name test -o -name tests -o -name doc -o -name docs \) | grep -q .; then
echo "Expected module test and doc folders to be removed from slim image"
exit 1
fi
SH
)"
}
validate_slim() {
validate_plugin_and_runtime_basics
validate_slim_pruning
run_in_image_shell "$(cat <<'SH'
set -eu
check_missing() {
if command -v -- "$1" >/dev/null 2>&1; then
echo "Expected command '$1' to be absent"
exit 1
fi
}
for tool in node npm gcc g++ make git svn hg; do
check_missing "$tool"
done
SH
)"
}
validate_slim_bim() {
validate_plugin_and_runtime_basics
validate_slim_pruning
run_in_image_shell "$(cat <<'SH'
set -eu
check_present() {
if ! command -v -- "$1" >/dev/null 2>&1; then
echo "Expected command '$1' to be present"
exit 1
fi
}
check_missing() {
if command -v -- "$1" >/dev/null 2>&1; then
echo "Expected command '$1' to be absent"
exit 1
fi
}
[ "${OPENPROJECT_EDITION:-}" = "bim" ] || {
echo "Expected OPENPROJECT_EDITION=bim, got '${OPENPROJECT_EDITION:-}'"
exit 1
}
for tool in node npm IfcConvert COLLADA2GLTF xeokit-metadata; do
check_present "$tool"
done
for tool in gcc g++ make git svn hg; do
check_missing "$tool"
done
SH
)"
}
validate_all_in_one() {
VALIDATION_CONTAINER_NAME="openproject-validate-${RANDOM}-${RANDOM}"
local deadline=$((SECONDS + VALIDATION_TIMEOUT_SECONDS))
local api_url="http://127.0.0.1:${VALIDATION_PORT}/api/v3"
local docker_run_args=(
--name "${VALIDATION_CONTAINER_NAME}"
-d
-p "${VALIDATION_PORT}:80"
-e SUPERVISORD_LOG_LEVEL=debug
-e OPENPROJECT_LOGIN__REQUIRED=false
-e OPENPROJECT_HTTPS=false
)
if [[ -n "${PLATFORM}" ]]; then
docker_run_args+=(--platform "${PLATFORM}")
fi
docker run "${docker_run_args[@]}" "${IMAGE}"
while true; do
if curl --silent --fail "${api_url}"; then
break
fi
if (( SECONDS >= deadline )); then
docker logs "${VALIDATION_CONTAINER_NAME}" --tail 400 || true
die "Timed out waiting for ${api_url}"
fi
sleep 2
done
docker exec "${VALIDATION_CONTAINER_NAME}" sh -lc '
set -eu
command -v -- gosu >/dev/null 2>&1
gosu nobody true
[ -d /opt/hocuspocus ]
[ -x /usr/lib/postgresql/17/bin/psql ]
command -v -- node >/dev/null 2>&1
command -v -- npm >/dev/null 2>&1
secret="$(tr "\0" "\n" < /proc/1/environ | sed -n "s/^OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__SECRET=//p" | head -n 1)"
[ -n "$secret" ]
case "$secret" in
(*[!A-Za-z0-9]*)
echo "Expected auto-generated hocuspocus secret to use YAML-safe alphanumeric characters only."
exit 1
;;
esac
ps -ef | grep -F "/opt/hocuspocus" | grep -v grep >/dev/null 2>&1 || {
echo "Expected bundled hocuspocus process to be running."
exit 1
}
ps -ef | grep -F "/usr/bin/memcached" | grep -v grep >/dev/null 2>&1 || {
echo "Expected memcached process to be running."
exit 1
}
'
if docker logs "${VALIDATION_CONTAINER_NAME}" 2>&1 | grep -q "gave up: hocuspocus entered FATAL state"; then
docker logs "${VALIDATION_CONTAINER_NAME}" --tail 200 || true
die "Bundled hocuspocus failed to start in all-in-one image."
fi
if docker logs "${VALIDATION_CONTAINER_NAME}" 2>&1 | grep -q "gave up: memcached entered FATAL state"; then
docker logs "${VALIDATION_CONTAINER_NAME}" --tail 200 || true
die "Bundled memcached failed to start in all-in-one image."
fi
}
case "${TARGET}" in
slim)
log "Validating slim image (${IMAGE})"
validate_slim
;;
slim-bim)
log "Validating slim-bim image (${IMAGE})"
validate_slim_bim
;;
all-in-one)
log "Validating all-in-one image (${IMAGE})"
validate_all_in_one
;;
*)
die "Unsupported target '${TARGET}'. Expected slim, slim-bim, or all-in-one."
;;
esac
log "Validation completed successfully for target '${TARGET}'."