paperless: port container from Dockerfile to Nix (default.nix)

Wrap nixpkgs paperless-ngx in dockerTools.buildLayeredImage, replacing
the s6-overlay Dockerfile build. The package bundles the full OCR/imaging
closure (tesseract, qpdf, jbig2enc, unpaper, pngquant, ocrmypdf, pikepdf)
and nltk data, so the image stays lean. Unlike the s6 image, this runs as
four containers on ringtail sharing one image (web/worker/beat/consumer);
the web wrapper mirrors the NixOS module's granian + PYTHONPATH invocation.

Self-pins nixos-unstable (stable lags at 2.19.6) for paperless-ngx 2.20.15
-- a same-minor forward patch bump from the v2.20.13 Dockerfile build.
Build verified on ringtail (nix-build, exit 0, version assert passes).

Also fixes pre-existing shower version drift (service-versions 1.1.2 ->
1.1.3 to match its default.nix) so container-version-check passes; the
paperless service-versions edit widens that check to all containers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-03 08:39:07 -07:00
commit 43047423c4
3 changed files with 86 additions and 160 deletions

View file

@ -1,156 +0,0 @@
# syntax=docker/dockerfile:1
# Paperless-ngx — self-hosted document management
# Built from source via forge mirror of paperless-ngx/paperless-ngx
# Closely follows upstream Dockerfile structure with git clone instead of COPY
ARG CONTAINER_APP_VERSION=v2.20.13
###############################################
# Stage 1: Clone source (reused by later stages)
###############################################
FROM docker.io/library/alpine:3.22 AS source
ARG CONTAINER_APP_VERSION
RUN apk add --no-cache git
RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \
https://forge.ops.eblu.me/mirrors/paperless-ngx.git /src
###############################################
# Stage 2: Compile frontend
###############################################
FROM --platform=$BUILDPLATFORM docker.io/node:20-trixie-slim AS compile-frontend
COPY --from=source /src/src-ui /src/src-ui
WORKDIR /src/src-ui
RUN set -eux \
&& npm update -g pnpm \
&& npm install -g corepack@latest \
&& corepack enable \
&& pnpm install
RUN set -eux \
&& ./node_modules/.bin/ng build --configuration production
###############################################
# Stage 3: s6-overlay base
###############################################
FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6
ENV S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_VERBOSITY=1 \
PATH=/command:$PATH
ARG TARGETARCH
ARG TARGETVARIANT
ARG S6_OVERLAY_VERSION=3.2.1.0
RUN set -eux \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends curl xz-utils \
&& 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: Cannot determine arch"; exit 1; fi \
&& curl --fail --silent --show-error --location --remote-name-all --parallel \
"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" \
&& sha256sum --check ./*.sha256 \
&& tar --directory / -Jxpf s6-overlay-noarch.tar.xz \
&& tar --directory / -Jxpf s6-overlay-${S6_ARCH}.tar.xz \
&& rm ./*.tar.xz ./*.sha256 \
&& apt-get --yes purge curl xz-utils \
&& apt-get --yes autoremove --purge \
&& rm -rf /var/lib/apt/lists/*
# Copy rootfs (s6 service definitions, init scripts)
COPY --from=source /src/docker/rootfs /
###############################################
# Stage 4: Main application
###############################################
FROM s6-overlay-base AS main-app
ARG CONTAINER_APP_VERSION
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETARCH
ARG JBIG2ENC_VERSION=0.30
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONWARNINGS="ignore:::django.http.response:517" \
PNGX_CONTAINERIZED=1 \
UV_LINK_MODE=copy \
UV_CACHE_DIR=/cache/uv/
# Runtime packages
RUN set -eux \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends \
curl gosu tzdata fonts-liberation gettext ghostscript gnupg \
icc-profiles-free imagemagick postgresql-client \
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
tesseract-ocr-ita tesseract-ocr-spa unpaper pngquant jbig2dec \
libxml2 libxslt1.1 qpdf file libmagic1 media-types zlib1g \
libzbar0 poppler-utils \
&& curl --fail --silent --show-error --location --remote-name-all \
"https://github.com/paperless-ngx/builder/releases/download/jbig2enc-trixie-v${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb" \
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
&& rm --force *.deb \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/paperless/src/
# Python dependencies
COPY --from=source /src/pyproject.toml /src/uv.lock /usr/src/paperless/src/
RUN --mount=type=cache,target=${UV_CACHE_DIR},id=python-cache \
set -eux \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends \
build-essential default-libmysqlclient-dev pkg-config \
&& uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \
&& uv pip install --system --no-python-downloads --python-preference system --requirements requirements.txt \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt_tab \
&& apt-get --yes purge build-essential default-libmysqlclient-dev pkg-config \
&& apt-get --yes autoremove --purge \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Copy backend source
COPY --from=source /src/src ./
# Copy compiled frontend
COPY --from=compile-frontend /src/src/documents/static/frontend/ ./documents/static/frontend/
# Create user and finalize
RUN set -eux \
&& addgroup --gid 1000 paperless \
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
&& mkdir -p /usr/src/paperless/data /usr/src/paperless/media \
/usr/src/paperless/consume /usr/src/paperless/export \
&& chown -R paperless:paperless /usr/src/paperless \
&& 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 ["/init"]
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=5 \
CMD [ "curl", "-fs", "-S", "-L", "--max-time", "2", "http://localhost:8000" ]
LABEL org.opencontainers.image.title="Paperless-ngx"
LABEL org.opencontainers.image.description="Self-hosted document management system"
LABEL org.opencontainers.image.version="${CONTAINER_APP_VERSION}"
LABEL org.opencontainers.image.source="https://forge.eblu.me/eblume/blumeops"
LABEL org.opencontainers.image.vendor="blumeops"

View file

@ -0,0 +1,77 @@
# Nix-built Paperless-ngx for ringtail (amd64).
#
# Replaces the from-source Dockerfile build (s6-overlay) with nixpkgs'
# paperless-ngx, which already bundles the full OCR/imaging closure
# (tesseract, ghostscript, imagemagick, qpdf, poppler, jbig2enc) and the
# NLTK data via wrappers — so the image stays lean.
#
# Unlike the upstream s6 image, this image does NOT run all processes
# itself. Paperless is multi-process; on ringtail it runs as four
# containers sharing this one image, each with a different command:
# web -> paperless-web (granian, the wrapper below)
# worker -> celery --app paperless worker
# beat -> celery --app paperless beat
# consumer -> paperless-ngx document_consumer
# plus a redis/valkey sidecar. The PYTHONPATH/granian invocation mirrors
# the nixpkgs paperless NixOS module's paperless-web service exactly.
#
# Self-pins nixos-unstable: stable nixpkgs lags at 2.19.6, while unstable
# carries 2.20.15 — a same-minor forward patch bump from the previous
# Dockerfile build (v2.20.13). The version assertion makes nix-build fail
# if a pin bump changes the version, forcing an explicit acknowledgment
# here and in service-versions.yaml (enforced by container-version-check).
let
nixpkgs = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/331800de5053fcebacf6813adb5db9c9dca22a0c.tar.gz";
sha256 = "1p54fm6dkbq62kpi55cr4wyx7b1nsajpsnjgs64cmp073fwi15f7";
};
pkgs = import nixpkgs { system = "x86_64-linux"; };
version = "2.20.15";
app = pkgs.paperless-ngx;
# Mirror the NixOS module's paperless-web service: granian serving the
# ASGI app with the package's propagated deps + src on PYTHONPATH.
pythonPath =
"${app.python.pkgs.makePythonPath app.propagatedBuildInputs}:${app}/lib/paperless-ngx/src";
paperless-web = pkgs.writeShellScriptBin "paperless-web" ''
export PYTHONPATH="${pythonPath}"
export PAPERLESS_NLTK_DIR="${app.nltkDataDir}"
exec ${app.python.pkgs.granian}/bin/granian \
--interface asginl --ws \
--host 0.0.0.0 --port 8000 \
"paperless.asgi:application"
'';
in
assert app.version == version;
pkgs.dockerTools.buildLayeredImage {
name = "blumeops/paperless";
contents = [
app
paperless-web
pkgs.bashInteractive
pkgs.coreutils
pkgs.cacert
pkgs.tzdata
];
config = {
# Default command is the web server; worker/beat/consumer containers
# override `command` in their k8s manifests.
Cmd = [ "${paperless-web}/bin/paperless-web" ];
Env = [
"PAPERLESS_NLTK_DIR=${app.nltkDataDir}"
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
"PYTHONUNBUFFERED=1"
"PNGX_CONTAINERIZED=1"
];
ExposedPorts = {
"8000/tcp" = { };
};
};
}

View file

@ -47,7 +47,7 @@ services:
- name: shower - name: shower
type: argocd type: argocd
last-reviewed: 2026-05-15 last-reviewed: 2026-05-15
current-version: "1.1.2" current-version: "1.1.3"
upstream-source: https://forge.eblu.me/eblume/adelaide-baby-shower-app upstream-source: https://forge.eblu.me/eblume/adelaide-baby-shower-app
notes: | notes: |
Django app for Adelaide / Heidi / Addie's baby shower. Wheel Django app for Adelaide / Heidi / Addie's baby shower. Wheel
@ -339,10 +339,15 @@ services:
- name: paperless - name: paperless
type: argocd type: argocd
last-reviewed: "2026-04-08" last-reviewed: "2026-06-03"
current-version: "v2.20.13" current-version: "v2.20.15"
upstream-source: https://github.com/paperless-ngx/paperless-ngx/releases upstream-source: https://github.com/paperless-ngx/paperless-ngx/releases
notes: Document management; built from source via forge mirror notes: >-
Document management. Container ported from Dockerfile to Nix
(containers/paperless/default.nix wraps nixpkgs paperless-ngx from a
pinned nixos-unstable). Runs as web/worker/beat/consumer containers on
ringtail (multi-process; no s6). Bumped v2.20.13 -> v2.20.15 (the
unstable package version, same-minor patch) as part of the port.
- name: unpoller - name: unpoller
type: argocd type: argocd