diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 75f4087f1c4ca149c25de053065dad10abf09384..7f50fb65351b209db45a7e2bf670f509c9effd4f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,10 @@
+stages:
+  - build-package
+  - build-container
+
 build-deb-packages:
     image: maven:3-eclipse-temurin-17-focal
-    stage: build
+    stage: build-package
     script:
         - mvn antrun:run
         - mvn package
@@ -9,3 +13,28 @@ build-deb-packages:
     artifacts:
         paths:
             - fess.deb
+
+build-docker:
+  # Use the official docker image.
+  image: gitlab.jonasled.de/jonasled/buildx-docker:latest
+  retry: 2
+  stage: build-container
+  services:
+    - docker:dind
+  before_script:
+    - docker context create build
+    - docker buildx create build --use
+    - docker run --rm --privileged docker/binfmt:66f9012c56a8316f9244ffd7622d7c21c1f6f28d
+    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
+  # Default branch leaves tagempty (= latest tag)
+  # All other branches are tagged with the escaped branch name (commit ref slug)
+  script:
+    - |
+      if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
+        tag="latest"
+      else
+        tag="$CI_COMMIT_REF_SLUG"
+      fi
+    - docker buildx build --platform linux/amd64 --push --tag "$CI_REGISTRY_IMAGE:${tag}" .
+  dependencies:
+    - build-deb-packages
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..71850bfdb071c823b83be9ee2976bdb46bbef7aa
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,32 @@
+FROM eclipse-temurin:17-jre-focal
+
+ENV FESS_APP_TYPE docker
+
+RUN apt-get update && \
+    apt-get install -y imagemagick unoconv poppler-utils && \
+    apt-get clean && rm -rf /var/lib/apt/lists/*
+
+ARG FESS_VERSION=14.4.0-SNAPSHOT
+
+RUN groupadd -g 1001 fess && \
+    useradd -u 1001 -g fess --system --no-create-home --home /var/lib/fess fess
+
+ARG CACHEBUST=1
+COPY fess.deb /tmp/fess.deb
+RUN set -x && \
+    dpkg -i /tmp/fess.deb && \
+    rm -rf /tmp/fess.deb && \
+    mkdir /opt/fess && \
+    chown -R fess.fess /opt/fess && \
+    sed -i -e 's#FESS_CLASSPATH="$FESS_CONF_PATH:$FESS_CLASSPATH"#FESS_CLASSPATH="$FESS_OVERRIDE_CONF_PATH:$FESS_CONF_PATH:$FESS_CLASSPATH"#g' /usr/share/fess/bin/fess && \
+    echo "export FESS_APP_TYPE=$FESS_APP_TYPE" >>  /usr/share/fess/bin/fess.in.sh && \
+    echo "export FESS_OVERRIDE_CONF_PATH=/opt/fess" >>  /usr/share/fess/bin/fess.in.sh && \
+    apt-get clean && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /usr/share/fess
+EXPOSE 8080
+
+USER root
+COPY run.sh /usr/share/fess/run.sh
+RUN chmod +x /usr/share/fess/run.sh
+ENTRYPOINT /usr/share/fess/run.sh
diff --git a/run.sh b/run.sh
new file mode 100644
index 0000000000000000000000000000000000000000..9c1b55a85336fceef26a3b0180cacf3bae5179a0
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+
+if [[ "x${FESS_DICTIONARY_PATH}" != "x" ]] ; then
+  sed -i -e "s|^FESS_DICTIONARY_PATH=.*|FESS_DICTIONARY_PATH=${FESS_DICTIONARY_PATH}|" /etc/default/fess
+fi
+
+if [[ "x${ES_HTTP_URL}" = "x" ]] ; then
+  ES_HTTP_URL=http://localhost:9200
+else
+  sed -i -e "s|^ES_HTTP_URL=.*|ES_HTTP_URL=${ES_HTTP_URL}|" /etc/default/fess
+fi
+
+if [[ "x${ES_TYPE}" != "x" ]] ; then
+  FESS_JAVA_OPTS="${FESS_JAVA_OPTS} -Dfess.config.elasticsearch.type=${ES_TYPE}"
+fi
+
+if [[ "x${ES_USERNAME}" != "x" ]] ; then
+  FESS_JAVA_OPTS="${FESS_JAVA_OPTS} -Dfess.config.elasticsearch.username=${ES_USERNAME}"
+fi
+
+if [[ "x${ES_PASSWORD}" != "x" ]] ; then
+  FESS_JAVA_OPTS="${FESS_JAVA_OPTS} -Dfess.config.elasticsearch.password=${ES_PASSWORD}"
+fi
+
+if [[ "x${FESS_JAVA_OPTS}" != "x" ]] ; then
+  echo "FESS_JAVA_OPTS=\"${FESS_JAVA_OPTS}\"" >> /etc/default/fess
+fi
+
+if [[ "x${PING_RETRIES}" = "x" ]] ; then
+  PING_RETRIES=3
+fi
+
+if [[ "x${PING_INTERVAL}" = "x" ]] ; then
+  PING_INTERVAL=60
+fi
+
+start_fess() {
+  ln -s /opt/java/openjdk/bin/java /usr/bin/java
+  touch /var/log/fess/fess-crawler.log \
+        /var/log/fess/fess-suggest.log \
+        /var/log/fess/fess-thumbnail.log \
+        /var/log/fess/fess-urls.log \
+        /var/log/fess/audit.log \
+        /var/log/fess/fess.log
+  chown fess:fess /var/log/fess/fess-crawler.log \
+                  /var/log/fess/fess-suggest.log \
+                  /var/log/fess/fess-thumbnail.log \
+                  /var/log/fess/fess-urls.log \
+                  /var/log/fess/audit.log \
+                  /var/log/fess/fess.log
+  tail -f /var/log/fess/*.log &
+  /etc/init.d/fess start
+}
+
+wait_app() {
+  if [[ "x${FESS_CONTEXT_PATH}" = "x" ]] ; then
+    ping_path=/json/ping
+  else
+    ping_path=${FESS_CONTEXT_PATH}/json/ping
+  fi
+  while true ; do
+    status=$(curl -w '%{http_code}\n' -s -o /dev/null "http://localhost:8080${ping_path}")
+    if [[ x"${status}" = x200 ]] ; then
+      error_count=0
+    else
+      error_count=$((error_count + 1))
+    fi
+    if [[ ${error_count} -ge ${PING_RETRIES} ]] ; then
+      echo "Fess is not available."
+      exit 1
+    fi
+    sleep ${PING_INTERVAL}
+  done
+}
+
+start_fess
+
+if [[ "x${RUN_SHELL}" = "xtrue" ]] ; then
+  /bin/bash
+else
+  wait_app
+fi