From 563e4c2244b53fa983133ad7425478507efd681f Mon Sep 17 00:00:00 2001 From: mr-forust Date: Fri, 9 Oct 2026 01:01:35 +0200 Subject: [PATCH] feat(homelab): isolate PR runner and add Paperless --- .gitea/runner/README.md | 71 ++++++--- .gitea/runner/pr-config.yaml | 6 +- .gitea/runner/pr-runner.service | 5 +- .gitea/runner/setup-pr-runner.sh | 24 ++- .gitea/workflows/ci.yaml | 72 +++++++-- .gitea/workflows/renovate-ci.yaml | 3 + .gitignore | 3 + converters/compose.yaml | 2 +- converters/k8s/bentopdf.yaml | 2 +- paperless/README.md | 49 +++++++ paperless/k8s/active | 1 + paperless/k8s/certificates.yaml | 28 ++++ paperless/k8s/database-init-job.yaml | 58 ++++++++ paperless/k8s/ingress.yaml | 33 +++++ paperless/k8s/namespace.yaml | 4 + paperless/k8s/network-policy.yaml | 39 +++++ paperless/k8s/paperless.yaml | 210 +++++++++++++++++++++++++++ paperless/k8s/secrets.yaml.example | 10 ++ streaming/compose.yaml | 3 +- 19 files changed, 581 insertions(+), 42 deletions(-) create mode 100644 paperless/README.md create mode 100644 paperless/k8s/active create mode 100644 paperless/k8s/certificates.yaml create mode 100644 paperless/k8s/database-init-job.yaml create mode 100644 paperless/k8s/ingress.yaml create mode 100644 paperless/k8s/namespace.yaml create mode 100644 paperless/k8s/network-policy.yaml create mode 100644 paperless/k8s/paperless.yaml create mode 100644 paperless/k8s/secrets.yaml.example diff --git a/.gitea/runner/README.md b/.gitea/runner/README.md index 98f2a24..b7adc68 100644 --- a/.gitea/runner/README.md +++ b/.gitea/runner/README.md @@ -1,10 +1,13 @@ # Homelab CI/CD The native Gitea runners run on **vps**; production runs on **workstation**. -Main-branch checks and image builds use `homelab:host`. Pull request and -non-main checks use `homelab-pr:host` under a separate account without Docker -access. The `homelab-pr` runner is registered at User scope for `forust`, so -any repository under that account can schedule jobs that request this label. +Main-branch checks and image builds use `homelab:host`. Pull request checks use +`homelab-pr` in a Docker job container. CI PR checks use `pull_request_target`, +so Gitea loads the workflow from the trusted base branch. That event then runs +untrusted PR code, so the workflow must select `homelab-pr` before checkout and +must not expose secrets. The CI validation jobs grant only `contents: read` and +checkout the explicit PR head SHA with `persist-credentials: false`. Register +`homelab-pr` at repository scope so only this repository can schedule its jobs. Each runner accepts one job at a time; the build waits for every check to pass. CI and deploy runs also show a summary with the release SHA, image build or reuse results, deploy mode, selected services, @@ -42,29 +45,57 @@ Nothing runs `docker system prune`, removes unrelated images, or deletes volumes ### Pull request runner -Install the unprivileged host runner on the VPS: +Install the PR container runner on the VPS: ```sh sudo bash .gitea/runner/setup-pr-runner.sh ``` -Get a registration token from the user Actions runner settings. Run the -installer in a terminal. It asks for the token without echoing it, registers the -runner as `homelab-pr` with label `homelab-pr:host`, then enables the service. -The work directory is `/var/lib/gitea-pr-runner`. Confirm that Gitea lists the -runner as User scope before merging the workflow change. An unmatched label can -fall back to the default job image. +Create a runner registration token from this repository's Actions runner +settings. Run the installer in a terminal. It asks for the token without +echoing it and registers `homelab-pr` with label +`homelab-pr:docker://docker.gitea.com/runner-images:ubuntu-24.04-v26.09.01`. Confirm that +Gitea lists the runner at Repository scope. The service runs as +`gitea-pr-runner`; systemd grants that service access to the Docker socket with +`SupplementaryGroups=docker`. Keep the account itself out of the `docker` +group. The work directory is `/var/lib/gitea-pr-runner`. -Renovate PR validation uses `pull_request_target`, which reads the workflow from -the base branch. It checks out the PR head only after runner selection and runs -that code on `homelab-pr`. Keep this workflow read-only and do not add secrets. +The runner config disables privileged containers, forbids workflow volume +mounts, and prevents the Docker socket from being mounted into job and action +containers. Do not mount the runner home or its host-side tool cache into a job. +The existing host-side cache is retained, but PR job containers cannot read it. +An unmatched label can fall back to the default job image; check the registered +label before enabling PR checks. -The PR runner has a separate home and tool cache. Do not add it to the `docker` -group or give it access to `/var/run/docker.sock`. It runs repository code from -pull requests, so keep its registration and permissions separate from the -trusted `homelab` runner. This separates users and host permissions, but both -runners still share the VPS kernel and network. Use a disposable VM if PRs from -untrusted external authors must be fully isolated. +The installer reuses `/var/lib/gitea-pr-runner/.runner` when it exists. That +file keeps the registration scope assigned by Gitea. To move an existing +User-scoped runner to Repository scope, stop the service, remove the old runner +from Gitea, back up and remove that registration file, then run the installer +with a token created in this repository's Actions runner settings. Confirm the +new scope in Gitea before enabling PR checks. + +The CI and Renovate workflows use `pull_request_target`, which reads the +workflow from the base branch. They select `homelab-pr` before checking out PR +code. The explicit head SHA and `persist-credentials: false` are mandatory: +without the latter, checkout can leave the job token in Git configuration. +Keep PR validation read-only and do not add Actions secrets. In the checked-in +workflows, only a push to `main` or a manual CI run on `main` can select the +trusted `homelab` runner. Gitea schedules jobs by matching `runs-on` labels; the +runner does not restrict jobs by event or branch. Keep Gitea's approval gate for +fork PR workflows enabled. Verify the live Gitea version and approval setting +before relying on this gate; the image tag in the repository does not prove the +version currently running. Before approving a fork workflow run, review all new +and changed workflow files: a PR-defined `pull_request` workflow can request +the `homelab` label. Automatic CI and Renovate PR checks use the trusted base +workflow and select only `homelab-pr`. The release and deploy jobs stay on the +trusted runner. + +The runner service can access the host Docker daemon, but job and action +containers do not receive its socket or arbitrary host mounts. The runner and +job containers still share the VPS kernel and Docker daemon. A container escape +can therefore affect the host and other workloads. This is container isolation, +not VM isolation; use disposable VMs for PRs that require a separate kernel and +Docker daemon. ## Workstation setup diff --git a/.gitea/runner/pr-config.yaml b/.gitea/runner/pr-config.yaml index 2b6ee49..881ac80 100644 --- a/.gitea/runner/pr-config.yaml +++ b/.gitea/runner/pr-config.yaml @@ -3,6 +3,10 @@ runner: capacity: 1 timeout: 5h labels: - - homelab-pr:host + - homelab-pr:docker://docker.gitea.com/runner-images:ubuntu-24.04-v26.09.01 cache: enabled: false +container: + privileged: false + valid_volumes: [] + docker_host: "-" diff --git a/.gitea/runner/pr-runner.service b/.gitea/runner/pr-runner.service index 9c6fd5a..73d6874 100644 --- a/.gitea/runner/pr-runner.service +++ b/.gitea/runner/pr-runner.service @@ -1,11 +1,12 @@ [Unit] Description=Gitea Actions untrusted pull request runner -After=network-online.target -Wants=network-online.target +After=network-online.target docker.service +Wants=network-online.target docker.service [Service] User=gitea-pr-runner Group=gitea-pr-runner +SupplementaryGroups=docker WorkingDirectory=/var/lib/gitea-pr-runner Environment=HOME=/var/lib/gitea-pr-runner Environment=PATH=/var/lib/gitea-pr-runner/.cache/homelab-ci/bin:/usr/local/bin:/usr/bin:/bin diff --git a/.gitea/runner/setup-pr-runner.sh b/.gitea/runner/setup-pr-runner.sh index 7c71679..0284f8a 100755 --- a/.gitea/runner/setup-pr-runner.sh +++ b/.gitea/runner/setup-pr-runner.sh @@ -1,16 +1,32 @@ #!/usr/bin/env bash -# Install a native runner for untrusted PR jobs without Docker access. +# Install the containerized runner service for untrusted PR jobs. set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" [ "$(id -u)" -eq 0 ] || { echo 'Run with sudo on the runner host' >&2; exit 1; } -for tool in cp cut date getent id install runuser systemctl useradd; do +for tool in cp cut date docker getent id install runuser systemctl useradd; do command -v "$tool" >/dev/null || { echo "Install missing prerequisite: $tool" >&2; exit 1; } done +docker info >/dev/null || { + echo 'Start Docker Engine before installing the PR runner' >&2 + exit 1 +} command -v /usr/local/bin/gitea-runner >/dev/null || { echo 'Install gitea-runner 3.0.2 at /usr/local/bin/gitea-runner first' >&2 exit 1 } +runner_version_output="$(/usr/local/bin/gitea-runner --version 2>&1)" || { + echo 'Cannot read the installed gitea-runner version' >&2 + exit 1 +} +if [[ ! "$runner_version_output" =~ (^|[[:space:]])v?3\.0\.2($|[[:space:]]) ]]; then + printf 'Expected gitea-runner 3.0.2; found: %s\n' "$runner_version_output" >&2 + exit 1 +fi +getent group docker >/dev/null || { + echo 'Install Docker Engine first; the docker group is missing' >&2 + exit 1 +} id gitea-pr-runner >/dev/null 2>&1 || \ useradd --system --create-home --home-dir /var/lib/gitea-pr-runner --shell /usr/bin/bash gitea-pr-runner runner_home="$(getent passwd gitea-pr-runner | cut -d: -f6)" @@ -20,7 +36,7 @@ runner_home="$(getent passwd gitea-pr-runner | cut -d: -f6)" } case " $(id -nG gitea-pr-runner) " in *' docker '*) - echo 'The PR runner account must not belong to the docker group' >&2 + echo 'Remove gitea-pr-runner from the docker group; only the systemd service gets Docker access' >&2 exit 1 ;; esac @@ -44,7 +60,7 @@ if [ ! -f /var/lib/gitea-pr-runner/.runner ]; then --config /etc/gitea-pr-runner/config.yaml \ --instance https://gitea.forust.xyz \ --name homelab-pr \ - --labels homelab-pr:host \ + --labels 'homelab-pr:docker://docker.gitea.com/runner-images:ubuntu-24.04-v26.09.01' \ --no-interactive unset GITEA_RUNNER_REGISTRATION_TOKEN fi diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 75ddd15..b5546f8 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -3,22 +3,30 @@ name: ci push: branches: - main - pull_request: null + # Use the base-branch workflow so PR changes cannot select trusted runners. + pull_request_target: null workflow_dispatch: null permissions: contents: read actions: read concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + group: ci-${{ github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.ref != 'refs/heads/main' }} jobs: + # pull_request_target uses the base ref (often main); check the event as well + # as the ref so every PR job stays on the isolated runner. compose: name: Compose - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Validate Compose files shell: bash @@ -66,11 +74,16 @@ jobs: fi workflows: name: Workflows - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Prepare pinned tools shell: bash @@ -102,11 +115,16 @@ jobs: fi shell: name: Shell - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Prepare pinned tools shell: bash @@ -146,11 +164,16 @@ jobs: fi formatting: name: Formatting - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Prepare pinned tools shell: bash @@ -194,11 +217,16 @@ jobs: fi python: name: Python and tests - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Prepare pinned tools shell: bash @@ -232,11 +260,16 @@ jobs: fi yaml: name: YAML - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Prepare pinned tools shell: bash @@ -280,11 +313,16 @@ jobs: fi dockerfiles: name: Dockerfiles - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Prepare pinned tools shell: bash @@ -326,11 +364,16 @@ jobs: fi kubernetes: name: Kubernetes - runs-on: ${{ github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} + permissions: + contents: read + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false id: source - name: Prepare pinned tools shell: bash @@ -377,7 +420,7 @@ jobs: fi image-plan: needs: [compose, workflows, shell, formatting, python, yaml, dockerfiles, kubernetes] - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: homelab timeout-minutes: 10 outputs: @@ -388,6 +431,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: fetch-depth: 0 + persist-credentials: false - name: Detect build inputs against successful CI id: plan env: @@ -433,6 +477,8 @@ jobs: - name: Checkout repository id: source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false - name: Download the checked image plan id: inputs uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -480,6 +526,8 @@ jobs: - name: Checkout repository id: source uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false - name: Download all image results id: inputs uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 diff --git a/.gitea/workflows/renovate-ci.yaml b/.gitea/workflows/renovate-ci.yaml index ea3b92b..129f800 100644 --- a/.gitea/workflows/renovate-ci.yaml +++ b/.gitea/workflows/renovate-ci.yaml @@ -28,6 +28,8 @@ permissions: jobs: validate-renovate: + permissions: + contents: read runs-on: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'homelab' || 'homelab-pr' }} timeout-minutes: 20 steps: @@ -35,6 +37,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false # renovate/k8s/cronjob.yaml is the single source of truth for the version. - name: Resolve the deployed Renovate version diff --git a/.gitignore b/.gitignore index 9df6ca4..07cc1af 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,6 @@ prometheus-stack/k8s/grafana-values.yaml traefik/k8s/local-tls.yaml converters/k8s/config.yaml convertx/k8s/config.yaml + +# Graphify local index and generated reports +graphify-out/ diff --git a/converters/compose.yaml b/converters/compose.yaml index bdfb8a9..e52bdd1 100644 --- a/converters/compose.yaml +++ b/converters/compose.yaml @@ -42,7 +42,7 @@ services: bentopdf: container_name: bentopdf - image: bentopdf/bentopdf@sha256:4eb4ec8f5030faf87c29a73d3d5a2781f28a597cf440c3ab111eb96aee550871 + image: bentopdfteam/bentopdf-simple:2.8.8 restart: unless-stopped labels: - "traefik.enable=true" diff --git a/converters/k8s/bentopdf.yaml b/converters/k8s/bentopdf.yaml index 1a8d19c..4654996 100644 --- a/converters/k8s/bentopdf.yaml +++ b/converters/k8s/bentopdf.yaml @@ -26,7 +26,7 @@ spec: app: bentopdf spec: containers: - - image: bentopdf/bentopdf@sha256:4eb4ec8f5030faf87c29a73d3d5a2781f28a597cf440c3ab111eb96aee550871 + - image: bentopdfteam/bentopdf-simple:2.8.8 imagePullPolicy: Always name: bentopdf ports: diff --git a/paperless/README.md b/paperless/README.md new file mode 100644 index 0000000..b82a630 --- /dev/null +++ b/paperless/README.md @@ -0,0 +1,49 @@ +# Paperless-ngx + +Paperless-ngx runs in the `paperless` namespace. It uses the shared PostgreSQL +service in the `database` namespace and Valkey for its task queue. The document +library, exports, and consume folder are stored on the `local-path-retain` +volume. The PVC size is fixed at 50 GiB because this storage class does not +support volume expansion. + +The local route is `https://papers.workstation.internal`; the public route is +`https://papers.forust.xyz`. Both use TLS. Paperless keeps its own login and +password authentication. OCR is configured for Russian and English documents. + +## Prepare the secret + +Create `k8s/secrets.yaml` on the workstation from +`k8s/secrets.yaml.example`. Set a unique random `PAPERLESS_SECRET_KEY`, a long +`PAPERLESS_ADMIN_PASSWORD`, and `PAPERLESS_DB_PASSWORD`. + +Add the same `PAPERLESS_DB_PASSWORD` value to the local +`postgres/k8s/secrets.yaml` file. Keep both secret files out of Git. The +database bootstrap Job creates the `paperless` role and database from the +shared PostgreSQL secret. The job runs in the `database` namespace and needs +that namespace's existing `postgres-shared-secrets` Secret. + +For example, generate a key with: + +```sh +python3 -c 'import secrets; print(secrets.token_urlsafe(64))' +``` + +Then apply the secret before enabling the service: + +```sh +kubectl apply -f paperless/k8s/namespace.yaml +kubectl apply -f postgres/k8s/secrets.yaml +kubectl apply -f paperless/k8s/secrets.yaml +``` + +The normal deploy workflow applies the remaining manifests when +`paperless/k8s/active` is present. Verify the rollout and ingress after deploy: + +```sh +kubectl -n paperless rollout status deployment/paperless +kubectl -n paperless get pods,pvc,services +``` + +Back up the `paperless-data` PVC and the shared PostgreSQL database. The PVC +contains the originals, archived PDFs, and export/consume folders. Valkey has +no persistent volume; queued tasks are recreated after a restart. diff --git a/paperless/k8s/active b/paperless/k8s/active new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/paperless/k8s/active @@ -0,0 +1 @@ + diff --git a/paperless/k8s/certificates.yaml b/paperless/k8s/certificates.yaml new file mode 100644 index 0000000..495269e --- /dev/null +++ b/paperless/k8s/certificates.yaml @@ -0,0 +1,28 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: paperless-prod-tls + namespace: paperless +spec: + secretName: paperless-prod-tls + dnsNames: + - papers.forust.xyz + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: internal-wildcard-tls + namespace: paperless +spec: + secretName: internal-wildcard-tls + dnsNames: + - "*.workstation.internal" + - "*.gigaforust.internal" + - workstation.internal + - gigaforust.internal + issuerRef: + name: internal-ca + kind: ClusterIssuer diff --git a/paperless/k8s/database-init-job.yaml b/paperless/k8s/database-init-job.yaml new file mode 100644 index 0000000..e921aca --- /dev/null +++ b/paperless/k8s/database-init-job.yaml @@ -0,0 +1,58 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: paperless-database-init + namespace: database +spec: + backoffLimit: 5 + template: + metadata: + labels: + app.kubernetes.io/name: paperless-database-init + spec: + restartPolicy: OnFailure + containers: + - name: create-database + image: postgres:17.11-alpine + command: + - /bin/sh + - -ec + - | + PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \ + --host postgres \ + --username postgres \ + --dbname postgres \ + --set ON_ERROR_STOP=1 \ + --set paperless_password="$PAPERLESS_DB_PASSWORD" <<'SQL' + SELECT format( + 'CREATE ROLE paperless LOGIN PASSWORD %L', + :'paperless_password' + ) + WHERE NOT EXISTS ( + SELECT FROM pg_roles WHERE rolname = 'paperless' + ) + \gexec + SELECT format('CREATE DATABASE paperless OWNER paperless') + WHERE NOT EXISTS ( + SELECT FROM pg_database WHERE datname = 'paperless' + ) + \gexec + SQL + env: + - name: POSTGRES_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-shared-secrets + key: POSTGRES_ADMIN_PASSWORD + - name: PAPERLESS_DB_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-shared-secrets + key: PAPERLESS_DB_PASSWORD + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi diff --git a/paperless/k8s/ingress.yaml b/paperless/k8s/ingress.yaml new file mode 100644 index 0000000..8da7c32 --- /dev/null +++ b/paperless/k8s/ingress.yaml @@ -0,0 +1,33 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: paperless-prod + namespace: paperless +spec: + entryPoints: + - websecure + routes: + - match: Host(`papers.forust.xyz`) + kind: Rule + services: + - name: paperless + port: 8000 + tls: + secretName: paperless-prod-tls +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: paperless-local + namespace: paperless +spec: + entryPoints: + - websecure + routes: + - match: Host(`papers.workstation.internal`) + kind: Rule + services: + - name: paperless + port: 8000 + tls: + secretName: internal-wildcard-tls diff --git a/paperless/k8s/namespace.yaml b/paperless/k8s/namespace.yaml new file mode 100644 index 0000000..37ad102 --- /dev/null +++ b/paperless/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: paperless diff --git a/paperless/k8s/network-policy.yaml b/paperless/k8s/network-policy.yaml new file mode 100644 index 0000000..e075792 --- /dev/null +++ b/paperless/k8s/network-policy.yaml @@ -0,0 +1,39 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: paperless-ingress + namespace: paperless +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: paperless + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik + ports: + - protocol: TCP + port: 8000 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: paperless-valkey-ingress + namespace: paperless +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: paperless-valkey + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: paperless + ports: + - protocol: TCP + port: 6379 diff --git a/paperless/k8s/paperless.yaml b/paperless/k8s/paperless.yaml new file mode 100644 index 0000000..b2b7d96 --- /dev/null +++ b/paperless/k8s/paperless.yaml @@ -0,0 +1,210 @@ +apiVersion: v1 +kind: Service +metadata: + name: paperless + namespace: paperless + labels: + app.kubernetes.io/name: paperless +spec: + selector: + app.kubernetes.io/name: paperless + ports: + - name: http + port: 8000 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paperless + namespace: paperless + labels: + app.kubernetes.io/name: paperless +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: paperless + template: + metadata: + labels: + app.kubernetes.io/name: paperless + spec: + enableServiceLinks: false + containers: + - name: paperless + image: ghcr.io/paperless-ngx/paperless-ngx:3.2.1 + ports: + - name: http + containerPort: 8000 + env: + - name: PAPERLESS_URL + value: https://papers.forust.xyz + - name: PAPERLESS_ALLOWED_HOSTS + value: papers.forust.xyz,papers.workstation.internal + - name: PAPERLESS_CSRF_TRUSTED_ORIGINS + value: https://papers.forust.xyz,https://papers.workstation.internal + - name: PAPERLESS_TIME_ZONE + value: Europe/Bratislava + - name: PAPERLESS_REDIS + value: redis://paperless-valkey:6379 + - name: PAPERLESS_DBENGINE + value: postgresql + - name: PAPERLESS_DBHOST + value: postgres.database.svc.cluster.local + - name: PAPERLESS_DBNAME + value: paperless + - name: PAPERLESS_DBUSER + value: paperless + - name: PAPERLESS_DBPASS + valueFrom: + secretKeyRef: + name: paperless-secrets + key: PAPERLESS_DB_PASSWORD + - name: PAPERLESS_OCR_LANGUAGE + value: rus+eng + - name: PAPERLESS_OCR_LANGUAGES + value: rus + - name: PAPERLESS_TASK_WORKERS + value: "1" + - name: PAPERLESS_ADMIN_USER + value: admin + - name: PAPERLESS_SECRET_KEY + valueFrom: + secretKeyRef: + name: paperless-secrets + key: PAPERLESS_SECRET_KEY + - name: PAPERLESS_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: paperless-secrets + key: PAPERLESS_ADMIN_PASSWORD + volumeMounts: + - name: documents + mountPath: /usr/src/paperless/data + subPath: data + - name: documents + mountPath: /usr/src/paperless/media + subPath: media + - name: documents + mountPath: /usr/src/paperless/export + subPath: export + - name: documents + mountPath: /usr/src/paperless/consume + subPath: consume + startupProbe: + httpGet: + path: / + port: http + httpHeaders: + - name: Host + value: papers.workstation.internal + failureThreshold: 60 + periodSeconds: 10 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: / + port: http + httpHeaders: + - name: Host + value: papers.workstation.internal + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: / + port: http + httpHeaders: + - name: Host + value: papers.workstation.internal + periodSeconds: 30 + timeoutSeconds: 5 + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + volumes: + - name: documents + persistentVolumeClaim: + claimName: paperless-data +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: paperless-data + namespace: paperless +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path-retain + resources: + requests: + storage: 50Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: paperless-valkey + namespace: paperless + labels: + app.kubernetes.io/name: paperless-valkey +spec: + selector: + app.kubernetes.io/name: paperless-valkey + ports: + - name: redis + port: 6379 + targetPort: redis +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paperless-valkey + namespace: paperless + labels: + app.kubernetes.io/name: paperless-valkey +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: paperless-valkey + template: + metadata: + labels: + app.kubernetes.io/name: paperless-valkey + spec: + containers: + - name: valkey + image: valkey/valkey:9.0.3-alpine + args: + - valkey-server + - --save + - "" + - --appendonly + - "no" + ports: + - name: redis + containerPort: 6379 + readinessProbe: + exec: + command: ["valkey-cli", "ping"] + periodSeconds: 10 + livenessProbe: + exec: + command: ["valkey-cli", "ping"] + periodSeconds: 30 + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi diff --git a/paperless/k8s/secrets.yaml.example b/paperless/k8s/secrets.yaml.example new file mode 100644 index 0000000..75b950e --- /dev/null +++ b/paperless/k8s/secrets.yaml.example @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: paperless-secrets + namespace: paperless +type: Opaque +stringData: + PAPERLESS_SECRET_KEY: "" + PAPERLESS_ADMIN_PASSWORD: "" + PAPERLESS_DB_PASSWORD: "" diff --git a/streaming/compose.yaml b/streaming/compose.yaml index 9c009f3..059658b 100644 --- a/streaming/compose.yaml +++ b/streaming/compose.yaml @@ -87,7 +87,8 @@ services: - streaming jellyseerr: - image: fallenbagel/jellyseerr:latest + image: ghcr.io/seerr-team/seerr:v3.5.0 + init: true container_name: jellyseerr restart: unless-stopped environment: