Skip to content

Object storage, TLS and split-origin deploys

Task attachments are the only user data TruePPM writes outside PostgreSQL. The local FileSystemStorage default is ephemeral in a container, and production refuses to boot on it, so a durable deploy points TRUEPPM_DEFAULT_FILE_STORAGE at an S3-compatible bucket.

The API image bundles the S3 backend, so the two variables below are all a deploy against AWS S3 needs:

Terminal window
TRUEPPM_DEFAULT_FILE_STORAGE=storages.backends.s3.S3Storage
TRUEPPM_S3_BUCKET_NAME=trueppm-attachments

Credentials are deliberately not required. Left unset, the AWS SDK resolves them from its own chain — IRSA on EKS, an IAM instance profile, or ~/.aws — which is preferable to pinning static keys into a Secret. Set TRUEPPM_S3_ACCESS_KEY_ID / TRUEPPM_S3_SECRET_ACCESS_KEY only when no such role is available (MinIO, SeaweedFS, Ceph, Wasabi).

VariableDefaultDescription
TRUEPPM_S3_BUCKET_NAME(empty)Bucket that holds task attachments. Required when TRUEPPM_DEFAULT_FILE_STORAGE names an S3 backend — startup fails with trueppm.E008 if it is missing, rather than accepting the config and failing on the first upload.
TRUEPPM_S3_ENDPOINT_URL(empty)Endpoint for a non-AWS S3-compatible store, e.g. http://minio:9000. Leave empty for AWS S3 so the SDK resolves the real regional endpoint.
TRUEPPM_S3_REGION_NAMEus-east-1Region for the bucket. Must be non-empty even against MinIO: SigV4 embeds the region in the credential scope, so an empty value produces an unusable signature.
TRUEPPM_S3_ADDRESSING_STYLE(SDK default auto)Set to path for MinIO, SeaweedFS, and Ceph RGW, which do not serve virtual-hosted bucket URLs without per-bucket DNS. Leave unset for AWS S3.
TRUEPPM_S3_ACCESS_KEY_ID(empty)Static access key. Omit to use the SDK credential chain (IRSA / instance profile).
TRUEPPM_S3_SECRET_ACCESS_KEY(empty)Static secret key. Omit to use the SDK credential chain.
TRUEPPM_S3_SIGNATURE_VERSIONs3v4Signing algorithm for presigned URLs. Leave at the default — the SDK would otherwise fall back to the deprecated SigV2 whenever an endpoint URL is set, and AWS rejects SigV2 in every region created after 2014.
TRUEPPM_S3_QUERYSTRING_EXPIRE900Lifetime, in seconds, of the presigned URL returned by the attachment Get signed download URL action.
Terminal window
TRUEPPM_DEFAULT_FILE_STORAGE=storages.backends.s3.S3Storage
TRUEPPM_S3_BUCKET_NAME=trueppm-attachments
TRUEPPM_S3_ENDPOINT_URL=http://minio:9000
TRUEPPM_S3_ADDRESSING_STYLE=path
TRUEPPM_S3_ACCESS_KEY_ID=your-access-key
TRUEPPM_S3_SECRET_ACCESS_KEY=your-secret-key

Create the bucket before first use — TruePPM does not create it for you. Keep it private: attachments are reached only through a presigned URL, and a world-readable bucket makes that signature meaningless.

Terminal window
TRUEPPM_DEFAULT_FILE_STORAGE=storages.backends.s3.S3Storage
TRUEPPM_S3_BUCKET_NAME=trueppm-attachments
TRUEPPM_S3_ENDPOINT_URL=http://seaweedfs:8333
TRUEPPM_S3_ADDRESSING_STYLE=path
TRUEPPM_S3_ACCESS_KEY_ID=your-access-key
TRUEPPM_S3_SECRET_ACCESS_KEY=your-secret-key

SeaweedFS (Apache 2.0) exposes an S3-compatible gateway — weed server -s3 in the all-in-one binary, or a standalone weed s3 process — on port 8333 by default. Create the bucket before first use here too; TruePPM does not create it for you.

The image bundles S3 only. storages.backends.gcloud.GoogleCloudStorage and storages.backends.azure_storage.AzureStorage are recognized as signing-capable by the signed-URL action, but their client libraries are not installed — naming one fails startup with trueppm.E007 and the exact package to install:

(trueppm.E007) STORAGES['default']['BACKEND'] is set to
'storages.backends.gcloud.GoogleCloudStorage', which cannot be imported:
Could not load Google Cloud Storage bindings.
HINT: Install it with: pip install 'django-storages[google]' (this image
bundles django-storages[s3] only), or point TRUEPPM_DEFAULT_FILE_STORAGE at
a backend the image carries.

To run on GCS or Azure Blob, install the extra into a derived image:

FROM registry.gitlab.com/trueppm/trueppm/api:latest
USER root
RUN pip install --no-cache-dir 'django-storages[google]'
USER trueppm

The attachment Get signed download URL action returns a time-limited URL only when it can confirm the backend genuinely signs one. On the S3 backend above it does, and the URL carries a real X-Amz-Expires. On FileSystemStorage — or any backend the action does not recognize — it refuses with 501 Not Implemented rather than hand back a permanent link labeled “signed”. If you run a signing-capable backend that is not recognized, opt in with TRUEPPM_ATTACHMENT_STORAGE_SIGNS_URLS=true.

The caller may request a lifetime with ?ttl=<seconds> (default 900, hard-capped at 3600). That value is applied to the signature itself, so the expires_at in the response and the URL’s real expiry always agree. TRUEPPM_S3_QUERYSTRING_EXPIRE sets the default used when a request omits ttl.

TRUEPPM_SECURE_SSL_REDIRECT controls whether the prod settings module issues an HTTP→HTTPS redirect for every request. It is opt-in and defaults to false for a reason that trips up a first deploy: most self-hosted installs terminate TLS at an ingress or load balancer and speak plain HTTP from there to the app pod, including the Kubernetes liveness/readiness probes hitting /api/v1/health/ and /api/v1/edition/. An unconditional redirect in that topology would 301-loop the probes and take the pod out of rotation.

VariableDefaultWhat it does
TRUEPPM_SECURE_SSL_REDIRECTfalseWhen true, prod redirects every non-HTTPS request to HTTPS, using SECURE_PROXY_SSL_HEADER (X-Forwarded-Proto) to detect the original scheme behind a proxy.

There is no companion variable for the host, deliberately. prod pins USE_X_FORWARDED_HOST = False and USE_X_FORWARDED_PORT = False: the scheme has to come from the proxy because the container cannot know it, but the host does not, and no proxy TruePPM ships sets X-Forwarded-Host. Trusting it would mean believing a header only the client could have written. When your edge rewrites Host, set TRUEPPM_PUBLIC_API_BASE_URL instead — a value you control, rather than a belief about a header TruePPM cannot verify.

Turn it on only when TruePPM itself terminates TLS or otherwise receives the original request scheme reliably — for example, a deployment that exposes the app directly over HTTPS with no intervening proxy, or a proxy configured to forward X-Forwarded-Proto correctly. The Kubernetes health-probe paths (/api/v1/health/, /api/v1/readyz, /api/v1/edition/) are always exempt from the redirect regardless of this setting, so turning it on never breaks the probes even if they are reached over plain HTTP.

This setting has no effect outside trueppm_api.settings.prod — the dev settings module never enforces an HTTPS redirect.

Serve the SPA, the API, and the WebSocket endpoint from one hostname, routed by path — / to the SPA, /api/ and /ws/ to Django. Every shipped topology (the Docker Compose nginx templates, the published web image, and the Helm chart’s default ingress.hosts) already does this, so a standard deploy needs no origin configuration at all: the secure defaults assume a single origin and are correct as shipped.

Four variables must all describe that one origin — DOMAIN, ALLOWED_HOSTS, TRUEPPM_FRONTEND_BASE_URL, and (with OIDC) TRUEPPM_PUBLIC_API_BASE_URL. See One origin, four variables for the full table and what each mismatch looks like.

Same-origin is the requirement for the SPA reaching its own API. The three settings remain useful for narrower cases on a single-origin deploy:

SettingStill needed when
CSRF_TRUSTED_ORIGINSA proxy in front of TruePPM rewrites the Origin or Referer header, so Django’s CSRF check sees an origin that is not the one it served. Add the origin the browser actually sends.
CSP_CONNECT_SRCThe page must open connections to something other than TruePPM — an analytics endpoint, or an object store you serve attachment downloads from directly. Add that origin; do not remove 'self'.
AUTH_REFRESH_COOKIE_SAMESITEYou embed TruePPM in an iframe on another site, where SameSite=Strict suppresses the refresh cookie. None requires Secure, which is already enforced, and the TRUEPPM_AUTH_REFRESH_COOKIE_SAMESITE_NONE_ACK sentinel (0.4) or it is refused at boot. Note the default CSP sets frame-ancestors 'none', so framing needs that changed too. Relaxing this setting does not relax the refresh/logout endpoints’ independent Sec-Fetch-Site / Origin check (0.4) — a cross-site Sec-Fetch-Site, or an Origin matching neither same-origin nor CSRF_TRUSTED_ORIGINS, is still rejected regardless of SameSite. A request with neither header (the mobile app, a non-browser API client) is unaffected either way.