Skip to main content

Certification track: Associate Cloud Engineer (ACE)

Paperless-ngx Common Shared Configuration Module

The Paperless Common module defines the Paperless-ngx document management system configuration for the RAD Modules ecosystem. It is a pure configuration module — it creates no GCP resources directly and produces a config output consumed by platform-specific wrapper modules (Paperless CloudRun and Paperless GKE). It also auto-generates two application secrets in Secret Manager.

1. Overview

Purpose: To centralise all Paperless-ngx-specific configuration (custom container image, PostgreSQL database setup, environment variable mapping, health probes, GCS Fuse media storage, and the db-init job definition) in a single module shared by both Cloud Run and GKE deployments.

Architecture:

Layer 3: Application Wrappers
├── Paperless_CloudRun ──┐
└── Paperless_GKE ──┤── instantiate Paperless_Common

Paperless_Common (this module)
Creates: PAPERLESS_ADMIN_PASSWORD secret
PAPERLESS_SECRET_KEY secret
Produces: config, secret_ids, storage_buckets

Layer 2: Platform Modules
├── App_CloudRun (serverless deployment)
└── App_GKE (Kubernetes deployment)

Layer 1: App_Common (networking, database, storage, secrets, IAM)

Key characteristics:

  • Uses PostgreSQL 15 (unlike Ghost Common which uses MySQL 8.0 — this is the standard for all other modules in the ecosystem).
  • Creates two GCP secrets in Secret Manager: PAPERLESS_ADMIN_PASSWORD and PAPERLESS_SECRET_KEY. This is a key difference from Ghost Common (which creates no secrets) and similar to Django Common and Directus Common.
  • Provides a GCS Fuse media volume as the primary persistence mechanism, rather than relying on NFS for document storage.
  • The PAPERLESS_SECRET_KEY is a Django application secret — regenerating it invalidates all existing user sessions.
  • Redis is required (not optional) — Paperless-ngx uses Redis as its Celery message broker for all background processing.

2. Outputs

config

The application configuration object passed to the platform module via application_config.

FieldValue / Description
app_name"paperless"
application_versionVersion tag (default: "latest")
container_image"ghcr.io/paperless-ngx/paperless-ngx" (GHCR image used as build base)
image_source"custom" — a custom wrapper image is built
enable_image_mirroringvar.enable_image_mirroring (default true) — mirrors the image to Artifact Registry
container_build_configdockerfile_path = "Dockerfile", context_path = ".", build_args = {}
container_port8000
database_type"POSTGRES_15"
db_nameDatabase name (default: "paperless")
db_userDatabase user (default: "paperless")
enable_cloudsql_volumeWhether to mount the Cloud SQL Auth Proxy sidecar (default: true)
cloudsql_volume_mount_path"/cloudsql"
gcs_volumesGCS Fuse volume mounts. When empty, a default paperless-media volume is auto-configured at /usr/src/paperless/media
container_resourcesCPU: 2000m, Memory: 2Gi
environment_variablesAssembled from all Paperless-ngx-specific env var inputs (see §7)
initialization_jobsDefault db-init job or custom override (see §5)
startup_probeHTTP GET /, 60s initial delay, 10s timeout, 10s period, 30 failure threshold
liveness_probeHTTP GET /, 60s initial delay, 10s timeout, 30s period, 3 failure threshold

secret_ids

A map of environment variable names to Secret Manager secret IDs. These are injected into the container at runtime by the platform module.

KeySecret Description
PAPERLESS_ADMIN_PASSWORDInitial admin account password. Auto-generated 24-character random string.
PAPERLESS_SECRET_KEYDjango application secret key. Auto-generated 64-character random string. Used for session signing and CSRF protection.

storage_buckets

A list of GCS bucket configurations for provisioning by the platform module:

FieldValue
name_suffix"media"
locationDeployment region
storage_class"STANDARD"
force_destroytrue
versioning_enabledfalse
lifecycle_rules[]
public_access_prevention"inherited"

3. Input Variables

Application

VariableTypeDefaultDescription
application_namestring"paperless"Application name used in resource naming
application_versionstring"latest"Paperless-ngx image version tag. Pin to a specific release for production
descriptionstring"Paperless-ngx - open-source document management system"Application description
db_namestring"paperless"PostgreSQL database name
db_userstring"paperless"PostgreSQL application user
cpu_limitstring"2000m"Container CPU limit
memory_limitstring"2Gi"Container memory limit
environment_variablesmap(string){}Additional environment variables passed to the container
initialization_jobslist(any)[]Custom init jobs; empty triggers the default db-init job
startup_probeobjectsee §4Startup health probe configuration
liveness_probeobjectsee §4Liveness health probe configuration
enable_image_mirroringbooltrueMirror the GHCR image to Artifact Registry before deployment
min_instance_countnumber1Minimum running instances
max_instance_countnumber3Maximum running instances

Storage & Volumes

VariableTypeDefaultDescription
enable_cloudsql_volumebooltrueMount Cloud SQL Auth Proxy sidecar socket
gcs_volumeslist(object)[]GCS Fuse volume mounts. Empty triggers default paperless-media auto-mount at /usr/src/paperless/media
regionstring"us-central1"Region for the storage bucket

Paperless-ngx Settings

VariableTypeDefaultDescription
time_zonestring"UTC"Timezone for document timestamps and scheduled tasks
ocr_languagestring"eng"Tesseract OCR language code(s). Use + to combine multiple
admin_userstring"admin"Username for the auto-created superuser
admin_emailstring"admin@example.com"Email for the auto-created superuser
service_urlstring""The public URL of the service. Used for PAPERLESS_URL.

External Integration

VariableTypeDefaultDescription
redis_hoststringnullRedis host IP or hostname. Leave empty to use NFS server IP
redis_portstring"6379"Redis TCP port
enable_redisbooltrueEnable Redis as the Celery broker and result backend
redis_authstring""Redis AUTH password. Sensitive
nfs_server_ipstringnullNFS server IP used as Redis host fallback when enable_redis = true and no redis_host provided

4. Health Probes

All probes target GET / (the Paperless-ngx login page, which returns HTTP 200 when fully initialised):

ProbeInitial DelayTimeoutPeriodFailure ThresholdPurpose
Startup60s10s10s30Allows roughly 360s total (60s delay + 30 × 10s) for Paperless-ngx to complete database migrations and start Celery workers
Liveness60s10s30s3Restarts the container if Paperless-ngx becomes unresponsive

Why the generous startup threshold: Paperless-ngx applies Django database migrations on first boot. On a fresh database, this can include dozens of migration steps — particularly for the initial schema creation covering documents, tags, correspondents, document types, and custom fields. The failure_threshold = 30 with period_seconds = 10 gives roughly 360 seconds of total startup tolerance (including the 60s initial delay), covering even slow Cloud SQL instances with many pending migrations.

Unlike Ghost Common, Paperless Common does not define a readiness probe — startup and liveness are sufficient for the Cloud Run and GKE deployment models.


5. Initialization Job

One db-init job runs by default (when initialization_jobs = []):

FieldValue
ImagePostgreSQL client image
Scriptscripts/db-init.sh
Secrets requiredDB_PASSWORD (app user)
execute_on_applytrue
Timeout600s, 1 retry

db-init.sh behaviour:

  1. Resolves the Cloud SQL connection from DB_HOST (Unix socket path under /cloudsql when enable_cloudsql_volume = true).
  2. Polls the database using the PostgreSQL client (up to 30 retries, 2s apart) until it is reachable.
  3. Creates the application user (paperless) with the password from Secret Manager (DB_PASSWORD) if it does not already exist.
  4. Creates the paperless database if it does not already exist, owned by the application user.
  5. Grants all necessary privileges on the database.
  6. Signals Cloud SQL Proxy shutdown after completion.

Override initialization_jobs with a non-empty list to replace this default with custom jobs. Each custom job must specify at least one of command, args, or script_path.


6. Secrets Created by Paperless Common

Unlike Ghost Common (which creates no secrets), Paperless Common auto-provisions two Secret Manager secrets at apply time. These secrets are created in the same GCP project as the deployment and are injected into the container at runtime via the platform module's module_secret_env_vars mechanism.

Secret Name PatternEnv VarValueRotation
secret-<resource-prefix>-paperless-admin-passwordPAPERLESS_ADMIN_PASSWORD24-character random alphanumeric stringNot rotated automatically. Retrieve from Secret Manager for first login.
secret-<resource-prefix>-paperless-keyPAPERLESS_SECRET_KEY64-character random alphanumeric stringNot rotated automatically. Rotating this value invalidates all active user sessions.

Important: The PAPERLESS_ADMIN_PASSWORD secret is consumed only during the initial superuser creation. After the first Paperless-ngx startup, the admin user exists in the PostgreSQL database and the password can be changed via the Paperless-ngx UI. However, the Secret Manager secret retains the original value — use gcloud secrets versions access latest to retrieve it for initial login if the password has not been changed.

PAPERLESS_SECRET_KEY stability: Django uses this key for session signing, CSRF tokens, and password reset links. If the key is regenerated (e.g., by destroying and redeploying the module), all existing user sessions are invalidated and all outstanding password reset links become invalid. This is intentional security behaviour.


7. Paperless-ngx Environment Variable Assembly

Paperless Common assembles the following environment variables and injects them into the config.environment_variables map, which the platform module passes to the container at runtime. These are merged with any additional environment_variables passed by the wrapper module.

Environment VariableSourceDescription
PAPERLESS_PORTHardcoded: "8000"Gunicorn server port
PAPERLESS_DBENGINEHardcoded: "postgresql"Database engine
PAPERLESS_DBPORTHardcoded: "5432"PostgreSQL port
PAPERLESS_MEDIA_ROOTHardcoded: "/usr/src/paperless/media"GCS Fuse mount point for persistent document storage
PAPERLESS_DATA_ROOTHardcoded: "/usr/src/paperless/data"Ephemeral metadata directory
PAPERLESS_CONSUMPTION_DIRHardcoded: "/usr/src/paperless/consume"Drop folder for automatic document ingestion
PAPERLESS_URLvar.service_urlPublic URL of the service. Set to the predicted Cloud Run or load balancer URL.
PAPERLESS_ALLOWED_HOSTSHardcoded: "*"Django allowed hosts. * permits all host headers.
PAPERLESS_CORS_ALLOWED_HOSTSvar.service_url (or "*" when unset)Django CORS allowed hosts.
PAPERLESS_TIME_ZONEvar.time_zoneTimezone for document date parsing and scheduled tasks
PAPERLESS_OCR_LANGUAGEvar.ocr_languageTesseract language code(s) for OCR
PAPERLESS_WEBSERVER_WORKERSHardcoded: "2"Number of gunicorn workers
USERMAP_UIDHardcoded: "1000"UID for the container user
USERMAP_GIDHardcoded: "1000"GID for the container user
PAPERLESS_ADMIN_USERvar.admin_userUsername for the auto-created superuser
PAPERLESS_ADMIN_MAILvar.admin_emailEmail for the auto-created superuser
PAPERLESS_TIKA_ENABLEDHardcoded: "false"Tika/Gotenberg office document conversion. Enable via environment_variables override
PAPERLESS_REDISAssembled from redis_host/redis_portRedis URL for Celery broker. Format: redis://:auth@host:port or redis://host:port

Redis URL assembly: The PAPERLESS_REDIS variable is assembled at apply time:

  • If redis_auth is non-empty: redis://:${redis_auth}@${redis_host}:${redis_port}
  • Otherwise: redis://${redis_host}:${redis_port}
  • If redis_host is empty (or null): the NFS server IP is substituted — nfs_server_ip when provided, otherwise the $(NFS_SERVER_IP) runtime placeholder that App_CloudRun / App_GKE resolve at deploy time.
  • If enable_redis = false: redis://localhost:6379 is used.

8. Scripts and Container Image

All supporting files are in scripts/. The scripts/ directory is used as the Docker build context.

Dockerfile

Thin wrapper around the public ghcr.io/paperless-ngx/paperless-ngx:<version> image:

  • Switches to USER root and copies entrypoint.sh in as /platform-entrypoint.sh.
  • Exposes port 8000.
  • Sets ENTRYPOINT ["/platform-entrypoint.sh"], replacing the upstream entrypoint.

entrypoint.sh

Runs before the upstream Paperless-ngx process starts:

  • Maps the platform-injected DB_HOST/DB_IP/DB_USER/DB_NAME/DB_PASSWORD env vars onto the Paperless-ngx-native PAPERLESS_DBHOST/PAPERLESS_DBUSER/PAPERLESS_DBNAME/PAPERLESS_DBPASS vars (preferring DB_IP over a socket-path DB_HOST since Paperless-ngx needs a TCP hostname/IP, not a Unix socket).
  • Reconstructs PAPERLESS_REDIS from NFS_SERVER_IP if it still contains an unresolved $(NFS_SERVER_IP) placeholder (Cloud Run only resolves $(VAR) references in declaration order).
  • execs /init — the upstream image's s6-overlay supervisor, which starts gunicorn (the web process) and the Celery worker/beat processes. The PAPERLESS_WEBSERVER_WORKERS=2 environment variable controls the gunicorn worker count; the Celery worker count is managed by Paperless-ngx internally.

db-init.sh

PostgreSQL initialisation script executed by the db-init Cloud Run Job or Kubernetes Job during first deployment. See §5 for detailed behaviour.


9. GCS Fuse Media Volume

The paperless-media GCS bucket is the primary persistence layer for all processed Paperless-ngx content. Unlike other modules that use NFS for application data, Paperless-ngx mounts GCS directly via GCS Fuse at /usr/src/paperless/media.

Default volume configuration (when gcs_volumes = []):

FieldValue
name"paperless-media"
bucket_nameAuto-provisioned gcs-paperless<tenant-resource-prefix>-media bucket (the bucket name the foundation actually creates — app-scoped, tenant-prefixed)
mount_path"/usr/src/paperless/media"
readonlyfalse
mount_options["implicit-dirs", "stat-cache-ttl=60s", "type-cache-ttl=60s", "uid=1000", "gid=1000", "file-mode=0664", "dir-mode=0775"] — the uid/gid options make the mount writable by the non-root Paperless-ngx user on GKE's GCS Fuse CSI driver

Directory structure under /usr/src/paperless/media:

/usr/src/paperless/media/
├── documents/
│ ├── originals/ # Original uploaded files (PDF, images, office docs)
│ ├── archive/ # OCR-processed, searchable PDFs (if archiving enabled)
│ └── thumbnails/ # Preview thumbnails for the web UI

PAPERLESS_DATA_ROOT (/usr/src/paperless/data) is intentionally ephemeral — it contains lock files, the SQLite database used for task state (in addition to PostgreSQL), and index files. These are recreated on startup and do not need to persist across Cloud Run revisions.

PAPERLESS_CONSUMPTION_DIR (/usr/src/paperless/consume) is the drop folder for document ingestion. Documents placed in this directory are automatically picked up by the Celery consumption task and moved to /usr/src/paperless/media/documents/originals after processing. In Cloud Run, this directory is ephemeral — for automated consumption, use the Paperless-ngx REST API or the web UI upload rather than filesystem drops.


10. Platform-Specific Differences

AspectPaperless CloudRunPaperless GKE
min_instance_count0 (default in variables.tf; user-configurable)1 (default in variables.tf; user-configurable)
max_instance_count3 (default in variables.tf; user-configurable)3 (default in variables.tf; user-configurable)
GCS FuseMounted via Cloud Run native GCS Fuse volumeMounted via GCS Fuse CSI Driver
PAPERLESS_URLSet to predicted Cloud Run service URLSet to load balancer or custom domain URL
RedisRequired; defaults to NFS server IP when redis_host blankRequired; defaults to NFS server IP when redis_host blank
db-init jobCloud Run JobKubernetes Job
PAPERLESS_CONSUMPTION_DIREphemeral (local to instance)Ephemeral unless NFS-mounted
Session affinityNot applicable (stateless Cloud Run)ClientIP by default — prevents Django session routing issues

11. Implementation Pattern

# Example: how Paperless_CloudRun instantiates Paperless_Common

module "paperless_app" {
source = "../Paperless_Common"

application_version = var.application_version
db_name = var.db_name
db_user = var.db_user
cpu_limit = var.cpu_limit
memory_limit = var.memory_limit
description = var.description
startup_probe = var.startup_probe
liveness_probe = var.liveness_probe
enable_cloudsql_volume = var.enable_cloudsql_volume
time_zone = var.time_zone
ocr_language = var.ocr_language
admin_user = var.admin_user
admin_email = var.admin_email
enable_redis = var.enable_redis
redis_host = var.redis_host
redis_port = var.redis_port
redis_auth = var.redis_auth
}

locals {
application_modules = { paperless = module.paperless_app.config }
module_env_vars = {}
module_secret_env_vars = module.paperless_app.secret_ids
module_storage_buckets = module.paperless_app.storage_buckets
scripts_dir = abspath("${module.paperless_app.path}/scripts")
}

module "app_cloudrun" {
source = "../App_CloudRun"

application_config = local.application_modules
module_secret_env_vars = local.module_secret_env_vars
module_storage_buckets = local.module_storage_buckets
scripts_dir = local.scripts_dir
# ... other inputs
}

12. Exploring with the GCP Console

After deployment, the following GCP Console areas are most relevant to Paperless Common-managed resources.

Secret Manager — Application Secrets Navigate to Security → Secret Manager. Filter by the deployment prefix. The two secrets created by Paperless Common are:

  • secret-<resource-prefix>-paperless-admin-password: Click View secret value on the latest version to retrieve the initial admin password for first login.
  • secret-<resource-prefix>-paperless-key: The Django secret key. No need to view this in normal operation — it is injected automatically into the container.

For each secret, the Versions tab shows all historical versions. Paperless Common creates a single version on the first apply. Secret Manager retains all versions until explicitly disabled or destroyed.

Cloud Storage — Media Bucket Navigate to Cloud Storage → Buckets → gcs-paperless<tenant-resource-prefix>-media. This bucket holds all Paperless-ngx persistent document data. Key directories to inspect:

  • documents/originals/ — Original uploaded files, organised by year/month/day.
  • documents/thumbnails/ — JPEG thumbnail previews for the web UI.
  • documents/archive/ — OCR-processed searchable PDFs (if archive mode is enabled in Paperless-ngx settings).

The bucket's Permissions tab shows that the Cloud Run service account has roles/storage.objectAdmin on this bucket, granted by App_CloudRun.

Cloud SQL — Database Verification Navigate to SQL → Instances → <instance-name> → Databases. The paperless database created by db-init is listed here. Click the database name to see its character set and collation (UTF8 / en_US.UTF-8 for PostgreSQL 15).


13. Exploring with gcloud

# Retrieve the initial admin password for first login
gcloud secrets versions access latest \
--secret="secret-RESOURCE_PREFIX-paperless-admin-password" \
--project=PROJECT_ID

# List all Secret Manager secrets created by Paperless Common
gcloud secrets list \
--project=PROJECT_ID \
--filter="name:paperless" \
--format="table(name,replication.automatic,createTime)"

# List versions of the admin password secret
gcloud secrets versions list secret-RESOURCE_PREFIX-paperless-admin-password \
--project=PROJECT_ID \
--format="table(name,state,createTime)"

# Inspect the media bucket contents (processed documents)
# SERVICE_NAME = paperless<tenant-resource-prefix>
gcloud storage ls --recursive gs://gcs-SERVICE_NAME-media/documents/

# Count objects in the media bucket (useful for monitoring growth)
gcloud storage ls --recursive gs://gcs-SERVICE_NAME-media/ | wc -l

# Check bucket size
gcloud storage du gs://gcs-SERVICE_NAME-media/ \
--summarize \
--readable-sizes

# Verify bucket IAM bindings (confirm Cloud Run SA has objectAdmin)
gcloud storage buckets get-iam-policy gs://gcs-SERVICE_NAME-media

# List all Cloud Run Jobs (db-init lives here)
gcloud run jobs list \
--project=PROJECT_ID \
--region=REGION \
--format="table(name,metadata.creationTimestamp)"

# Check the db-init job execution history (job name = SERVICE_NAME-db-init)
gcloud run jobs executions list \
--job=SERVICE_NAME-db-init \
--project=PROJECT_ID \
--region=REGION \
--format="table(name,completionStatus,startTime,completionTime)"

# Re-run the db-init job (idempotent — safe to run again)
gcloud run jobs execute SERVICE_NAME-db-init \
--project=PROJECT_ID \
--region=REGION \
--wait

# Stream db-init job logs
gcloud logging read \
'resource.type="cloud_run_job" AND resource.labels.job_name="SERVICE_NAME-db-init"' \
--project=PROJECT_ID \
--freshness=1h \
--format="table(timestamp,textPayload)" \
--order=asc

# Verify PostgreSQL database was created (connect via Cloud SQL proxy)
# First: start the proxy locally
cloud_sql_proxy -instances=PROJECT_ID:REGION:SQL_INSTANCE_NAME=tcp:5432 &
psql -h 127.0.0.1 -U paperless -d paperless -c "\dt" # list tables

# Check Redis connectivity (if using Memorystore)
gcloud redis instances describe REDIS_INSTANCE_NAME \
--region=REGION \
--project=PROJECT_ID \
--format="table(name,host,port,state,authEnabled)"