Browse Source

[mod] rework container deployment (#4764)

container.yml will run after integration.yml COMPLETES successfully and in master branch.

Style changes, cleanup and improved integration with CI by leveraging the use of
shared cache between all workflows.

* Podman is now supported to build the container images (Docker also received a refactor, merging both build and buildx)
* Container images are being built by Buildah instead of Docker BuildKit.
* Container images are tested before release.
* Splitting "modern" (amd64 & arm64) and "legacy" (armv7) arches on different Dockerfiles allowing future optimizations.
Ivan Gabaldon 6 days ago
parent
commit
d16854e67a

+ 183 - 0
.github/workflows/container.yml

@@ -0,0 +1,183 @@
+---
+name: Container
+
+# yamllint disable-line rule:truthy
+on:
+  workflow_dispatch:
+  workflow_run:
+    workflows:
+      - Integration
+    types:
+      - completed
+    branches:
+      - master
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref_name }}
+  cancel-in-progress: false
+
+permissions:
+  contents: read
+  # Organization GHCR
+  packages: read
+
+env:
+  PYTHON_VERSION: "3.13"
+
+jobs:
+  build:
+    if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
+    name: Build (${{ matrix.arch }})
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - arch: amd64
+            os: ubuntu-24.04
+            emulation: false
+          - arch: arm64
+            os: ubuntu-24.04-arm
+            emulation: false
+          - arch: armv7
+            os: ubuntu-24.04-arm
+            emulation: true
+
+    permissions:
+      # Organization GHCR
+      packages: write
+
+    outputs:
+      version_string: ${{ steps.build.outputs.version_string }}
+      version_tag: ${{ steps.build.outputs.version_tag }}
+      docker_tag: ${{ steps.build.outputs.docker_tag }}
+      git_url: ${{ steps.build.outputs.git_url }}
+      git_branch: ${{ steps.build.outputs.git_branch }}
+
+    steps:
+      - name: Setup Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "${{ env.PYTHON_VERSION }}"
+
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: "false"
+
+      - name: Setup cache Python
+        uses: actions/cache@v4
+        with:
+          key: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-${{ hashFiles('./requirements*.txt') }}"
+          restore-keys: "python-${{ env.PYTHON_VERSION }}-${{ runner.arch }}-"
+          path: "./local/"
+
+      - name: Setup cache container mounts
+        uses: actions/cache@v4
+        with:
+          # yamllint disable-line rule:line-length
+          key: "container-mounts-${{ matrix.arch }}-${{ hashFiles('./container/Dockerfile ./container/legacy/Dockerfile') }}"
+          restore-keys: "container-mounts-${{ matrix.arch }}-"
+          path: |
+            /var/tmp/buildah-cache/
+            /var/tmp/buildah-cache-*/
+
+      - if: ${{ matrix.emulation }}
+        name: Setup QEMU
+        uses: docker/setup-qemu-action@v3
+
+      - name: Login to GHCR
+        uses: docker/login-action@v3
+        with:
+          registry: "ghcr.io"
+          username: "${{ github.repository_owner }}"
+          password: "${{ secrets.GITHUB_TOKEN }}"
+
+      - name: Build
+        id: build
+        env:
+          OVERRIDE_ARCH: "${{ matrix.arch }}"
+        run: make podman.build
+
+  test:
+    name: Test (${{ matrix.arch }})
+    runs-on: ${{ matrix.os }}
+    needs: build
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - arch: amd64
+            os: ubuntu-24.04
+            emulation: false
+          - arch: arm64
+            os: ubuntu-24.04-arm
+            emulation: false
+          - arch: armv7
+            os: ubuntu-24.04-arm
+            emulation: true
+
+    permissions:
+      # Organization GHCR
+      packages: write
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: "false"
+
+      - if: ${{ matrix.emulation }}
+        name: Setup QEMU
+        uses: docker/setup-qemu-action@v3
+
+      - name: Login to GHCR
+        uses: docker/login-action@v3
+        with:
+          registry: "ghcr.io"
+          username: "${{ github.repository_owner }}"
+          password: "${{ secrets.GITHUB_TOKEN }}"
+
+      - name: Test
+        env:
+          OVERRIDE_ARCH: "${{ matrix.arch }}"
+          GIT_URL: "${{ needs.build.outputs.git_url }}"
+        run: make container.test
+
+  release:
+    if: github.repository_owner == 'searxng' && github.ref_name == 'master'
+    name: Release
+    runs-on: ubuntu-24.04-arm
+    needs:
+      - build
+      - test
+
+    steps:
+      - if: env.DOCKERHUB_USERNAME != ''
+        name: Checkout
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: "false"
+
+      - if: env.DOCKERHUB_USERNAME != ''
+        name: Login to GHCR
+        uses: docker/login-action@v3
+        with:
+          registry: "ghcr.io"
+          username: "${{ github.repository_owner }}"
+          password: "${{ secrets.GITHUB_TOKEN }}"
+
+      - if: env.DOCKERHUB_USERNAME != ''
+        name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          registry: "docker.io"
+          username: "${{ env.DOCKERHUB_USERNAME }}"
+          password: "${{ secrets.DOCKERHUB_TOKEN }}"
+
+      - if: env.DOCKERHUB_USERNAME != ''
+        name: Release
+        env:
+          GIT_URL: "${{ needs.build.outputs.git_url }}"
+          DOCKER_TAG: "${{ needs.build.outputs.docker_tag }}"
+        run: make container.push

+ 0 - 46
.github/workflows/integration.yml

@@ -94,49 +94,3 @@ jobs:
 
       - name: Build
         run: make themes.all
-
-  dockers:
-    name: Docker
-    if: github.ref == 'refs/heads/master'
-    needs:
-      - test
-      - theme
-    env:
-      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
-    runs-on: ubuntu-24.04
-    steps:
-      - name: Checkout
-        if: env.DOCKERHUB_USERNAME != null
-        uses: actions/checkout@v4
-        with:
-          # make sure "make docker.push" can get the git history
-          fetch-depth: '0'
-      - name: Set up Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: '3.12'
-          architecture: 'x64'
-      - name: Cache Python dependencies
-        id: cache-python
-        uses: actions/cache@v4
-        with:
-          path: |
-            ./local
-            ./.nvm
-            ./node_modules
-          key: python-ubuntu-20.04-3.12-${{ hashFiles('requirements*.txt', 'setup.py','.nvmrc', 'package.json') }}
-      - name: Set up QEMU
-        if: env.DOCKERHUB_USERNAME != null
-        uses: docker/setup-qemu-action@v1
-      - name: Set up Docker Buildx
-        if: env.DOCKERHUB_USERNAME != null
-        uses: docker/setup-buildx-action@v1
-      - name: Login to DockerHub
-        if: env.DOCKERHUB_USERNAME != null
-        uses: docker/login-action@v1
-        with:
-          username: ${{ secrets.DOCKERHUB_USERNAME }}
-          password: ${{ secrets.DOCKERHUB_TOKEN }}
-      - name: Build and push
-        if: env.DOCKERHUB_USERNAME != null
-        run: make -e GIT_URL=$(git remote get-url origin) docker.buildx

+ 6 - 4
Makefile

@@ -54,7 +54,7 @@ ci.test: test.yamllint test.black test.types.ci  test.pylint test.unit test.robo
 test:    test.yamllint test.black test.types.dev test.pylint test.unit test.robot test.rst test.shell
 test.shell:
 	$(Q)shellcheck -x -s dash \
-		dockerfiles/docker-entrypoint.sh
+		container/docker-entrypoint.sh
 	$(Q)shellcheck -x -s bash \
 		utils/brand.sh \
 		$(MTOOLS) \
@@ -77,7 +77,9 @@ test.shell:
 MANAGE += weblate.translations.commit weblate.push.translations
 MANAGE += data.all data.traits data.useragents data.locales data.currencies
 MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean
-MANAGE += docker.build docker.push docker.buildx
+MANAGE += podman.build
+MANAGE += docker.build docker.buildx
+MANAGE += container.build container.test container.push
 MANAGE += gecko.driver
 MANAGE += node.env node.env.dev node.clean
 MANAGE += py.build py.clean
@@ -95,8 +97,8 @@ $(MANAGE):
 
 # short hands of selected targets
 
-PHONY += docs docker themes
+PHONY += docs container themes
 
 docs: docs.html
-docker:  docker.build
+container:  container.build
 themes: themes.all

+ 100 - 0
container/Dockerfile

@@ -0,0 +1,100 @@
+FROM docker.io/library/python:3.13-slim AS builder
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+    build-essential \
+    brotli \
+    # uwsgi
+    libpcre3-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /usr/local/searxng/
+
+COPY ./requirements.txt ./requirements.txt
+
+RUN --mount=type=cache,id=pip,target=/root/.cache/pip python -m venv ./venv \
+ && . ./venv/bin/activate \
+ && pip install -r requirements.txt \
+ && pip install "uwsgi~=2.0"
+
+COPY ./searx/ ./searx/
+
+ARG TIMESTAMP_SETTINGS=0
+ARG TIMESTAMP_UWSGI=0
+
+RUN python -m compileall -q searx \
+ && touch -c --date=@$TIMESTAMP_SETTINGS ./searx/settings.yml \
+ && touch -c --date=@$TIMESTAMP_UWSGI ./container/uwsgi.ini \
+ && find /usr/local/searxng/searx/static \
+    \( -name '*.html' -o -name '*.css' -o -name '*.js' -o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \
+    -type f -exec gzip -9 -k {} + -exec brotli --best {} +
+
+ARG SEARXNG_UID=977
+ARG SEARXNG_GID=977
+
+RUN grep -m1 root /etc/group > /tmp/.searxng.group \
+ && grep -m1 root /etc/passwd > /tmp/.searxng.passwd \
+ && echo "searxng:x:$SEARXNG_GID:" >> /tmp/.searxng.group \
+ && echo "searxng:x:$SEARXNG_UID:$SEARXNG_GID:searxng:/usr/local/searxng:/bin/bash" >> /tmp/.searxng.passwd
+
+FROM docker.io/library/python:3.13-slim
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+    # healthcheck
+    wget \
+    # uwsgi
+    libpcre3 \
+    libxml2 \
+    mailcap \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --chown=root:root --from=builder /tmp/.searxng.passwd /etc/passwd
+COPY --chown=root:root --from=builder /tmp/.searxng.group /etc/group
+
+ARG LABEL_DATE="0001-01-01T00:00:00Z"
+ARG GIT_URL="unspecified"
+ARG SEARXNG_GIT_VERSION="unspecified"
+ARG LABEL_VCS_REF="unspecified"
+ARG LABEL_VCS_URL="unspecified"
+
+WORKDIR /usr/local/searxng/
+
+COPY --chown=searxng:searxng --from=builder /usr/local/searxng/venv/ ./venv/
+COPY --chown=searxng:searxng --from=builder /usr/local/searxng/searx/ ./searx/
+COPY --chown=searxng:searxng ./container/ ./container/
+
+LABEL org.opencontainers.image.authors="searxng <$GIT_URL>" \
+      org.opencontainers.image.created=$LABEL_DATE \
+      org.opencontainers.image.description="A privacy-respecting, hackable metasearch engine" \
+      org.opencontainers.image.documentation="https://github.com/searxng/searxng-docker" \
+      org.opencontainers.image.licenses="AGPL-3.0-or-later" \
+      org.opencontainers.image.revision=$LABEL_VCS_REF \
+      org.opencontainers.image.source=$LABEL_VCS_URL \
+      org.opencontainers.image.title="searxng" \
+      org.opencontainers.image.url=$LABEL_VCS_URL \
+      org.opencontainers.image.version=$SEARXNG_GIT_VERSION
+
+ENV CONFIG_PATH=/etc/searxng \
+    DATA_PATH=/var/cache/searxng
+
+ENV SEARXNG_VERSION=$SEARXNG_GIT_VERSION \
+    INSTANCE_NAME=searxng \
+    AUTOCOMPLETE="" \
+    BASE_URL="" \
+    BIND_ADDRESS=[::]:8080 \
+    MORTY_KEY="" \
+    MORTY_URL="" \
+    SEARXNG_SETTINGS_PATH=$CONFIG_PATH/settings.yml \
+    UWSGI_SETTINGS_PATH=$CONFIG_PATH/uwsgi.ini \
+    UWSGI_WORKERS=%k \
+    UWSGI_THREADS=4
+
+VOLUME $CONFIG_PATH
+VOLUME $DATA_PATH
+
+EXPOSE 8080
+
+HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:8080/healthz || exit 1
+
+ENTRYPOINT ["/usr/local/searxng/container/docker-entrypoint.sh"]

+ 2 - 2
dockerfiles/docker-entrypoint.sh → container/docker-entrypoint.sh

@@ -140,14 +140,14 @@ if [ "$SEARX_CONF" -eq "1" ]; then
     cat << EOF > /etc/searx/deprecated_volume_read_me.txt
 This Docker image uses the volume /etc/searxng
 Update your configuration:
-* remove uwsgi.ini (or very carefully update your existing uwsgi.ini using https://github.com/searxng/searxng/blob/master/dockerfiles/uwsgi.ini )
+* remove uwsgi.ini (or very carefully update your existing uwsgi.ini using https://github.com/searxng/searxng/blob/master/container/uwsgi.ini )
 * mount /etc/searxng instead of /etc/searx
 EOF
 fi
 # end of searx compatibility
 
 # make sure there are uwsgi settings
-update_conf "${FORCE_CONF_UPDATE}" "${UWSGI_SETTINGS_PATH}" "/usr/local/searxng/dockerfiles/uwsgi.ini" "patch_uwsgi_settings"
+update_conf "${FORCE_CONF_UPDATE}" "${UWSGI_SETTINGS_PATH}" "/usr/local/searxng/container/uwsgi.ini" "patch_uwsgi_settings"
 
 # make sure there are searxng settings
 update_conf "${FORCE_CONF_UPDATE}" "${SEARXNG_SETTINGS_PATH}" "/usr/local/searxng/searx/settings.yml" "patch_searxng_settings"

+ 6 - 7
Dockerfile → container/legacy/Dockerfile

@@ -1,3 +1,5 @@
+# For armv7 architecture
+
 FROM docker.io/library/python:3.13-slim AS builder
 
 RUN apt-get update \
@@ -16,8 +18,7 @@ WORKDIR /usr/local/searxng/
 
 COPY ./requirements.txt ./requirements.txt
 
-# Readd on #4707 "--mount=type=cache,id=pip,target=/root/.cache/pip"
-RUN python -m venv ./venv \
+RUN --mount=type=cache,id=pip,target=/root/.cache/pip python -m venv ./venv \
  && . ./venv/bin/activate \
  && pip install -r requirements.txt \
  && pip install "uwsgi~=2.0"
@@ -29,7 +30,7 @@ ARG TIMESTAMP_UWSGI=0
 
 RUN python -m compileall -q searx \
  && touch -c --date=@$TIMESTAMP_SETTINGS ./searx/settings.yml \
- && touch -c --date=@$TIMESTAMP_UWSGI ./dockerfiles/uwsgi.ini \
+ && touch -c --date=@$TIMESTAMP_UWSGI ./container/uwsgi.ini \
  && find /usr/local/searxng/searx/static \
     \( -name '*.html' -o -name '*.css' -o -name '*.js' -o -name '*.svg' -o -name '*.ttf' -o -name '*.eot' \) \
     -type f -exec gzip -9 -k {} + -exec brotli --best {} +
@@ -69,7 +70,7 @@ WORKDIR /usr/local/searxng/
 
 COPY --chown=searxng:searxng --from=builder /usr/local/searxng/venv/ ./venv/
 COPY --chown=searxng:searxng --from=builder /usr/local/searxng/searx/ ./searx/
-COPY --chown=searxng:searxng ./dockerfiles/ ./dockerfiles/
+COPY --chown=searxng:searxng ./container/ ./container/
 
 LABEL org.opencontainers.image.authors="searxng <$GIT_URL>" \
       org.opencontainers.image.created=$LABEL_DATE \
@@ -90,8 +91,6 @@ ENV SEARXNG_VERSION=$SEARXNG_GIT_VERSION \
     AUTOCOMPLETE="" \
     BASE_URL="" \
     BIND_ADDRESS=[::]:8080 \
-    MORTY_KEY="" \
-    MORTY_URL="" \
     SEARXNG_SETTINGS_PATH=$CONFIG_PATH/settings.yml \
     UWSGI_SETTINGS_PATH=$CONFIG_PATH/uwsgi.ini \
     UWSGI_WORKERS=%k \
@@ -104,4 +103,4 @@ EXPOSE 8080
 
 HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:8080/healthz || exit 1
 
-ENTRYPOINT ["/usr/local/searxng/dockerfiles/docker-entrypoint.sh"]
+ENTRYPOINT ["/usr/local/searxng/container/docker-entrypoint.sh"]

+ 0 - 0
dockerfiles/uwsgi.ini → container/uwsgi.ini


+ 2 - 9
docs/admin/installation-docker.rst

@@ -145,13 +145,6 @@ shell inside container
    - `How to make bash scripts work in dash <http://mywiki.wooledge.org/Bashism>`_
    - `Checking for Bashisms  <https://dev.to/bowmanjd/writing-bash-scripts-that-are-not-only-bash-checking-for-bashisms-and-testing-with-dash-1bli>`_
 
-Like in many other distributions, Alpine's `/bin/sh
-<https://wiki.ubuntu.com/DashAsBinSh>`__ is :man:`dash`.  Dash is meant to be
-`POSIX-compliant <https://pubs.opengroup.org/onlinepubs/9699919799>`__.
-Compared to debian, in the Alpine image :man:`bash` is not installed.  The
-:origin:`dockerfiles/docker-entrypoint.sh` script is checked *against dash*
-(``make tests.shell``).
-
 To open a shell inside the container:
 
 .. code:: sh
@@ -188,10 +181,10 @@ Command line
    <https://docs.docker.com/engine/reference/run/#foreground>`__.
 
 In the :origin:`Dockerfile` the ENTRYPOINT_ is defined as
-:origin:`dockerfiles/docker-entrypoint.sh`
+:origin:`container/docker-entrypoint.sh`
 
 .. code:: sh
 
     docker run --rm -it searxng/searxng -h
 
-.. program-output:: ../dockerfiles/docker-entrypoint.sh -h
+.. program-output:: ../container/docker-entrypoint.sh -h

+ 4 - 87
manage

@@ -11,6 +11,9 @@ source "$(dirname "${BASH_SOURCE[0]}")/utils/lib.sh"
 # shellcheck source=utils/lib.sh
 source "$(dirname "${BASH_SOURCE[0]}")/utils/lib_nvm.sh"
 
+# shellcheck source=utils/lib_sxng_container.sh
+source "$(dirname "${BASH_SOURCE[0]}")/utils/lib_sxng_container.sh"
+
 # shellcheck source=utils/lib_sxng_data.sh
 source "$(dirname "${BASH_SOURCE[0]}")/utils/lib_sxng_data.sh"
 
@@ -77,9 +80,6 @@ docs.:
   gh-pages  : deploy on gh-pages branch
   prebuild  : build reST include files (./${DOCS_BUILD}/includes)
   clean     : clean documentation build
-docker.:
-  build     : build docker image
-  push      : build and push docker image
 gecko.driver:
   download & install geckodriver if not already installed (required for
   robot_tests)
@@ -101,6 +101,7 @@ EOF
     go.help
     node.help
     weblate.help
+    container.help
     data.help
     test.help
     themes.help
@@ -136,90 +137,6 @@ webapp.run() {
     SEARXNG_DEBUG=1 pyenv.cmd python -m searx.webapp
 }
 
-docker.push() {
-    docker.build push
-}
-
-docker.buildx() {
-    docker.build buildx
-}
-
-# shellcheck disable=SC2119
-docker.build() {
-    pyenv.install
-
-    local SEARXNG_GIT_VERSION
-    local VERSION_GITCOMMIT
-    local GITHUB_USER
-    local SEARXNG_IMAGE_NAME
-    local BUILD
-
-    build_msg DOCKER build
-    # run installation in a subprocess and activate pyenv
-
-    # See https://www.shellcheck.net/wiki/SC1001 and others ..
-    # shellcheck disable=SC2031,SC2230,SC2002,SC2236,SC2143,SC1001
-    (   set -e
-        pyenv.activate
-
-        # Check if it is a git repository
-        if [ ! -d .git ]; then
-	    die 1 "This is not Git repository"
-        fi
-        if [ ! -x "$(which git)" ]; then
-	    die 1 "git is not installed"
-        fi
-
-        if ! git remote get-url origin 2> /dev/null; then
-	    die 1 "there is no remote origin"
-        fi
-
-        # This is a git repository
-        git update-index -q --refresh
-        python -m searx.version freeze
-        eval "$(python -m searx.version)"
-
-        # Get the last git commit id
-        VERSION_GITCOMMIT=$(echo "$VERSION_TAG" | cut -d+ -f2)
-        build_msg DOCKER "Last commit : $VERSION_GITCOMMIT"
-
-        # define the docker image name
-        GITHUB_USER=$(echo "${GIT_URL}" | sed 's/.*github\.com\/\([^\/]*\).*/\1/')
-        SEARXNG_IMAGE_NAME="${SEARXNG_IMAGE_NAME:-${GITHUB_USER:-searxng}/searxng}"
-
-        BUILD="build"
-        if [ "$1" = "buildx" ]; then
-            # buildx includes the push option
-            CACHE_TAG="${SEARXNG_IMAGE_NAME}:latest-build-cache"
-            BUILD="buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push --cache-from=type=registry,ref=$CACHE_TAG --cache-to=type=registry,ref=$CACHE_TAG,mode=max"
-            shift
-        fi
-        build_msg DOCKER "Build command: ${BUILD}"
-
-        # build Docker image
-        build_msg DOCKER "Building image ${SEARXNG_IMAGE_NAME}:${SEARXNG_GIT_VERSION}"
-        # shellcheck disable=SC2086
-        docker $BUILD \
-         --build-arg BASE_IMAGE="${DEPENDENCIES_IMAGE_NAME}" \
-         --build-arg GIT_URL="${GIT_URL}" \
-         --build-arg SEARXNG_DOCKER_TAG="${DOCKER_TAG}" \
-         --build-arg SEARXNG_GIT_VERSION="${VERSION_STRING}" \
-         --build-arg VERSION_GITCOMMIT="${VERSION_GITCOMMIT}" \
-         --build-arg LABEL_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
-         --build-arg LABEL_VCS_REF="$(git rev-parse HEAD)" \
-         --build-arg LABEL_VCS_URL="${GIT_URL}" \
-         --build-arg TIMESTAMP_SETTINGS="$(git log -1 --format="%cd" --date=unix -- searx/settings.yml)" \
-         --build-arg TIMESTAMP_UWSGI="$(git log -1 --format="%cd" --date=unix -- dockerfiles/uwsgi.ini)" \
-         -t "${SEARXNG_IMAGE_NAME}:latest" -t "${SEARXNG_IMAGE_NAME}:${DOCKER_TAG}" .
-
-        if [ "$1" = "push" ]; then
-	        docker push "${SEARXNG_IMAGE_NAME}:latest"
-	        docker push "${SEARXNG_IMAGE_NAME}:${DOCKER_TAG}"
-	    fi
-    )
-    dump_return $?
-}
-
 # shellcheck disable=SC2119
 gecko.driver() {
     pyenv.install

+ 6 - 0
searx/version.py

@@ -41,6 +41,12 @@ def subprocess_run(args, **kwargs):
 
 
 def get_git_url_and_branch():
+    # handle GHA directly
+    if "GITHUB_REPOSITORY" in os.environ and "GITHUB_REF_NAME" in os.environ:
+        git_url = f"https://github.com/{os.environ['GITHUB_REPOSITORY']}"
+        git_branch = os.environ["GITHUB_REF_NAME"]
+        return git_url, git_branch
+
     try:
         ref = subprocess_run("git rev-parse --abbrev-ref @{upstream}")
     except subprocess.CalledProcessError:

+ 319 - 0
utils/lib_sxng_container.sh

@@ -0,0 +1,319 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+container.help() {
+    cat <<EOF
+container.:
+  build     : build container image
+EOF
+}
+
+CONTAINER_IMAGE_ORGANIZATION=${GITHUB_REPOSITORY_OWNER:-"searxng"}
+CONTAINER_IMAGE_NAME="searxng"
+
+container.build() {
+    local parch=${OVERRIDE_ARCH:-$(uname -m)}
+    local container_engine
+    local dockerfile
+    local arch
+    local variant
+    local platform
+
+    # Check if git is installed
+    if ! command -v git &>/dev/null; then
+        die 1 "Git is not installed"
+    fi
+
+    # Check if podman or docker is installed
+    if [ "$1" = "docker" ]; then
+        if command -v docker &>/dev/null; then
+            container_engine="docker"
+        else
+            die 1 "Docker is not installed"
+        fi
+    elif [ "$1" = "podman" ]; then
+        if command -v podman &>/dev/null; then
+            container_engine="podman"
+        else
+            die 1 "Podman is not installed"
+        fi
+    else
+        # If no explicit engine is passed, prioritize podman over docker
+        if command -v podman &>/dev/null; then
+            container_engine="podman"
+        elif command -v docker &>/dev/null; then
+            container_engine="docker"
+        else
+            die 1 "Podman/Docker is not installed"
+        fi
+    fi
+    info_msg "Selected engine: $container_engine"
+
+    # Setup arch specific
+    case $parch in
+    "X64" | "x86_64" | "amd64")
+        dockerfile="Dockerfile"
+        arch="amd64"
+        variant=""
+        platform="linux/$arch"
+        ;;
+    "ARM64" | "aarch64" | "arm64")
+        dockerfile="Dockerfile"
+        arch="arm64"
+        variant=""
+        platform="linux/$arch"
+        ;;
+    "ARMV7" | "armhf" | "armv7l" | "armv7")
+        dockerfile="legacy/Dockerfile"
+        arch="arm"
+        variant="v7"
+        platform="linux/$arch/$variant"
+        ;;
+    *)
+        err_msg "Unsupported architecture; $parch"
+        exit 1
+        ;;
+    esac
+    info_msg "Selected platform: $platform"
+
+    pyenv.install
+
+    (
+        set -e
+        pyenv.activate
+
+        # Check if it is a git repository
+        if [ ! -d .git ]; then
+            die 1 "This is not Git repository"
+        fi
+
+        if ! git remote get-url origin &>/dev/null; then
+            die 1 "There is no remote origin"
+        fi
+
+        # This is a git repository
+        git update-index -q --refresh
+        python -m searx.version freeze
+        eval "$(python -m searx.version)"
+
+        info_msg "Set \$VERSION_STRING: $VERSION_STRING"
+        info_msg "Set \$VERSION_TAG: $VERSION_TAG"
+        info_msg "Set \$DOCKER_TAG: $DOCKER_TAG"
+        info_msg "Set \$GIT_URL: $GIT_URL"
+        info_msg "Set \$GIT_BRANCH: $GIT_BRANCH"
+
+        if [ "$container_engine" = "podman" ]; then
+            params_build_builder="build --format=docker --platform=$platform --target=builder --layers --identity-label=false"
+            params_build="build --format=docker --platform=$platform --layers --squash-all --omit-history --identity-label=false"
+        else
+            params_build_builder="build --platform=$platform --target=builder"
+            params_build="build --platform=$platform --squash"
+        fi
+
+        if [ "$GITHUB_ACTIONS" = "true" ]; then
+            params_build_builder+=" --cache-from=ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache --cache-to=ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache"
+            params_build+=" --cache-from=ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache --cache-to=ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache"
+
+            # Tags
+            params_build+=" --tag=ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache:$CONTAINER_IMAGE_NAME-$arch$variant"
+        else
+            # Tags
+            params_build+=" --tag=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:latest"
+            params_build+=" --tag=localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:$DOCKER_TAG"
+        fi
+
+        # shellcheck disable=SC2086
+        "$container_engine" $params_build_builder \
+            --build-arg="TIMESTAMP_SETTINGS=$(git log -1 --format="%cd" --date=unix -- ./searx/settings.yml)" \
+            --build-arg="TIMESTAMP_UWSGI=$(git log -1 --format="%cd" --date=unix -- ./container/uwsgi.ini)" \
+            --tag="localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:builder" \
+            --file="./container/$dockerfile" \
+            .
+        build_msg CONTAINER "Image \"builder\" built"
+
+        # shellcheck disable=SC2086
+        "$container_engine" $params_build \
+            --build-arg="GIT_URL=$GIT_URL" \
+            --build-arg="SEARXNG_GIT_VERSION=$VERSION_STRING" \
+            --build-arg="LABEL_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+            --build-arg="LABEL_VCS_REF=$(git rev-parse HEAD)" \
+            --build-arg="LABEL_VCS_URL=$GIT_URL" \
+            --file="./container/$dockerfile" \
+            .
+        build_msg CONTAINER "Image built"
+
+        if [ "$GITHUB_ACTIONS" = "true" ]; then
+            "$container_engine" push "ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache:$CONTAINER_IMAGE_NAME-$arch$variant"
+
+            # Output to GHA
+            {
+                echo "version_string=$VERSION_STRING"
+                echo "version_tag=$VERSION_TAG"
+                echo "docker_tag=$DOCKER_TAG"
+                echo "git_url=$GIT_URL"
+                echo "git_branch=$GIT_BRANCH"
+            } >>"$GITHUB_OUTPUT"
+        fi
+    )
+    dump_return $?
+}
+
+container.test() {
+    local parch=${OVERRIDE_ARCH:-$(uname -m)}
+    local arch
+    local variant
+    local platform
+
+    if [ "$GITHUB_ACTIONS" != "true" ]; then
+        die 1 "This command is intended to be run in GitHub Actions"
+    fi
+
+    # Check if podman is installed
+    if ! command -v podman &>/dev/null; then
+        die 1 "podman is not installed"
+    fi
+
+    # Setup arch specific
+    case $parch in
+    "X64" | "x86_64" | "amd64")
+        arch="amd64"
+        variant=""
+        platform="linux/$arch"
+        ;;
+    "ARM64" | "aarch64" | "arm64")
+        arch="arm64"
+        variant=""
+        platform="linux/$arch"
+        ;;
+    "ARMV7" | "armhf" | "armv7l" | "armv7")
+        arch="arm"
+        variant="v7"
+        platform="linux/$arch/$variant"
+        ;;
+    *)
+        err_msg "Unsupported architecture; $parch"
+        exit 1
+        ;;
+    esac
+    build_msg CONTAINER "Selected platform: $platform"
+
+    (
+        set -e
+
+        podman pull "ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache:$CONTAINER_IMAGE_NAME-$arch$variant"
+
+        name="$CONTAINER_IMAGE_NAME-$(date +%N)"
+
+        podman create --name="$name" --rm --timeout=60 --network="host" \
+            "ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache:$CONTAINER_IMAGE_NAME-$arch$variant" >/dev/null
+
+        podman start "$name" >/dev/null
+        podman logs -f "$name" &
+        pid_logs=$!
+
+        # Wait until container is ready
+        sleep 5
+
+        curl -vf --max-time 5 "http://localhost:8080/healthz"
+
+        kill $pid_logs &>/dev/null || true
+        podman stop "$name" >/dev/null
+    )
+    dump_return $?
+}
+
+container.push() {
+    # Architectures on manifest
+    local release_archs=("amd64" "arm64" "armv7")
+
+    local archs=()
+    local variants=()
+    local platforms=()
+
+    if [ "$GITHUB_ACTIONS" != "true" ]; then
+        die 1 "This command is intended to be run in GitHub Actions"
+    fi
+
+    # Check if podman is installed
+    if ! command -v podman &>/dev/null; then
+        die 1 "podman is not installed"
+    fi
+
+    for arch in "${release_archs[@]}"; do
+        case $arch in
+        "X64" | "x86_64" | "amd64")
+            archs+=("amd64")
+            variants+=("")
+            platforms+=("linux/${archs[-1]}")
+            ;;
+        "ARM64" | "aarch64" | "arm64")
+            archs+=("arm64")
+            variants+=("")
+            platforms+=("linux/${archs[-1]}")
+            ;;
+        "ARMV7" | "armv7" | "armhf" | "arm")
+            archs+=("arm")
+            variants+=("v7")
+            platforms+=("linux/${archs[-1]}/${variants[-1]}")
+            ;;
+        *)
+            err_msg "Unsupported architecture; $arch"
+            exit 1
+            ;;
+        esac
+    done
+
+    (
+        set -e
+
+        # Pull archs
+        for i in "${!archs[@]}"; do
+            podman pull "ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache:$CONTAINER_IMAGE_NAME-${archs[$i]}${variants[$i]}"
+        done
+
+        # Manifest tags
+        release_tags=("latest")
+        release_tags+=("$DOCKER_TAG")
+
+        # Create manifests
+        for tag in "${release_tags[@]}"; do
+            if ! podman manifest exists "localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:$tag"; then
+                podman manifest create "localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:$tag"
+            fi
+
+            # Add archs to manifest
+            for i in "${!archs[@]}"; do
+                podman manifest add \
+                    "localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:$tag" \
+                    "containers-storage:ghcr.io/$CONTAINER_IMAGE_ORGANIZATION/cache:$CONTAINER_IMAGE_NAME-${archs[$i]}${variants[$i]}"
+            done
+        done
+
+        podman image list
+
+        # Push manifests
+        for tag in "${release_tags[@]}"; do
+            build_msg CONTAINER "Pushing manifest with tag: $tag"
+
+            podman manifest push \
+                "localhost/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:$tag" \
+                "docker://docker.io/$CONTAINER_IMAGE_ORGANIZATION/$CONTAINER_IMAGE_NAME:$tag"
+        done
+    )
+    dump_return $?
+}
+
+# Alias
+podman.build() {
+    container.build podman
+}
+
+# Alias
+docker.build() {
+    container.build docker
+}
+
+# Alias
+docker.buildx() {
+    container.build docker
+}