Run Docker images as non-root user and default to /data workdir
Harden the Docker images by dropping root privileges, and make the bind-mount workflow less fiddly. - Create a non-root `app` user (uid/gid 1000) in both images and add `USER app` before the entrypoint, so ocrmypdf (and the webservice/watcher) no longer run as root. This also fixes the previously dangling `--chown=app:app`, which referenced a user that was never created. The Ubuntu base ships a default `ubuntu`/1000 user, so remove it first so `app` can take uid 1000 (parity with Alpine). - Add `WORKDIR /data` (created and app-owned) so bind-mounted input and output can be passed as relative paths without `--workdir`. The webservice/watcher are now invoked by absolute path (`/app/*.py`) since the working directory is no longer `/app`. - Drop the redundant `ppa:alex-p/tesseract-ocr5` from the Ubuntu image: Tesseract 5 ships in the Ubuntu archive as of 24.04, and the PPA had no build for the 26.04 base, which broke the build outright. - Rewrite docs/docker.md rootless-first: stdin/stdout piping as the recommended permission-free path, then per-runtime volume guidance (rootless Docker `--user 0:0`, Podman `--userns keep-id`, rootful Docker as the special case). Update batch.md and the compose example to match (absolute script paths, per-runtime `user:` guidance).
This commit is contained in:
+27
-5
@@ -60,10 +60,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
|
||||
FROM base
|
||||
|
||||
RUN apt-get update && apt-get install -y software-properties-common
|
||||
|
||||
RUN add-apt-repository -y ppa:alex-p/tesseract-ocr5
|
||||
|
||||
# Tesseract 5 ships in the Ubuntu archive as of 24.04, so no third-party PPA is
|
||||
# needed. (Previously this used ppa:alex-p/tesseract-ocr5.)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ghostscript \
|
||||
fonts-droid-fallback \
|
||||
@@ -81,6 +79,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
unpaper \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create a non-root user to run the application (defense in depth). The build
|
||||
# stages above need root to install packages, but the entrypoint should not.
|
||||
# A fixed uid/gid of 1000 keeps `--user`/`--userns keep-id` mappings predictable
|
||||
# and matches the --chown below. See docs/docker.md for the volume/permissions
|
||||
# implications under rootless vs rootful Docker.
|
||||
# The Ubuntu base ships a default "ubuntu" user at uid/gid 1000; remove it so
|
||||
# "app" can claim that uid for parity with the Alpine image.
|
||||
RUN userdel -r ubuntu 2>/dev/null; groupdel ubuntu 2>/dev/null; \
|
||||
groupadd -g 1000 app \
|
||||
&& useradd -u 1000 -g app -m -d /home/app app
|
||||
ENV HOME=/home/app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /usr/local/lib/ /usr/local/lib/
|
||||
@@ -90,9 +100,21 @@ COPY --from=builder --chown=app:app /app /app
|
||||
|
||||
RUN rm -rf /app/.git && \
|
||||
ln -s /app/misc/webservice.py /app/webservice.py && \
|
||||
ln -s /app/misc/watcher.py /app/watcher.py
|
||||
ln -s /app/misc/watcher.py /app/watcher.py && \
|
||||
chown app:app /app
|
||||
|
||||
# Default working directory for bind-mounted data, so relative input/output
|
||||
# paths work without passing --workdir (e.g. `-v "$PWD:/data" in.pdf out.pdf`).
|
||||
# The webservice/watcher are run by absolute path (/app/*.py), unaffected by this.
|
||||
RUN mkdir -p /data && chown app:app /data
|
||||
WORKDIR /data
|
||||
|
||||
ENV PATH="/app/.venv/bin:${PATH}"
|
||||
|
||||
# Drop privileges: run the entrypoint (ocrmypdf, or the webservice/watcher when
|
||||
# overridden) as the unprivileged app user. Override with `--user root` if you
|
||||
# need root inside a running container (e.g. to apt install extra packages).
|
||||
USER app
|
||||
|
||||
ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
|
||||
|
||||
|
||||
@@ -62,14 +62,35 @@ RUN apk add --no-cache \
|
||||
unpaper \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Create a non-root user to run the application (defense in depth). The build
|
||||
# stages above need root to install packages, but the entrypoint should not.
|
||||
# A fixed uid/gid of 1000 keeps `--user`/`--userns keep-id` mappings predictable
|
||||
# and matches the --chown below. See docs/docker.md for the volume/permissions
|
||||
# implications under rootless vs rootful Docker.
|
||||
RUN addgroup -g 1000 app \
|
||||
&& adduser -u 1000 -G app -D -h /home/app app
|
||||
ENV HOME=/home/app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder --chown=app:app /app /app
|
||||
|
||||
RUN rm -rf /app/.git && \
|
||||
ln -s /app/misc/webservice.py /app/webservice.py && \
|
||||
ln -s /app/misc/watcher.py /app/watcher.py
|
||||
ln -s /app/misc/watcher.py /app/watcher.py && \
|
||||
chown app:app /app
|
||||
|
||||
# Default working directory for bind-mounted data, so relative input/output
|
||||
# paths work without passing --workdir (e.g. `-v "$PWD:/data" in.pdf out.pdf`).
|
||||
# The webservice/watcher are run by absolute path (/app/*.py), unaffected by this.
|
||||
RUN mkdir -p /data && chown app:app /data
|
||||
WORKDIR /data
|
||||
|
||||
ENV PATH="/app/.venv/bin:${PATH}"
|
||||
|
||||
# Drop privileges: run the entrypoint (ocrmypdf, or the webservice/watcher when
|
||||
# overridden) as the unprivileged app user. Override with `--user root` if you
|
||||
# need root inside a running container (e.g. to apk add extra packages).
|
||||
USER app
|
||||
|
||||
ENTRYPOINT ["/app/.venv/bin/ocrmypdf"]
|
||||
|
||||
+9
-1
@@ -174,7 +174,15 @@ docker run \
|
||||
--env PYTHONUNBUFFERED=1 \
|
||||
--interactive --tty --entrypoint python3 \
|
||||
jbarlow83/ocrmypdf \
|
||||
watcher.py
|
||||
/app/watcher.py
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
The image runs as the non-root `app` user (uid 1000) by default, so it
|
||||
may not be able to write to the `/output` and `/processed` volumes unless
|
||||
you add a `--user` argument. The correct value depends on whether you use
|
||||
rootful Docker, rootless Docker, or Podman -- see
|
||||
{ref}`Bind-mounted volumes <docker-volumes>` for details.
|
||||
:::
|
||||
|
||||
This service will watch for a file that matches `/input/\*.pdf`, convert
|
||||
|
||||
+84
-23
@@ -71,15 +71,29 @@ application (as opposed to the more conventional case, where a Docker
|
||||
container runs as a server). For that reason we usually use the `--rm`
|
||||
argument to delete the container when it exits.
|
||||
|
||||
:::{note}
|
||||
The image runs as a non-root user (`app`, uid/gid 1000) by default,
|
||||
rather than as root. This is a defense-in-depth measure: a flaw in
|
||||
OCRmyPDF or one of its dependencies cannot trivially act as root inside
|
||||
the container. The examples below assume **rootless Docker** or
|
||||
**Podman**; the differences for traditional *rootful* Docker are
|
||||
described separately under *Special case: rootful Docker* below.
|
||||
:::
|
||||
|
||||
To start a Docker container (instance of the image):
|
||||
|
||||
:::{code} bash
|
||||
docker run --rm -i jbarlow83/ocrmypdf-alpine (... all other arguments here...) - -
|
||||
:::
|
||||
|
||||
For convenience, create a shell alias to hide the Docker command. It is
|
||||
easier to send the input file as stdin and read the output from stdout
|
||||
-- **this avoids the messy permission issues with Docker entirely**.
|
||||
### Recommended: pipe through stdin and stdout
|
||||
|
||||
The easiest and most portable way to use the image is to send the input
|
||||
file on stdin and read the output from stdout. This **avoids file
|
||||
permission issues entirely** -- nothing is written to a mounted
|
||||
directory, so it does not matter which user the container runs as, nor
|
||||
whether you use rootless or rootful Docker. For convenience, create a
|
||||
shell alias to hide the Docker command:
|
||||
|
||||
:::{code} bash
|
||||
alias docker_ocrmypdf='docker run --rm -i jbarlow83/ocrmypdf-alpine'
|
||||
@@ -90,28 +104,42 @@ docker_ocrmypdf - - <input.pdf >output.pdf
|
||||
Or in the wonderful [fish shell](https://fishshell.com/):
|
||||
|
||||
:::{code} fish
|
||||
alias docker_ocrmypdf 'docker run --rm jbarlow83/ocrmypdf-alpine'
|
||||
alias docker_ocrmypdf 'docker run --rm -i jbarlow83/ocrmypdf-alpine'
|
||||
funcsave docker_ocrmypdf
|
||||
:::
|
||||
|
||||
Alternately, you could mount the local current working directory as a
|
||||
Docker volume:
|
||||
{#docker-volumes}
|
||||
### Bind-mounted volumes
|
||||
|
||||
If you would rather mount a directory and pass file paths, you need to
|
||||
consider which user owns the files OCRmyPDF writes back into that
|
||||
directory. The image's default working directory is `/data`, so mounting
|
||||
your files there lets you pass plain relative paths without an explicit
|
||||
`--workdir`. Because the container runs as the non-root `app` user, the
|
||||
right invocation otherwise depends on your container runtime.
|
||||
|
||||
**Rootless Docker (the assumed default).** Your own account runs the
|
||||
daemon, so the container's `root` maps back to *your* unprivileged host
|
||||
user, while every other container uid -- including the image's default
|
||||
`app`/1000 -- maps to a *subordinate* uid. A directory you own on the
|
||||
host therefore appears owned by `root` inside the container, so the
|
||||
default `app` user usually **cannot write to it at all**. Run the job as
|
||||
container-`root`, which under rootless Docker is still your ordinary host
|
||||
user, so the write succeeds and the output is owned by you:
|
||||
|
||||
:::{code} bash
|
||||
alias docker_ocrmypdf='docker run --rm -i --user "$(id -u):$(id -g)" --workdir /data -v "$PWD:/data" jbarlow83/ocrmypdf-alpine'
|
||||
docker_ocrmypdf /data/input.pdf /data/output.pdf
|
||||
alias docker_ocrmypdf='docker run --rm -i --user 0:0 -v "$PWD:/data" jbarlow83/ocrmypdf-alpine'
|
||||
docker_ocrmypdf input.pdf output.pdf
|
||||
:::
|
||||
|
||||
## Podman
|
||||
|
||||
Especially if you use [Podman](https://podman.io/) (or use Docker in
|
||||
rootless mode), you may need to add `--userns keep-id` there,
|
||||
otherwise you may get access errors, because the user ID is otherwise not
|
||||
mapped to the same UID as on the host:
|
||||
**Podman.** Podman provides `--userns keep-id`, which maps your host uid
|
||||
straight through into the container. Combined with `--user`, you run as
|
||||
your own uid and own the output directly, otherwise you may get access
|
||||
errors because the user ID is not mapped to the same UID as on the host:
|
||||
|
||||
:::{code} bash
|
||||
alias podman_ocrmypdf='podman run --rm -i --user "$(id -u):$(id -g)" --userns keep-id --workdir /data -v "$PWD:/data" jbarlow83/ocrmypdf-alpine'
|
||||
podman_ocrmypdf /data/input.pdf /data/output.pdf
|
||||
alias podman_ocrmypdf='podman run --rm -i --user "$(id -u):$(id -g)" --userns keep-id -v "$PWD:/data" jbarlow83/ocrmypdf-alpine'
|
||||
podman_ocrmypdf input.pdf output.pdf
|
||||
:::
|
||||
|
||||
If you have SELinux enabled, you may additionally need to add the `:Z` [suffix to
|
||||
@@ -124,10 +152,27 @@ the end of the linked podman documentation for details. This results in
|
||||
the following full command:
|
||||
|
||||
:::{code} bash
|
||||
alias podman_ocrmypdf='podman run --rm -i --user "$(id -u):$(id -g)" --userns keep-id --workdir /data -v "$PWD:/data" --security-opt label=disable jbarlow83/ocrmypdf-alpine'
|
||||
podman_ocrmypdf /data/input.pdf /data/output.pdf
|
||||
alias podman_ocrmypdf='podman run --rm -i --user "$(id -u):$(id -g)" --userns keep-id -v "$PWD:/data" --security-opt label=disable jbarlow83/ocrmypdf-alpine'
|
||||
podman_ocrmypdf input.pdf output.pdf
|
||||
:::
|
||||
|
||||
{#docker-rootful}
|
||||
### Special case: rootful Docker
|
||||
|
||||
With a traditional root daemon, container uid *N* is the *same* uid *N*
|
||||
on the host. Running the container as root would therefore fill your
|
||||
mounted directory with root-owned files and -- more importantly -- a
|
||||
container escape would run as real host root. Drop to your own uid so the
|
||||
output is owned by you and the process stays unprivileged:
|
||||
|
||||
:::{code} bash
|
||||
alias docker_ocrmypdf='docker run --rm -i --user "$(id -u):$(id -g)" -v "$PWD:/data" jbarlow83/ocrmypdf-alpine'
|
||||
docker_ocrmypdf input.pdf output.pdf
|
||||
:::
|
||||
|
||||
The non-root default and the `--user` override both reduce the risk here,
|
||||
but rootless Docker or Podman remain the safer choice when available.
|
||||
|
||||
{#docker-lang-packs}
|
||||
## Adding languages to the Docker image
|
||||
|
||||
@@ -139,8 +184,12 @@ creating a new Dockerfile based on the public one.
|
||||
:::{code} dockerfile
|
||||
FROM jbarlow83/ocrmypdf
|
||||
|
||||
# The image runs as the non-root "app" user, so switch back to root for
|
||||
# build steps that install packages, then drop back to "app".
|
||||
USER root
|
||||
# Example: add Italian
|
||||
RUN apt install tesseract-ocr-ita
|
||||
RUN apt-get update && apt-get install -y tesseract-ocr-ita
|
||||
USER app
|
||||
:::
|
||||
|
||||
To install language packs (training data) such as the
|
||||
@@ -179,7 +228,11 @@ Extending the Docker image
|
||||
--------------------------
|
||||
|
||||
You can extend the Docker image with your own customizations, similar to
|
||||
the way it is extended to add language packs.
|
||||
the way it is extended to add language packs. Because the image runs as
|
||||
the non-root `app` user, switch to `USER root` for any build steps that
|
||||
require root (installing packages, writing to system directories) and
|
||||
back to `USER app` afterwards, as shown in the language pack example
|
||||
above.
|
||||
|
||||
Note that the Docker image is subject to change at any time. For
|
||||
example, the base image may be updated to a newer version of Ubuntu or
|
||||
@@ -196,7 +249,7 @@ Executing the test suite
|
||||
The OCRmyPDF test suite is installed with image. To run it:
|
||||
|
||||
:::{code} bash
|
||||
docker run --rm --entrypoint python jbarlow83/ocrmypdf -m pytest
|
||||
docker run --rm --workdir /app --entrypoint python jbarlow83/ocrmypdf -m pytest
|
||||
:::
|
||||
|
||||
Accessing the shell
|
||||
@@ -205,7 +258,15 @@ Accessing the shell
|
||||
To use the shell in the Docker image:
|
||||
|
||||
:::{code} bash
|
||||
docker run -it --entrypoint sh jbarlow83/ocrmypdf
|
||||
docker run -it --entrypoint sh jbarlow83/ocrmypdf-alpine
|
||||
:::
|
||||
|
||||
This shell runs as the non-root `app` user. If you need root inside the
|
||||
container -- for example to install extra packages with `apk` or `apt` --
|
||||
add `--user root`:
|
||||
|
||||
:::{code} bash
|
||||
docker run -it --user root --entrypoint sh jbarlow83/ocrmypdf-alpine
|
||||
:::
|
||||
|
||||
Using the OCRmyPDF web service wrapper
|
||||
@@ -215,7 +276,7 @@ The OCRmyPDF Docker image includes an example, barebones HTTP web
|
||||
service. The webservice may be launched as follows:
|
||||
|
||||
:::{code} bash
|
||||
docker run --entrypoint python -p 5000:5000 jbarlow83/ocrmypdf webservice.py
|
||||
docker run --entrypoint python -p 5000:5000 jbarlow83/ocrmypdf /app/webservice.py
|
||||
:::
|
||||
|
||||
We omit the `--rm` parameter so that the container will not be
|
||||
|
||||
@@ -6,12 +6,19 @@ services:
|
||||
ocrmypdf:
|
||||
restart: always
|
||||
container_name: ocrmypdf
|
||||
image: jbarlow83/ocrmypdf
|
||||
image: jbarlow83/ocrmypdf-alpine
|
||||
volumes:
|
||||
- "/media/scan:/input"
|
||||
- "/mnt/scan:/output"
|
||||
environment:
|
||||
- OCR_OUTPUT_DIRECTORY_YEAR_MONTH=0
|
||||
# The image runs as the non-root "app" user (uid 1000) by default. The
|
||||
# correct value here depends on your runtime, so that the watcher can write
|
||||
# to the /output bind mount and the files end up owned by you:
|
||||
# rootful Docker -> your host uid:gid
|
||||
# rootless Docker -> "0:0" (container root maps to your host user)
|
||||
# Podman -> your host uid:gid, plus `userns_mode: "keep-id"`
|
||||
# See docs/docker.md ("Bind-mounted volumes") for the reasoning.
|
||||
user: "<SET TO YOUR USER ID>:<SET TO YOUR GROUP ID>"
|
||||
entrypoint: python3
|
||||
command: watcher.py
|
||||
command: /app/watcher.py
|
||||
|
||||
Reference in New Issue
Block a user