mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
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:
@@ -41,5 +41,7 @@ frontend/node_modules
|
|||||||
node_modules
|
node_modules
|
||||||
# travis
|
# travis
|
||||||
vendor/bundle
|
vendor/bundle
|
||||||
|
# Local checkout; all-in-one copies hocuspocus from its dedicated image.
|
||||||
|
vendor/hocuspocus
|
||||||
/public/assets
|
/public/assets
|
||||||
/config/frontend_assets.manifest.json
|
/config/frontend_assets.manifest.json
|
||||||
|
|||||||
@@ -255,23 +255,12 @@ jobs:
|
|||||||
if [ -d vendor/bundle.bak ]; then
|
if [ -d vendor/bundle.bak ]; then
|
||||||
mv vendor/bundle.bak vendor/bundle
|
mv vendor/bundle.bak vendor/bundle
|
||||||
fi
|
fi
|
||||||
- name: Test
|
- name: Validate image
|
||||||
# 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'
|
|
||||||
run: |
|
run: |
|
||||||
docker run \
|
./script/ci/docker_validate_image.sh \
|
||||||
--name openproject \
|
--image "${{ steps.build.outputs.imageid }}" \
|
||||||
-d -p 8080:80 --platform ${{ matrix.platform }} \
|
--target "${{ matrix.target }}" \
|
||||||
-e SUPERVISORD_LOG_LEVEL=debug \
|
--platform "${{ matrix.platform }}"
|
||||||
-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
|
|
||||||
- name: Push image
|
- name: Push image
|
||||||
id: push
|
id: push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
|||||||
+41
-7
@@ -3,7 +3,7 @@ ARG DEBIAN_BASE="trixie"
|
|||||||
# Add SBOM scan context for intermediate steps
|
# Add SBOM scan context for intermediate steps
|
||||||
ARG BUILDKIT_SBOM_SCAN_CONTEXT=true
|
ARG BUILDKIT_SBOM_SCAN_CONTEXT=true
|
||||||
ARG BUILDKIT_SBOM_SCAN_STAGE=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"
|
LABEL maintainer="operations@openproject.com"
|
||||||
|
|
||||||
ARG NODE_VERSION="22.21.0"
|
ARG NODE_VERSION="22.21.0"
|
||||||
@@ -19,6 +19,8 @@ ENV DOCKER=1
|
|||||||
ENV APP_USER=app
|
ENV APP_USER=app
|
||||||
ENV APP_PATH=/app
|
ENV APP_PATH=/app
|
||||||
ENV APP_DATA_PATH=/var/openproject/assets
|
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="17"
|
||||||
ENV PGVERSION_CHOICES="13 15 17"
|
ENV PGVERSION_CHOICES="13 15 17"
|
||||||
ENV PGBIN="/usr/lib/postgresql/$PGVERSION/bin"
|
ENV PGBIN="/usr/lib/postgresql/$PGVERSION/bin"
|
||||||
@@ -54,10 +56,19 @@ WORKDIR $APP_PATH
|
|||||||
# upgrade bundler
|
# upgrade bundler
|
||||||
RUN gem install bundler --no-document
|
RUN gem install bundler --no-document
|
||||||
|
|
||||||
# system dependencies, nodejs
|
# runtime dependencies
|
||||||
COPY ./docker/prod/setup/preinstall-common.sh ./docker/prod/setup/preinstall-common.sh
|
COPY ./docker/prod/setup/preinstall-common.sh ./docker/prod/setup/preinstall-common.sh
|
||||||
RUN ./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
|
# stuff required for gems
|
||||||
COPY Gemfile Gemfile.* .ruby-version ./
|
COPY Gemfile Gemfile.* .ruby-version ./
|
||||||
COPY modules ./modules
|
COPY modules ./modules
|
||||||
@@ -73,15 +84,32 @@ COPY . .
|
|||||||
|
|
||||||
# Copy lock file again as the updated version was overriden by COPY just now
|
# 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 && \
|
RUN cp Gemfile.lock.bak Gemfile.lock && rm Gemfile.lock.bak && \
|
||||||
./docker/prod/setup/precompile-assets.sh && \
|
./docker/prod/setup/precompile-assets.sh
|
||||||
./docker/prod/setup/postinstall-common.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 && \
|
cp ./config/database.production.yml config/database.yml && \
|
||||||
ln -s $APP_PATH/docker/prod/setup/.irbrc /home/$APP_USER/
|
ln -s $APP_PATH/docker/prod/setup/.irbrc /home/$APP_USER/
|
||||||
|
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
# slim (public)
|
# slim (public)
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
FROM base AS slim
|
FROM app-runtime-slim AS slim
|
||||||
|
|
||||||
USER $APP_USER
|
USER $APP_USER
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
@@ -93,7 +121,7 @@ VOLUME ["$APP_DATA_PATH"]
|
|||||||
# slim-bim (public)
|
# slim-bim (public)
|
||||||
# same as slim but with BIM support enabled
|
# same as slim but with BIM support enabled
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
FROM base AS slim-bim
|
FROM app-runtime-slim AS slim-bim
|
||||||
|
|
||||||
USER $APP_USER
|
USER $APP_USER
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
@@ -104,7 +132,7 @@ ENV OPENPROJECT_EDITION=bim
|
|||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
# all-in-one (public)
|
# all-in-one (public)
|
||||||
# -------------------------------------
|
# -------------------------------------
|
||||||
FROM base AS all-in-one
|
FROM app-runtime AS all-in-one
|
||||||
|
|
||||||
ENV OPENPROJECT_RAILS__CACHE__STORE=memcache
|
ENV OPENPROJECT_RAILS__CACHE__STORE=memcache
|
||||||
ENV DATABASE_URL=postgres://openproject:openproject@127.0.0.1/openproject
|
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
|
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
|
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 && \
|
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/
|
ln -s /app/docker/prod/setup/.irbrc /root/
|
||||||
|
|
||||||
# Expose ports for apache and postgres
|
# Expose ports for apache and postgres
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ if [ "$(id -u)" = '0' ]; then
|
|||||||
|
|
||||||
HP_HOST=${OPENPROJECT_HOST__NAME:="localhost"}
|
HP_HOST=${OPENPROJECT_HOST__NAME:="localhost"}
|
||||||
export OPENPROJECT_COLLABORATIVE__EDITING__HOCUSPOCUS__URL="${HP_PROTOCOL}://${HP_HOST}/hocuspocus"
|
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
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -10,3 +10,8 @@ cp Gemfile.lock Gemfile.lock.bak
|
|||||||
rm -rf vendor/bundle/ruby/*/cache
|
rm -rf vendor/bundle/ruby/*/cache
|
||||||
rm -rf vendor/bundle/ruby/*/gems/*/spec
|
rm -rf vendor/bundle/ruby/*/gems/*/spec
|
||||||
rm -rf vendor/bundle/ruby/*/gems/*/test
|
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
|
||||||
|
|||||||
Executable
+40
@@ -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
|
||||||
@@ -20,7 +20,6 @@ get_architecture() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
set -exo pipefail
|
|
||||||
ARCHITECTURE=$(get_architecture)
|
ARCHITECTURE=$(get_architecture)
|
||||||
|
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
@@ -28,21 +27,18 @@ apt-get update -qq
|
|||||||
apt-get upgrade -y
|
apt-get upgrade -y
|
||||||
|
|
||||||
apt-get install -yq --no-install-recommends \
|
apt-get install -yq --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
wget \
|
wget \
|
||||||
file \
|
file \
|
||||||
gnupg2 \
|
gnupg2 \
|
||||||
lsb-release
|
lsb-release
|
||||||
|
|
||||||
# install node + npm
|
wget --quiet -O- https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg -
|
||||||
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
|
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
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
apt-get install -yq --no-install-recommends \
|
apt-get install -yq --no-install-recommends \
|
||||||
libpq-dev \
|
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libffi8 \
|
libffi8 \
|
||||||
unrtf \
|
unrtf \
|
||||||
@@ -50,11 +46,7 @@ apt-get install -yq --no-install-recommends \
|
|||||||
poppler-utils \
|
poppler-utils \
|
||||||
catdoc \
|
catdoc \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
libclang-dev \
|
libjemalloc2
|
||||||
libjemalloc2 \
|
|
||||||
git \
|
|
||||||
build-essential \
|
|
||||||
libyaml-dev \
|
|
||||||
|
|
||||||
for version in $PGVERSION_CHOICES ; do
|
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
|
||||||
@@ -62,7 +54,10 @@ done
|
|||||||
|
|
||||||
# Specifics for BIM edition
|
# Specifics for BIM edition
|
||||||
if [ ! "$BIM_SUPPORT" = "false" ]; then
|
if [ ! "$BIM_SUPPORT" = "false" ]; then
|
||||||
apt-get install -y wget unzip
|
apt-get install -yq --no-install-recommends unzip
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
tmpdir=$(mktemp -d)
|
tmpdir=$(mktemp -d)
|
||||||
cd $tmpdir
|
cd $tmpdir
|
||||||
@@ -92,5 +87,16 @@ fi
|
|||||||
|
|
||||||
id $APP_USER || useradd -d /home/$APP_USER -m $APP_USER
|
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/*
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
truncate -s 0 /var/log/*log
|
truncate -s 0 /var/log/*log
|
||||||
|
|||||||
Executable
+38
@@ -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
@@ -57,16 +57,29 @@ install_plugins() {
|
|||||||
popd >/dev/null
|
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() {
|
migrate() {
|
||||||
wait_for_postgres
|
wait_for_postgres
|
||||||
pushd $APP_PATH >/dev/null
|
pushd $APP_PATH >/dev/null
|
||||||
/etc/init.d/memcached start
|
start_memcached_daemon
|
||||||
echo "-----> Running migrations..."
|
echo "-----> Running migrations..."
|
||||||
bundle exec rake db:migrate
|
bundle exec rake db:migrate
|
||||||
# run seed as app user so created attachments (and folder) belong to app, not root
|
# run seed as app user so created attachments (and folder) belong to app, not root
|
||||||
echo "-----> Seeding database..."
|
echo "-----> Seeding database..."
|
||||||
su app -c 'bundle exec rake db:seed'
|
su app -c 'bundle exec rake db:seed'
|
||||||
/etc/init.d/memcached stop
|
stop_memcached_daemon
|
||||||
popd >/dev/null
|
popd >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +154,9 @@ fi
|
|||||||
echo "-----> Database setup finished."
|
echo "-----> Database setup finished."
|
||||||
echo " On first installation, the default admin credentials are login: admin, password: admin"
|
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..."
|
echo "-----> Launching supervisord..."
|
||||||
erb -r uri $APP_PATH/docker/prod/supervisord.conf.erb > /etc/supervisor/supervisord.conf
|
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}
|
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf -e ${SUPERVISORD_LOG_LEVEL}
|
||||||
|
|||||||
Executable
+323
@@ -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}'."
|
||||||
Reference in New Issue
Block a user