./dist
./scripts
./resources
+# Other stuff
+**/*.drawio.png
-
name: Configure ImageMagick
run: |
- sudo cp docker/imagemagick-policy.xml /etc/ImageMagick-6/policy.xml
+ sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml
-
name: Install Python dependencies
run: |
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001", "PTH"] # TODO PTH Enable & remove
"src/paperless_tika/tests/test_live_tika.py" = ["PTH"] # TODO Enable & remove
"src/paperless_tika/tests/test_tika_parser.py" = ["PTH"] # TODO Enable & remove
+# Testing
"*/tests/*.py" = ["E501", "SIM117"]
+# Migrations
"*/migrations/*.py" = ["E501", "SIM", "T201"]
+# Docker specific
+"docker/rootfs/usr/local/bin/wait-for-redis.py" = ["INP001", "T201"]
[lint.isort]
force-single-line = true
&& echo "Generating requirement.txt" \
&& pipenv requirements > requirements.txt
+# Stage: s6-overlay-base
+# Purpose: Installs s6-overlay and rootfs
+# Comments:
+# - Don't leave anything extra in here either
+FROM docker.io/python:3.12-slim-bookworm AS s6-overlay-base
+
+WORKDIR /usr/src/s6
+
+# https://github.com/just-containers/s6-overlay#customizing-s6-overlay-behaviour
+ENV \
+ S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
+ S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
+ S6_VERBOSITY=1 \
+ PATH=/command:$PATH
+
+# Buildx provided, must be defined to use though
+ARG TARGETARCH
+ARG TARGETVARIANT
+# Lock this version
+ARG S6_OVERLAY_VERSION=3.2.0.2
+
+ARG S6_BUILD_TIME_PKGS="curl \
+ xz-utils"
+
+RUN set -eux \
+ && echo "Installing build time packages" \
+ && apt-get update \
+ && apt-get install --yes --quiet --no-install-recommends ${S6_BUILD_TIME_PKGS} \
+ && echo "Determining arch" \
+ && S6_ARCH="" \
+ && if [ "${TARGETARCH}${TARGETVARIANT}" = "amd64" ]; then S6_ARCH="x86_64"; \
+ elif [ "${TARGETARCH}${TARGETVARIANT}" = "arm64" ]; then S6_ARCH="aarch64"; fi\
+ && if [ -z "${S6_ARCH}" ]; then { echo "Error: Not able to determine arch"; exit 1; }; fi \
+ && echo "Installing s6-overlay for ${S6_ARCH}" \
+ && curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
+ "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \
+ "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" \
+ "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \
+ "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" \
+ && echo "Validating s6-archive checksums" \
+ && sha256sum --check ./*.sha256 \
+ && echo "Unpacking archives" \
+ && tar --directory / -Jxpf s6-overlay-noarch.tar.xz \
+ && tar --directory / -Jxpf s6-overlay-${S6_ARCH}.tar.xz \
+ && echo "Removing downloaded archives" \
+ && rm ./*.tar.xz \
+ && rm ./*.sha256 \
+ && echo "Cleaning up image" \
+ && apt-get --yes purge ${S6_BUILD_TIME_PKGS} \
+ && apt-get --yes autoremove --purge \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy our service defs and filesystem
+COPY ./docker/rootfs /
+
# Stage: main-app
# Purpose: The final image
# Comments:
# - Don't leave anything extra in here
-FROM docker.io/python:3.12-slim-bookworm AS main-app
+FROM s6-overlay-base AS main-app
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/"
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
&& echo "Installing jbig2enc" \
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
+ && echo "Configuring imagemagick" \
+ && cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
&& echo "Cleaning up image layer" \
&& rm --force --verbose *.deb \
- && rm --recursive --force --verbose /var/lib/apt/lists/* \
- && echo "Installing supervisor" \
- && python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor==4.2.5
+ && rm --recursive --force --verbose /var/lib/apt/lists/*
# Copy gunicorn config
# Changes very infrequently
WORKDIR /usr/src/paperless/
-COPY gunicorn.conf.py .
-
-# setup docker-specific things
-# These change sometimes, but rarely
-WORKDIR /usr/src/paperless/src/docker/
-
-COPY [ \
- "docker/imagemagick-policy.xml", \
- "docker/supervisord.conf", \
- "docker/docker-entrypoint.sh", \
- "docker/docker-prepare.sh", \
- "docker/paperless_cmd.sh", \
- "docker/wait-for-redis.py", \
- "docker/env-from-file.sh", \
- "docker/management_script.sh", \
- "docker/flower-conditional.sh", \
- "docker/install_management_commands.sh", \
- "/usr/src/paperless/src/docker/" \
-]
-
-RUN set -eux \
- && echo "Configuring ImageMagick" \
- && mv imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \
- && echo "Configuring supervisord" \
- && mkdir /var/log/supervisord /var/run/supervisord \
- && mv supervisord.conf /etc/supervisord.conf \
- && echo "Setting up Docker scripts" \
- && mv docker-entrypoint.sh /sbin/docker-entrypoint.sh \
- && chmod 755 /sbin/docker-entrypoint.sh \
- && mv docker-prepare.sh /sbin/docker-prepare.sh \
- && chmod 755 /sbin/docker-prepare.sh \
- && mv wait-for-redis.py /sbin/wait-for-redis.py \
- && chmod 755 /sbin/wait-for-redis.py \
- && mv env-from-file.sh /sbin/env-from-file.sh \
- && chmod 755 /sbin/env-from-file.sh \
- && mv paperless_cmd.sh /usr/local/bin/paperless_cmd.sh \
- && chmod 755 /usr/local/bin/paperless_cmd.sh \
- && mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \
- && chmod 755 /usr/local/bin/flower-conditional.sh \
- && echo "Installing management commands" \
- && chmod +x install_management_commands.sh \
- && ./install_management_commands.sh
+COPY --chown=1000:1000 gunicorn.conf.py /usr/src/paperless/gunicorn.conf.py
WORKDIR /usr/src/paperless/src/
# Python dependencies
# Change pretty frequently
-COPY --from=pipenv-base /usr/src/pipenv/requirements.txt ./
+COPY --chown=1000:1000 --from=pipenv-base /usr/src/pipenv/requirements.txt ./
# Packages needed only for building a few quick Python
# dependencies
&& echo "Installing build system packages" \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
- && python3 -m pip install --no-cache-dir --upgrade wheel \
+ && python3 -m pip install --upgrade wheel \
&& echo "Installing Python requirements" \
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
https://github.com/paperless-ngx/builder/releases/download/psycopg-${PSYCOPG_VERSION}/psycopg_c-${PSYCOPG_VERSION}-cp312-cp312-linux_x86_64.whl \
&& echo "Adjusting all permissions" \
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
&& echo "Collecting static files" \
- && gosu paperless python3 manage.py collectstatic --clear --no-input --link \
- && gosu paperless python3 manage.py compilemessages
+ && s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
+ && s6-setuidgid paperless python3 manage.py compilemessages
VOLUME ["/usr/src/paperless/data", \
"/usr/src/paperless/media", \
"/usr/src/paperless/consume", \
"/usr/src/paperless/export"]
-ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
+ENTRYPOINT ["/init"]
EXPOSE 8000
-CMD ["/usr/local/bin/paperless_cmd.sh"]
-
HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000" ]
+++ /dev/null
-#!/usr/bin/env bash
-
-set -e
-
-wait_for_postgres() {
- local attempt_num=1
- local -r max_attempts=5
-
- echo "Waiting for PostgreSQL to start..."
-
- local -r host="${PAPERLESS_DBHOST:-localhost}"
- local -r port="${PAPERLESS_DBPORT:-5432}"
-
- # Disable warning, host and port can't have spaces
- # shellcheck disable=SC2086
- while [ ! "$(pg_isready --host ${host} --port ${port})" ]; do
-
- if [ $attempt_num -eq $max_attempts ]; then
- echo "Unable to connect to database."
- exit 1
- else
- echo "Attempt $attempt_num failed! Trying again in 5 seconds..."
- fi
-
- attempt_num=$(("$attempt_num" + 1))
- sleep 5
- done
- echo "Connected to PostgreSQL"
-}
-
-wait_for_mariadb() {
- echo "Waiting for MariaDB to start..."
-
- local -r host="${PAPERLESS_DBHOST:=localhost}"
- local -r port="${PAPERLESS_DBPORT:=3306}"
-
- local attempt_num=1
- local -r max_attempts=5
-
- # Disable warning, host and port can't have spaces
- # shellcheck disable=SC2086
- while ! true > /dev/tcp/$host/$port; do
-
- if [ $attempt_num -eq $max_attempts ]; then
- echo "Unable to connect to database."
- exit 1
- else
- echo "Attempt $attempt_num failed! Trying again in 5 seconds..."
-
- fi
-
- attempt_num=$(("$attempt_num" + 1))
- sleep 5
- done
- echo "Connected to MariaDB"
-}
-
-wait_for_redis() {
- # We use a Python script to send the Redis ping
- # instead of installing redis-tools just for 1 thing
- if ! python3 /sbin/wait-for-redis.py; then
- exit 1
- fi
-}
-
-migrations() {
- (
- # flock is in place to prevent multiple containers from doing migrations
- # simultaneously. This also ensures that the db is ready when the command
- # of the current container starts.
- flock 200
- echo "Apply database migrations..."
- python3 manage.py migrate --skip-checks --no-input
- ) 200>"${DATA_DIR}/migration_lock"
-}
-
-django_checks() {
- # Explicitly run the Django system checks
- echo "Running Django checks"
- python3 manage.py check
-}
-
-search_index() {
-
- local -r index_version=9
- local -r index_version_file=${DATA_DIR}/.index_version
-
- if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
- echo "Search index out of date. Updating..."
- python3 manage.py document_index reindex --no-progress-bar
- echo ${index_version} | tee "${index_version_file}" >/dev/null
- fi
-}
-
-superuser() {
- if [[ -n "${PAPERLESS_ADMIN_USER}" ]]; then
- python3 manage.py manage_superuser
- fi
-}
-
-do_work() {
- if [[ "${PAPERLESS_DBENGINE}" == "mariadb" ]]; then
- wait_for_mariadb
- elif [[ -n "${PAPERLESS_DBHOST}" ]]; then
- wait_for_postgres
- fi
-
- wait_for_redis
-
- migrations
-
- django_checks
-
- search_index
-
- superuser
-
-}
-
-do_work
+++ /dev/null
-#!/usr/bin/env bash
-
-# Scans the environment variables for those with the suffix _FILE
-# When located, checks the file exists, and exports the contents
-# of the file as the same name, minus the suffix
-# This allows the use of Docker secrets or mounted files
-# to fill in any of the settings configurable via environment
-# variables
-
-set -eu
-
-for line in $(printenv)
-do
- # Extract the name of the environment variable
- env_name=${line%%=*}
- # Check if it starts with "PAPERLESS_" and ends in "_FILE"
- if [[ ${env_name} == PAPERLESS_*_FILE ]]; then
- # This should have been named different..
- if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || ${env_name} == "PAPERLESS_MODEL_FILE" ]]; then
- continue
- fi
- # Extract the value of the environment
- env_value=${line#*=}
-
- # Check the file exists
- if [[ -f ${env_value} ]]; then
-
- # Trim off the _FILE suffix
- non_file_env_name=${env_name%"_FILE"}
- echo "Setting ${non_file_env_name} from file"
-
- # Reads the value from th file
- val="$(< "${!env_name}")"
-
- # Sets the normal name to the read file contents
- export "${non_file_env_name}"="${val}"
-
- else
- echo "File ${env_value} referenced by ${env_name} doesn't exist"
- fi
- fi
-done
+++ /dev/null
-#!/usr/bin/env bash
-
-echo "Checking if we should start flower..."
-
-if [[ -n "${PAPERLESS_ENABLE_FLOWER}" ]]; then
- # Small delay to allow celery to be up first
- echo "Starting flower in 5s"
- sleep 5
- celery --app paperless flower --conf=/usr/src/paperless/src/paperless/flowerconfig.py
-else
- echo "Not starting flower"
-fi
#!/usr/bin/env bash
+# Run this script to generate the management commands again (for example if a new command is create or the template is updated)
+
set -eu
for command in decrypt_documents \
prune_audit_logs;
do
echo "installing $command..."
- sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command
- chmod +x /usr/local/bin/$command
+ sed "s/management_command/$command/g" management_script.sh >"$PWD/rootfs/usr/local/bin/$command"
+ chmod +x "$PWD/rootfs/usr/local/bin/$command"
done
-#!/usr/bin/env bash
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
set -e
-cd /usr/src/paperless/src/
-# This ensures environment is setup
-# shellcheck disable=SC1091
-source /sbin/env-from-file.sh
+cd "${PAPERLESS_SRC_DIR}"
-if [[ $(id -u) == 0 ]] ;
-then
- gosu paperless python3 manage.py management_command "$@"
-elif [[ $(id -un) == "paperless" ]] ;
-then
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py management_command "$@"
+elif [[ $(id -un) == "paperless" ]]; then
python3 manage.py management_command "$@"
else
echo "Unknown user."
+++ /dev/null
-#!/usr/bin/env bash
-
-SUPERVISORD_WORKING_DIR="${PAPERLESS_SUPERVISORD_WORKING_DIR:-$PWD}"
-rootless_args=()
-if [ "$(id -u)" == "$(id -u paperless)" ]; then
- rootless_args=(
- --user
- paperless
- --logfile
- "${SUPERVISORD_WORKING_DIR}/supervisord.log"
- --pidfile
- "${SUPERVISORD_WORKING_DIR}/supervisord.pid"
- )
-fi
-
-exec /usr/local/bin/supervisord -c /etc/supervisord.conf "${rootless_args[@]}"
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+declare -r log_prefix="[init-complete]"
+declare -r end_time=$(date +%s)
+declare -r start_time=${PAPERLESS_START_TIME_S}
+
+echo "${log_prefix} paperless-ngx docker container init completed in $(($end_time-$start_time)) seconds"
+echo "${log_prefix} Starting services"
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-complete/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[custom-init]"
+
+# Mostly borrowed from the LinuxServer.io base image
+# https://github.com/linuxserver/docker-baseimage-ubuntu/tree/bionic/root/etc/cont-init.d
+declare -r custom_script_dir="/custom-cont-init.d"
+
+# Tamper checking.
+# Don't run files which are owned by anyone except root
+# Don't run files which are writeable by others
+if [ -d "${custom_script_dir}" ]; then
+ if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 ! -user root)" ]; then
+ echo "${log_prefix} **** Potential tampering with custom scripts detected ****"
+ echo "${log_prefix} **** The folder '${custom_script_dir}' must be owned by root ****"
+ exit 0
+ fi
+ if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 -perm -o+w)" ]; then
+ echo "${log_prefix} **** The folder '${custom_script_dir}' or some of contents have write permissions for others, which is a security risk. ****"
+ echo "${log_prefix} **** Please review the permissions and their contents to make sure they are owned by root, and can only be modified by root. ****"
+ exit 0
+ fi
+
+ # Make sure custom init directory has files in it
+ if [ -n "$(/bin/ls --almost-all "${custom_script_dir}" 2>/dev/null)" ]; then
+ echo "${log_prefix} files found in ${custom_script_dir} executing"
+ # Loop over files in the directory
+ for SCRIPT in "${custom_script_dir}"/*; do
+ NAME="$(basename "${SCRIPT}")"
+ if [ -f "${SCRIPT}" ]; then
+ echo "${log_prefix} ${NAME}: executing..."
+ /command/with-contenv /bin/bash "${SCRIPT}"
+ echo "${log_prefix} ${NAME}: exited $?"
+ elif [ ! -f "${SCRIPT}" ]; then
+ echo "${log_prefix} ${NAME}: is not a file"
+ fi
+ done
+ else
+ echo "${log_prefix} no custom files found exiting..."
+ fi
+else
+ echo "${log_prefix} ${custom_script_dir} doesn't exist, nothing to do"
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-custom-init/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[env-init]"
+
+echo "${log_prefix} Checking for environment from files"
+
+if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; then
+ for FILENAME in /run/s6/container_environment/*; do
+ if [[ "${FILENAME##*/}" == PAPERLESS_*_FILE ]]; then
+ # This should have been named different..
+ if [[ ${FILENAME} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || ${FILENAME} == "PAPERLESS_MODEL_FILE" ]]; then
+ continue
+ fi
+ SECRETFILE=$(cat "${FILENAME}")
+ # Check the file exists
+ if [[ -f ${SECRETFILE} ]]; then
+ # Trim off trailing _FILE
+ FILESTRIP=${FILENAME//_FILE/}
+ # Set environment variable
+ cat "${SECRETFILE}" > "${FILESTRIP}"
+ echo "${log_prefix} ${FILESTRIP##*/} set from ${FILENAME##*/}"
+ else
+ echo "${log_prefix} cannot find secret in ${FILENAME##*/}"
+ fi
+ fi
+ done
+else
+ echo "${log_prefix} No *_FILE environment found"
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-env-file/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-folders]"
+
+declare -r export_dir="/usr/src/paperless/export"
+declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
+declare -r media_root_dir="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
+declare -r consume_dir="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
+declare -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
+
+echo "${log_prefix} Checking for folder existence"
+
+for dir in \
+ "${export_dir}" \
+ "${data_dir}" "${data_dir}/index" \
+ "${media_root_dir}" "${media_root_dir}/documents" "${media_root_dir}/documents/originals" "${media_root_dir}/documents/thumbnails" \
+ "${consume_dir}" \
+ "${tmp_dir}"; do
+ if [[ ! -d "${dir}" ]]; then
+ mkdir --parents --verbose "${dir}"
+ fi
+done
+
+echo "${log_prefix} Adjusting file and folder permissions"
+for dir in \
+ "${export_dir}" \
+ "${data_dir}" \
+ "${media_root_dir}" \
+ "${consume_dir}" \
+ "${tmp_dir}"; do
+ find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
+done
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-folders/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+declare -r log_prefix="[init-migrations]"
+declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
+
+(
+ # flock is in place to prevent multiple containers from doing migrations
+ # simultaneously. This also ensures that the db is ready when the command
+ # of the current container starts.
+ flock 200
+ echo "${log_prefix} Apply database migrations..."
+ cd "${PAPERLESS_SRC_DIR}"
+
+ if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ exec python3 manage.py migrate --skip-checks --no-input
+ else
+ exec s6-setuidgid paperless python3 manage.py migrate --skip-checks --no-input
+ fi
+
+) 200>"${data_dir}/migration_lock"
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-migrations/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+declare -r log_prefix="[init-user]"
+
+declare -r usermap_original_uid=$(id -u paperless)
+declare -r usermap_original_gid=$(id -g paperless)
+declare -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid}
+declare -r usermap_new_gid=${USERMAP_GID:-${usermap_original_gid:-$usermap_new_uid}}
+
+if [[ ${usermap_new_uid} != "${usermap_original_uid}" ]]; then
+ echo "${log_prefix} Mapping UID for paperless to $usermap_new_uid"
+ usermod --non-unique --uid "${usermap_new_uid}" paperless
+else
+ echo "${log_prefix} No UID changes for paperless"
+fi
+
+if [[ ${usermap_new_gid} != "${usermap_original_gid}" ]]; then
+ echo "${log_prefix} Mapping GID for paperless to $usermap_new_gid"
+ groupmod --non-unique --gid "${usermap_new_gid}" paperless
+else
+ echo "${log_prefix} No GID changes for paperless"
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-modify-user/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-index]"
+
+declare -r index_version=9
+declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
+declare -r index_version_file="${data_dir}/.index_version"
+
+update_index () {
+ echo "${log_prefix} Search index out of date. Updating..."
+ cd "${PAPERLESS_SRC_DIR}"
+ if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_index reindex --no-progress-bar
+ echo ${index_version} | tee "${index_version_file}" > /dev/null
+ else
+ s6-setuidgid paperless python3 manage.py document_index reindex --no-progress-bar
+ echo ${index_version} | s6-setuidgid paperless tee "${index_version_file}" > /dev/null
+ fi
+}
+
+if [[ (! -f "${index_version_file}") ]]; then
+ echo "${log_prefix} No index version file found"
+ update_index
+elif [[ $(<"${index_version_file}") != "$index_version" ]]; then
+ echo "${log_prefix} index version updated"
+ update_index
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-search-index/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-start]"
+
+echo "${log_prefix} paperless-ngx docker container starting..."
+
+# Set some directories into environment for other steps to access via environment
+# Sort of like variables for later
+printf "/usr/src/paperless/src" > /var/run/s6/container_environment/PAPERLESS_SRC_DIR
+echo $(date +%s) > /var/run/s6/container_environment/PAPERLESS_START_TIME_S
+
+# Check if we're starting as a non-root user
+if [ $(id -u) == $(id -u paperless) ]; then
+ printf "true" > /var/run/s6/container_environment/USER_IS_NON_ROOT
+ echo "${log_prefix} paperless-ngx docker container running under a user"
+else
+ echo "${log_prefix} paperless-ngx docker container starting init as root"
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-start/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-superuser]"
+
+if [[ -n "${PAPERLESS_ADMIN_USER}" ]]; then
+ echo "${log_prefix} Creating superuser..."
+ cd "${PAPERLESS_SRC_DIR}"
+
+ if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py manage_superuser
+ else
+ s6-setuidgid paperless python3 manage.py manage_superuser
+ fi
+
+ echo "${log_prefix} Superuser creation done"
+
+else
+ echo "${log_prefix} Not creating superuser"
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-superuser/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-checks]"
+
+# Explicitly run the Django system checks
+echo "${log_prefix} Running Django checks"
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py check
+else
+ s6-setuidgid paperless python3 manage.py check
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-system-checks/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-tesseract-langs]"
+
+install_languages() {
+ echo "Installing languages..."
+
+ read -ra langs <<<"$1"
+
+ # Check that it is not empty
+ if [ ${#langs[@]} -eq 0 ]; then
+ return
+ fi
+
+ # Build list of packages to install
+ to_install=()
+ for lang in "${langs[@]}"; do
+ pkg="tesseract-ocr-$lang"
+
+ if dpkg --status "$pkg" &>/dev/null; then
+ echo "${log_prefix} Package $pkg already installed!"
+ continue
+ else
+ to_install+=("$pkg")
+ fi
+ done
+
+ # Use apt only when we install packages
+ if [ ${#to_install[@]} -gt 0 ]; then
+
+ # Warn the user if they're not root, but try anyway
+ if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ echo "${log_prefix} ERROR: Unable to install language ${pkg} as non-root, startup may fail"
+ fi
+
+ apt-get --quiet update &>/dev/null
+
+ for pkg in "${to_install[@]}"; do
+ if ! apt-cache --quiet show "$pkg" &>/dev/null; then
+ echo "${log_prefix} Skipped $pkg: Package not found! :("
+ continue
+ fi
+ echo "${log_prefix} Installing package $pkg..."
+ if ! apt-get --quiet --assume-yes install "$pkg" &>/dev/null; then
+ echo "${log_prefix} Could not install $pkg"
+ exit 1
+ else
+ echo "${log_prefix} Installed $pkg"
+ fi
+ done
+
+ fi
+}
+
+echo "${log_prefix} Checking if additional teseract languages needed"
+
+# Install additional languages if specified
+if [[ -n "$PAPERLESS_OCR_LANGUAGES" ]]; then
+
+ install_languages "$PAPERLESS_OCR_LANGUAGES"
+ echo "${log_prefix} Additional packages installed"
+else
+ echo "${log_prefix} No additional installs requested"
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-tesseract-langs/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-db-wait]"
+
+wait_for_postgres() {
+ local attempt_num=1
+ local -r max_attempts=5
+
+ echo "${log_prefix} Waiting for PostgreSQL to start..."
+
+ local -r host="${PAPERLESS_DBHOST:-localhost}"
+ local -r port="${PAPERLESS_DBPORT:-5432}"
+ local -r user="${PAPERLESS_DBUSER:-paperless}"
+
+ # Disable warning, host and port can't have spaces
+ # shellcheck disable=SC2086
+ while [ ! "$(pg_isready -h ${host} -p ${port} --username ${user})" ]; do
+
+ if [ $attempt_num -eq $max_attempts ]; then
+ echo "${log_prefix} Unable to connect to database."
+ exit 1
+ else
+ echo "${log_prefix} Attempt $attempt_num failed! Trying again in 5 seconds..."
+ fi
+
+ attempt_num=$(("$attempt_num" + 1))
+ sleep 5
+ done
+ # Extra in case this is a first start
+ sleep 5
+ echo "Connected to PostgreSQL"
+}
+
+wait_for_mariadb() {
+ echo "${log_prefix} Waiting for MariaDB to start..."
+
+ local -r host="${PAPERLESS_DBHOST:=localhost}"
+ local -r port="${PAPERLESS_DBPORT:=3306}"
+
+ local attempt_num=1
+ local -r max_attempts=5
+
+ # Disable warning, host and port can't have spaces
+ # shellcheck disable=SC2086
+ while ! true > /dev/tcp/$host/$port; do
+
+ if [ $attempt_num -eq $max_attempts ]; then
+ echo "${log_prefix} Unable to connect to database."
+ exit 1
+ else
+ echo "${log_prefix} Attempt $attempt_num failed! Trying again in 5 seconds..."
+
+ fi
+
+ attempt_num=$(("$attempt_num" + 1))
+ sleep 5
+ done
+ echo "Connected to MariaDB"
+}
+
+if [[ "${PAPERLESS_DBENGINE}" == "mariadb" ]]; then
+ echo "${log_prefix} Waiting for MariaDB to report ready"
+ wait_for_mariadb
+elif [[ -n "${PAPERLESS_DBHOST}" ]]; then
+ echo "${log_prefix} Waiting for postgresql to report ready"
+ wait_for_postgres
+fi
+
+ echo "${log_prefix} Database is ready"
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-wait-for-db/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-redis-wait]"
+
+echo "${log_prefix} Waiting for Redis to report ready"
+
+# We use a Python script to send the Redis ping
+# instead of installing redis-tools just for 1 thing
+if ! python3 /usr/local/bin/wait-for-redis.py; then
+ exit 1
+else
+ echo "${log_prefix} Redis ready"
+fi
--- /dev/null
+/etc/s6-overlay/s6-rc.d/init-wait-for-redis/run
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+cd ${PAPERLESS_SRC_DIR}
+
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ exec python3 manage.py document_consumer
+else
+ exec s6-setuidgid paperless python3 manage.py document_consumer
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[svc-flower]"
+
+echo "${log_prefix} Checking if we should start flower..."
+
+if [[ -n "${PAPERLESS_ENABLE_FLOWER}" ]]; then
+ # Small delay to allow celery to be up first
+ echo "${log_prefix} Starting flower in 5s"
+ sleep 5
+ cd ${PAPERLESS_SRC_DIR}
+
+ if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ exec /usr/local/bin/celery --app paperless flower --conf=${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py
+ else
+ exec s6-setuidgid paperless /usr/local/bin/celery --app paperless flower --conf=${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py
+ fi
+
+else
+ echo "${log_prefix} Not starting flower"
+ # https://skarnet.org/software/s6/s6-svc.html
+ s6-svc -Od .
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+cd ${PAPERLESS_SRC_DIR}
+
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ exec /usr/local/bin/celery --app paperless beat --loglevel INFO
+else
+ exec s6-setuidgid paperless /usr/local/bin/celery --app paperless beat --loglevel INFO
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+cd ${PAPERLESS_SRC_DIR}
+
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ exec /usr/local/bin/gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application
+else
+ exec s6-setuidgid paperless /usr/local/bin/gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+cd ${PAPERLESS_SRC_DIR}
+
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ exec /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
+else
+ exec s6-setuidgid paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py convert_mariadb_uuid "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py decrypt_documents "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py decrypt_documents "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_archiver "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_archiver "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_create_classifier "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_create_classifier "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_exporter "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_exporter "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_fuzzy_match "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_importer "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_importer "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_index "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_index "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_renamer "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_renamer "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_retagger "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_retagger "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_sanity_checker "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_sanity_checker "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py document_thumbnails "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py document_thumbnails "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py mail_fetcher "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py mail_fetcher "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py manage_superuser "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py manage_superuser "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+set -e
+
+cd "${PAPERLESS_SRC_DIR}"
+
+if [[ $(id -u) == 0 ]]; then
+ s6-setuidgid paperless python3 manage.py prune_audit_logs "$@"
+elif [[ $(id -un) == "paperless" ]]; then
+ python3 manage.py prune_audit_logs "$@"
+else
+ echo "Unknown user."
+fi
--- /dev/null
+#!/usr/bin/env python3
+"""
+Simple script which attempts to ping the Redis broker as set in the environment for
+a certain number of times, waiting a little bit in between
+
+"""
+
+import os
+import sys
+import time
+
+import click
+from redis import Redis
+
+
+@click.command(context_settings={"show_default": True})
+@click.option(
+ "--retry-count",
+ default=5,
+ type=int,
+ help="Count of times to retry the Redis connection",
+)
+@click.option(
+ "--retry-sleep",
+ default=5,
+ type=int,
+ help="Seconds to wait between Redis connection retries",
+)
+@click.argument(
+ "redis_url",
+ type=str,
+ envvar="PAPERLESS_REDIS",
+ default="redis://localhost:6379",
+)
+def wait(redis_url: str, retry_count: int, retry_sleep: int) -> None:
+ click.echo("Waiting for Redis...")
+
+ attempt = 0
+ with Redis.from_url(url=redis_url) as client:
+ while attempt < retry_count:
+ try:
+ client.ping()
+ break
+ except Exception as e:
+ click.echo(
+ f"Redis ping #{attempt} failed.\n"
+ f"Error: {e!s}.\n"
+ f"Waiting {retry_sleep}s",
+ )
+ time.sleep(retry_sleep)
+ attempt += 1
+
+ if attempt >= retry_count:
+ click.echo(
+ "Failed to connect to redis using environment variable PAPERLESS_REDIS.",
+ )
+ sys.exit(os.EX_UNAVAILABLE)
+ else:
+ click.echo("Connected to Redis broker.")
+ sys.exit(os.EX_OK)
+
+
+if __name__ == "__main__":
+ wait()
+++ /dev/null
-[supervisord]
-nodaemon=true ; start in foreground if true; default false
-logfile=/var/log/supervisord/supervisord.log ; main log file; default $CWD/supervisord.log
-pidfile=/var/run/supervisord/supervisord.pid ; supervisord pidfile; default supervisord.pid
-logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB
-logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
-loglevel=info ; log level; default info; others: debug,warn,trace
-user=root
-
-[program:gunicorn]
-command=gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application
-user=paperless
-priority = 1
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment = HOME="/usr/src/paperless",USER="paperless"
-
-[program:consumer]
-command=python3 manage.py document_consumer
-user=paperless
-stopsignal=INT
-priority = 20
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment = HOME="/usr/src/paperless",USER="paperless"
-
-[program:celery]
-
-command = celery --app paperless worker --loglevel INFO --without-mingle --without-gossip
-user=paperless
-stopasgroup = true
-stopwaitsecs = 60
-priority = 5
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment = HOME="/usr/src/paperless",USER="paperless"
-
-[program:celery-beat]
-
-command = celery --app paperless beat --loglevel INFO
-user=paperless
-stopasgroup = true
-priority = 10
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment = HOME="/usr/src/paperless",USER="paperless"
-
-[program:celery-flower]
-command = /usr/local/bin/flower-conditional.sh
-user = paperless
-startsecs = 0
-priority = 40
-stdout_logfile=/dev/stdout
-stdout_logfile_maxbytes=0
-stderr_logfile=/dev/stderr
-stderr_logfile_maxbytes=0
-environment = HOME="/usr/src/paperless",USER="paperless"
+++ /dev/null
-#!/usr/bin/env python3
-"""
-Simple script which attempts to ping the Redis broker as set in the environment for
-a certain number of times, waiting a little bit in between
-
-"""
-
-import os
-import sys
-import time
-from typing import Final
-
-from redis import Redis
-
-if __name__ == "__main__":
- MAX_RETRY_COUNT: Final[int] = 5
- RETRY_SLEEP_SECONDS: Final[int] = 5
-
- REDIS_URL: Final[str] = os.getenv("PAPERLESS_REDIS", "redis://localhost:6379")
-
- print("Waiting for Redis...", flush=True)
-
- attempt = 0
- with Redis.from_url(url=REDIS_URL) as client:
- while attempt < MAX_RETRY_COUNT:
- try:
- client.ping()
- break
- except Exception as e:
- print(
- f"Redis ping #{attempt} failed.\n"
- f"Error: {e!s}.\n"
- f"Waiting {RETRY_SLEEP_SECONDS}s",
- flush=True,
- )
- time.sleep(RETRY_SLEEP_SECONDS)
- attempt += 1
-
- if attempt >= MAX_RETRY_COUNT:
- print("Failed to connect to redis using environment variable PAPERLESS_REDIS.")
- sys.exit(os.EX_UNAVAILABLE)
- else:
- print("Connected to Redis broker.")
- sys.exit(os.EX_OK)
#### [`PAPERLESS_SUPERVISORD_WORKING_DIR=<defined>`](#PAPERLESS_SUPERVISORD_WORKING_DIR) {#PAPERLESS_SUPERVISORD_WORKING_DIR}
-: If this environment variable is defined, the `supervisord.log` and `supervisord.pid` file will be created under the specified path in `PAPERLESS_SUPERVISORD_WORKING_DIR`. Setting `PAPERLESS_SUPERVISORD_WORKING_DIR=/tmp` and `PYTHONPYCACHEPREFIX=/tmp/pycache` would allow paperless to work on a read-only filesystem.
+!!! warning
- Please take note that the `PAPERLESS_DATA_DIR` and `PAPERLESS_MEDIA_ROOT` paths still have to be writable, just like the `PAPERLESS_SUPERVISORD_WORKING_DIR`. The can be archived by using bind or volume mounts. Only works in the container is run as user *paperless*
+ This option is deprecated and has no effect. For read only file system support,
+ see [S6_READ_ONLY_ROOT](https://github.com/just-containers/s6-overlay#customizing-s6-overlay-behaviour)
+ from s6-overlay.
## Frontend Settings