diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..681087b7c2 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,148 @@ +name: Docker Image + +# Builds release containers from the official zips on files.basex.org. +# +# Triggers: +# - 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: + schedule: + - cron: '30 4 * * 1' + workflow_dispatch: + inputs: + version: + 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 + 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_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 [ -n "${{ github.event.inputs.version }}" ]; then + VERSIONS='["${{ github.event.inputs.version }}"]' + elif [ "${{ github.event_name }}" = "push" ]; then + VERSIONS='["${{ github.ref_name }}"]' + else + # 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]') + fi + echo "versions=$VERSIONS" >> "$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 + 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@v7 + + - 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 }}" + 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 + echo "tags=$TAGS" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build image for smoke test + uses: docker/build-push-action@v7 + 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@v4 + 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@v7 + 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..b5f6033fb3 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,42 @@ +# 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" \ + org.opencontainers.image.description="BaseX XML database / XQuery server" + +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..b9663fe275 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#!/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 + basex -c "PASSWORD $BASEX_ADMIN_PASSWORD" +fi + +exec "$@"