From 7adc1d5cb1697140bc2397b3750089fcf6fce614 Mon Sep 17 00:00:00 2001 From: Michael Seiferle Date: Tue, 7 Jul 2026 11:54:16 +0200 Subject: [PATCH 1/6] Add Dockerfile and workflow for automated image builds --- .github/workflows/docker.yml | 130 +++++++++++++++++++++++++++++++++++ docker/Dockerfile | 41 +++++++++++ docker/docker-entrypoint.sh | 10 +++ 3 files changed, 181 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 docker/Dockerfile create mode 100644 docker/docker-entrypoint.sh diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..b482d5296c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,130 @@ +name: Docker Image + +# Builds release containers from the official zips on files.basex.org. +# +# Triggers: +# - schedule: weekly rebuild of the newest release per supported major line, +# picking up new BaseX releases and security updates of the base image. +# - push tag / workflow_dispatch: build one specific version. +on: + schedule: + - cron: '30 4 * * 1' + workflow_dispatch: + inputs: + version: + description: 'BaseX release version (e.g. 12.4)' + required: true + publish: + description: 'Push the image to the registry' + type: boolean + default: true + push: + tags: + - '[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+' + +env: + # major lines that receive weekly rebuilds + SUPPORTED_MAJORS: '12' + +jobs: + versions: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + matrix: ${{ steps.list.outputs.versions }} + newest: ${{ steps.list.outputs.newest }} + steps: + - name: Determine versions to build + id: list + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSIONS='["${{ github.event.inputs.version }}"]' + elif [ "${{ github.event_name }}" = "push" ]; then + VERSIONS='["${{ github.ref_name }}"]' + else + # newest release per supported major line, from the release index + ALL=$(curl -fs https://files.basex.org/releases/ \ + | grep -oE 'href="[0-9]+\.[0-9]+(\.[0-9]+)?/"' \ + | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | sort -uV) + VERSIONS=$(for MAJOR in $SUPPORTED_MAJORS; do + echo "$ALL" | grep "^$MAJOR\." | tail -1 + done | jq -Rnc '[inputs]') + fi + echo "versions=$VERSIONS" >> "$GITHUB_OUTPUT" + echo "newest=$(echo "$VERSIONS" | jq -r 'sort_by(split(".") | map(tonumber)) | last')" >> "$GITHUB_OUTPUT" + + docker: + needs: versions + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.versions.outputs.matrix) }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Derive tags + id: tags + run: | + # in a fork, images go to the fork owner's namespace (ghcr requires lowercase) + OWNER="${{ github.repository_owner }}" + IMAGE="ghcr.io/${OWNER,,}/basex" + VERSION="${{ matrix.version }}" + TAGS="$IMAGE:$VERSION,$IMAGE:${VERSION%%.*}" + if [ "$VERSION" = "${{ needs.versions.outputs.newest }}" ]; then + TAGS="$TAGS,$IMAGE:latest" + fi + echo "tags=$TAGS" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image for smoke test + uses: docker/build-push-action@v6 + with: + context: docker + build-args: BASEX_VERSION=${{ matrix.version }} + pull: true + load: true + tags: basex:smoke + + - name: Smoke test (HTTP server, DBA, admin password) + run: | + docker run -d --name smoke -p 8080:8080 -e BASEX_ADMIN_PASSWORD=smoke-test basex:smoke + timeout 90 sh -c 'until [ "$(docker inspect -f "{{.State.Health.Status}}" smoke)" = "healthy" ]; do sleep 2; done' + curl -fs http://localhost:8080/ > /dev/null + curl -fsL http://localhost:8080/dba > /dev/null + curl -fs -u admin:smoke-test http://localhost:8080/rest > /dev/null + docker rm -f smoke + + - name: Log in to GitHub Container Registry + if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # For Docker Hub, add a second docker/login-action with + # DOCKERHUB_USERNAME/DOCKERHUB_TOKEN secrets and extra tags below. + + - name: Build and push multi-arch image + if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }} + uses: docker/build-push-action@v6 + with: + context: docker + build-args: BASEX_VERSION=${{ matrix.version }} + platforms: linux/amd64,linux/arm64 + pull: true + push: true + tags: ${{ steps.tags.outputs.tags }} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000..d5b5f9cdf0 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,41 @@ +# Fetch stage: plain Alpine suffices (busybox wget is built in, only unzip is added). +FROM alpine:3.22 AS fetch + +# Release paths use the dotted version (12.4), the zip name the undotted one (BaseX124.zip). +ARG BASEX_VERSION=12.4 + +RUN apk add --no-cache unzip \ + && ZIP="BaseX$(echo "${BASEX_VERSION}" | tr -d '.').zip" \ + && wget -q "https://files.basex.org/releases/${BASEX_VERSION}/${ZIP}" \ + -O /tmp/basex.zip \ + && unzip -q /tmp/basex.zip -d /opt + +FROM eclipse-temurin:21-jre-alpine + +LABEL org.opencontainers.image.source="https://basex.org" + +RUN apk add --no-cache bash \ + && addgroup -S basex && adduser -S basex -G basex + +COPY --from=fetch --chown=basex:basex /opt/basex /opt/basex +COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/ + +ENV PATH=/opt/basex/bin:$PATH + +WORKDIR /opt/basex + +VOLUME ["/opt/basex/data"] + +USER basex + +# 1984: database server (client API) +# 8080: HTTP server (REST, RESTXQ, DBA) +EXPOSE 1984 8080 + +# / is served by webapp/restxq.xqm without authentification; /rest/ would require credentials. +HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \ + CMD wget -q -O /dev/null http://localhost:8080/ + +# Sets the admin password on first run (see docker-entrypoint.sh). +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["basexhttp"] diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 0000000000..2741e2c814 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +# On first run, apply the admin password from $BASEX_ADMIN_PASSWORD (if provided). +# If unset, BaseX generates a random initial password and writes it to the log. +if [ -n "$BASEX_ADMIN_PASSWORD" ] && [ ! -e data/users.xml ]; then + basex -c "PASSWORD $BASEX_ADMIN_PASSWORD" +fi + +exec "$@" From 84ad440ace0abfaaa637c0b504f3d90121b9bb15 Mon Sep 17 00:00:00 2001 From: Michael Seiferle Date: Tue, 7 Jul 2026 12:41:41 +0200 Subject: [PATCH 2/6] Docker: log to standard output by default --- docker/docker-entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 2741e2c814..b9663fe275 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/sh set -e +# Route the log to standard output (docker logs). Prepended, so additional +# options in $BASEX_JVM can still override it. +export BASEX_JVM="-Dorg.basex.LOG=stdout $BASEX_JVM" + # On first run, apply the admin password from $BASEX_ADMIN_PASSWORD (if provided). # If unset, BaseX generates a random initial password and writes it to the log. if [ -n "$BASEX_ADMIN_PASSWORD" ] && [ ! -e data/users.xml ]; then From 5208e30ac3304fcdd6e9372a014f7d1e4b4afbdc Mon Sep 17 00:00:00 2001 From: Michael Seiferle Date: Tue, 7 Jul 2026 12:42:26 +0200 Subject: [PATCH 3/6] Docker: add image description label --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d5b5f9cdf0..b5f6033fb3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -12,7 +12,8 @@ RUN apk add --no-cache unzip \ FROM eclipse-temurin:21-jre-alpine -LABEL org.opencontainers.image.source="https://basex.org" +LABEL org.opencontainers.image.source="https://basex.org" \ + org.opencontainers.image.description="BaseX XML database / XQuery server" RUN apk add --no-cache bash \ && addgroup -S basex && adduser -S basex -G basex From ef6f391c6ffb7653283ba460da2ccd6811f9fe51 Mon Sep 17 00:00:00 2001 From: Michael Seiferle Date: Tue, 7 Jul 2026 12:50:32 +0200 Subject: [PATCH 4/6] Docker workflow: weekly rebuild of all supported releases, moving tags on newest only --- .github/workflows/docker.yml | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b482d5296c..4f98a20b53 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,7 +3,7 @@ name: Docker Image # Builds release containers from the official zips on files.basex.org. # # Triggers: -# - schedule: weekly rebuild of the newest release per supported major line, +# - schedule: weekly rebuild of all releases of the supported major lines, # picking up new BaseX releases and security updates of the base image. # - push tag / workflow_dispatch: build one specific version. on: @@ -33,26 +33,35 @@ jobs: timeout-minutes: 5 outputs: matrix: ${{ steps.list.outputs.versions }} + newest_map: ${{ steps.list.outputs.newest_map }} newest: ${{ steps.list.outputs.newest }} steps: - name: Determine versions to build id: list run: | + # all published releases, and the newest one per major line + ALL=$(curl -fs https://files.basex.org/releases/ \ + | grep -oE 'href="[0-9]+\.[0-9]+(\.[0-9]+)?/"' \ + | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | sort -uV) + NEWEST_MAP=$(echo "$ALL" | jq -Rnc \ + '[inputs] | group_by(split(".")[0]) | map({(.[0] | split(".")[0]): last}) | add') if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then VERSIONS='["${{ github.event.inputs.version }}"]' elif [ "${{ github.event_name }}" = "push" ]; then VERSIONS='["${{ github.ref_name }}"]' else - # newest release per supported major line, from the release index - ALL=$(curl -fs https://files.basex.org/releases/ \ - | grep -oE 'href="[0-9]+\.[0-9]+(\.[0-9]+)?/"' \ - | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | sort -uV) + # all releases of the supported major lines, so that older + # version tags also receive base-image security updates VERSIONS=$(for MAJOR in $SUPPORTED_MAJORS; do - echo "$ALL" | grep "^$MAJOR\." | tail -1 + echo "$ALL" | grep "^$MAJOR\." done | jq -Rnc '[inputs]') fi echo "versions=$VERSIONS" >> "$GITHUB_OUTPUT" - echo "newest=$(echo "$VERSIONS" | jq -r 'sort_by(split(".") | map(tonumber)) | last')" >> "$GITHUB_OUTPUT" + echo "newest_map=$NEWEST_MAP" >> "$GITHUB_OUTPUT" + # newest release across the supported major lines ("latest" tag) + echo "newest=$(for MAJOR in $SUPPORTED_MAJORS; do + echo "$NEWEST_MAP" | jq -r --arg m "$MAJOR" '.[$m] // empty' + done | sort -V | tail -1)" >> "$GITHUB_OUTPUT" docker: needs: versions @@ -77,7 +86,15 @@ jobs: OWNER="${{ github.repository_owner }}" IMAGE="ghcr.io/${OWNER,,}/basex" VERSION="${{ matrix.version }}" - TAGS="$IMAGE:$VERSION,$IMAGE:${VERSION%%.*}" + MAJOR="${VERSION%%.*}" + TAGS="$IMAGE:$VERSION" + # moving tags only when this is actually the newest release, so that + # backfilling an old version does not drag them backwards + NEWEST_IN_MAJOR=$(echo '${{ needs.versions.outputs.newest_map }}' \ + | jq -r --arg m "$MAJOR" '.[$m] // empty') + if [ "$VERSION" = "$NEWEST_IN_MAJOR" ]; then + TAGS="$TAGS,$IMAGE:$MAJOR" + fi if [ "$VERSION" = "${{ needs.versions.outputs.newest }}" ]; then TAGS="$TAGS,$IMAGE:latest" fi From 4a4577f8c0e4346ecaad18539f5c70855e7f4793 Mon Sep 17 00:00:00 2001 From: Michael Seiferle Date: Tue, 7 Jul 2026 13:01:10 +0200 Subject: [PATCH 5/6] Docker workflow: update actions to current major versions --- .github/workflows/docker.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4f98a20b53..1e53f3321c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -77,7 +77,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Derive tags id: tags @@ -101,13 +101,13 @@ jobs: echo "tags=$TAGS" >> "$GITHUB_OUTPUT" - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build image for smoke test - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: docker build-args: BASEX_VERSION=${{ matrix.version }} @@ -126,7 +126,7 @@ jobs: - name: Log in to GitHub Container Registry if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -137,7 +137,7 @@ jobs: - name: Build and push multi-arch image if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }} - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: docker build-args: BASEX_VERSION=${{ matrix.version }} From 3afdcd1b4d4b353a62864519480226d3eb1a588e Mon Sep 17 00:00:00 2001 From: Michael Seiferle Date: Tue, 7 Jul 2026 14:30:39 +0200 Subject: [PATCH 6/6] Docker workflow: allow dispatch without version to build all releases --- .github/workflows/docker.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1e53f3321c..681087b7c2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,8 +12,8 @@ on: workflow_dispatch: inputs: version: - description: 'BaseX release version (e.g. 12.4)' - required: true + description: 'BaseX release version (e.g. 12.4); leave empty to build all supported releases' + required: false publish: description: 'Push the image to the registry' type: boolean @@ -45,13 +45,14 @@ jobs: | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | sort -uV) NEWEST_MAP=$(echo "$ALL" | jq -Rnc \ '[inputs] | group_by(split(".")[0]) | map({(.[0] | split(".")[0]): last}) | add') - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ -n "${{ github.event.inputs.version }}" ]; then VERSIONS='["${{ github.event.inputs.version }}"]' elif [ "${{ github.event_name }}" = "push" ]; then VERSIONS='["${{ github.ref_name }}"]' else - # all releases of the supported major lines, so that older - # version tags also receive base-image security updates + # schedule, or a manual run without version: all releases of the + # supported major lines, so that older version tags also receive + # base-image security updates VERSIONS=$(for MAJOR in $SUPPORTED_MAJORS; do echo "$ALL" | grep "^$MAJOR\." done | jq -Rnc '[inputs]')