diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..51fd659 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +* +!pyproject.toml +!uv.lock +!data/ +!bindery/ +**/__pycache__ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..022aa50 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1 +FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim + +# Prevents Python from buffering stdout and stderr +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PATH=/app/.venv/bin:$PATH + +WORKDIR /app + +# uv installs the locked dependencies without the dev tools. The app comes after them, so a code change reuses this +# layer. +COPY pyproject.toml uv.lock ./ +RUN UV_COMPILE_BYTECODE=1 UV_PYTHON_DOWNLOADS=never \ + uv sync --locked --no-default-groups --no-install-project --no-cache + +# --chmod sets the mode as part of the copy, so every user can read the app. The container can +# run as any user, also with rootless Docker or Podman, and root owns the copies otherwise. +COPY --chmod=a+rX data ./data +COPY --chmod=a+rX bindery ./bindery + +EXPOSE 8085 + +# The commands come from /app/.venv/bin. "uv run" would try to sync the root-owned environment as the container user. +HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \ + CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8085/api/health', timeout=5)"] + +# One worker only, because the job and scheduler threads run inside it. Bindery does not use the control socket, and +# its default folder, $HOME/.gunicorn, is not writable for a container user without a home folder. +CMD ["gunicorn", "--bind", "0.0.0.0:8085", "--workers", "1", "--threads", "4", "--timeout", "300", \ + "--no-control-socket", "bindery.web:app"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ab08cf1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + bindery: + build: . + image: bindery:latest + # The user and group that own ./config and can write CWA's ingest folder. Usually the same as CWA's. + user: "1000:1000" + environment: + # The address of CWA, as the Bindery container reaches it, and the CWA account that Bindery uses. + CWA_URL: http://calibre-web-automated:8083 + CWA_USERNAME: bindery + CWA_PASSWORD: change-me + # none, cwa (log in with a CWA user) or proxy (a reverse proxy sets a user header) + AUTH_METHOD: cwa + # Optional and independent. app.db is for logins, metadata.db is for book IDs. + # CWA_DB_PATH: /cwa-config/app.db + # CALIBRE_DB_PATH: /calibre-library/metadata.db + # URL_BASE: /bindery + # true only if clients reach Bindery through a reverse proxy alone. The login lockout then uses X-Forwarded-For. + # TRUST_PROXY: "true" + # BINDERY_API_KEY: a-long-random-string + ports: + - 8085:8085 + restart: unless-stopped + # /api/health needs no login, and works with and without URL_BASE. + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8085/api/health', timeout=5)"] + interval: 60s + timeout: 10s + start_period: 30s + retries: 3 + volumes: + # CWA's ingest folder. New stories and updates go here. + - /path/to/cwa/ingest:/cwa-book-ingest + # The story database and personal.ini. Create the folder before the first start. + - ./config:/config + # Optional read-only folders. Include SQLite journal files beside each database. + # Mount the library that receives the ingest files, not a copy or another library. + # - /path/to/cwa/config:/cwa-config:ro + # - /path/to/calibre/library:/calibre-library:ro