diff --git a/paperless/README.md b/paperless/README.md new file mode 100644 index 0000000..740af5b --- /dev/null +++ b/paperless/README.md @@ -0,0 +1,51 @@ +# 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. +Keep `papers.forust.xyz` in the existing Cloudflare DDNS `DOMAINS` setting so +the public record follows the workstation address. + +## 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/postgres/.env.example b/postgres/.env.example index 2255fdf..949e398 100644 --- a/postgres/.env.example +++ b/postgres/.env.example @@ -4,4 +4,5 @@ GITEA_DB_PASSWORD= NETBOX_DB_PASSWORD= NETRONOME_DB_PASSWORD= PENPOT_DB_PASSWORD= +PAPERLESS_DB_PASSWORD= STATUSPAGE_DB_PASSWORD= diff --git a/postgres/README.md b/postgres/README.md index db166ac..68d553b 100644 --- a/postgres/README.md +++ b/postgres/README.md @@ -1,10 +1,10 @@ # Shared PostgreSQL This directory contains the shared PostgreSQL 17 deployment for Authentik, -Gitea, NetBox, Netronome, and Statuspage. It creates one database and one login role -per service. Per-service standalone databases were removed after the -migration (Sep 2026); Penpot stays on its own compose PostgreSQL (archived, -not part of the shared instance). +Gitea, NetBox, Netronome, Paperless-ngx, and Statuspage. It creates one database +and one login role per service. Per-service standalone databases were removed +after the migration (Sep 2026); Penpot stays on its own compose PostgreSQL +(archived, not part of the shared instance). ## Compatibility baseline @@ -14,6 +14,7 @@ not part of the shared instance). | Gitea | 1.27.3 | Supported (Gitea requires 12+) | | NetBox | 4.7.x | Supported (NetBox 4.x requires 13+) | | Netronome | 0.14.0 | Supported (upstream's example uses 17) | +| Paperless-ngx | 3.2.1 | Supported | | Statuspage | custom | Supported | A major-version change must use a logical dump/restore; changing only the diff --git a/postgres/k8s/network-policy.yaml b/postgres/k8s/network-policy.yaml index 17203de..529b802 100644 --- a/postgres/k8s/network-policy.yaml +++ b/postgres/k8s/network-policy.yaml @@ -26,6 +26,18 @@ spec: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: penpot + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: paperless + podSelector: + matchLabels: + app.kubernetes.io/name: paperless + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: database + podSelector: + matchLabels: + app.kubernetes.io/name: paperless-database-init - namespaceSelector: matchLabels: kubernetes.io/metadata.name: statuspage diff --git a/postgres/k8s/postgres.yaml b/postgres/k8s/postgres.yaml index 51112c8..83fe8ae 100644 --- a/postgres/k8s/postgres.yaml +++ b/postgres/k8s/postgres.yaml @@ -126,6 +126,7 @@ data: : "${NETBOX_DB_PASSWORD:?NETBOX_DB_PASSWORD is required}" : "${NETRONOME_DB_PASSWORD:?NETRONOME_DB_PASSWORD is required}" : "${PENPOT_DB_PASSWORD:?PENPOT_DB_PASSWORD is required}" + : "${PAPERLESS_DB_PASSWORD:?PAPERLESS_DB_PASSWORD is required}" : "${STATUSPAGE_DB_PASSWORD:?STATUSPAGE_DB_PASSWORD is required}" create_role_and_database() { @@ -147,4 +148,5 @@ data: create_role_and_database netbox netbox "$NETBOX_DB_PASSWORD" create_role_and_database netronome netronome "$NETRONOME_DB_PASSWORD" create_role_and_database penpot penpot "$PENPOT_DB_PASSWORD" + create_role_and_database paperless paperless "$PAPERLESS_DB_PASSWORD" create_role_and_database statuspage statuspage "$STATUSPAGE_DB_PASSWORD" diff --git a/postgres/k8s/secrets.yaml.example b/postgres/k8s/secrets.yaml.example index 7311069..ae5cafe 100644 --- a/postgres/k8s/secrets.yaml.example +++ b/postgres/k8s/secrets.yaml.example @@ -11,4 +11,5 @@ stringData: NETBOX_DB_PASSWORD: "" NETRONOME_DB_PASSWORD: "" PENPOT_DB_PASSWORD: "" + PAPERLESS_DB_PASSWORD: "" STATUSPAGE_DB_PASSWORD: ""