Skip to main content

Configuration

storage-service reads configuration from environment variables. Copy .env.example to .env for local runs. Pass the same keys to Docker, Coolify, or your orchestrator in production.

An empty boolean uses the default. 1, true, yes, and on are true. Any other value, including false, is false. Byte sizes accept a bare integer or a K, M, G, or T suffix. Each suffix is a multiple of 1024, so 10G is 10737418240 bytes.

.env.example is generated from config/env_registry.py, the single list of settings. It keeps only the settings you need active. Optional ones are commented out with the default the code uses. After changing a setting, run python manage.py generate_env --example > .env.example. A test fails when the code, the registry, .env.example and this page drift apart.

Generate production settings​

manage.py generate_env prints every setting production needs, with a fresh SECRET_KEY. It runs before Django settings load, so it works with no env, database or Redis:

python manage.py generate_env --host storage.example.com
python manage.py generate_env --host storage.example.com --optional # also list optional settings with defaults

--host fills ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS. Replace every change-me. The output holds a secret: store it in a secret manager and do not commit it.

Required to start​

SECRET_KEY is required in every mode. Django uses it to sign sessions and CSRF tokens. With DEBUG=false it must be at least 50 characters and not a placeholder. generate_env makes one.

Required when DEBUG is false​

The Docker image sets DEBUG=false. Startup fails until these are set:

VariablePurpose
IDENTITY_ISSUERJWT iss must match this value
IDENTITY_AUDIENCEJWT aud must match this value
REDIS_URLShared cache and job broker. The entrypoint exits without it

Error messages name the missing setting and never print a value.

JWT_HS256_FALLBACK_SECRET is refused unless you also set ALLOW_JWT_HS256_FALLBACK=true. CORS_ALLOW_ALL_ORIGINS=true together with CORS_ALLOW_CREDENTIALS=true is also refused.

Pin a JSON Web Key Set (JWKS) with IDENTITY_JWKS_FILE or IDENTITY_JWKS in production. A URL fetch is allowed at startup, and it is the wrong choice on a host that cannot reach https://id.shellui.com from the container. Copy the document from outside the server and restart after identity-service rotates keys. See JWT and claim trust.

Runtime​

VariableDefaultPurpose
DEBUGfalseLocal mode. The image sets false. Compose passes the value from .env, where the example file sets true
LOG_LEVELINFO, or DEBUG when DEBUG=truePython log level
SETUP_TOKENemptyOne-time token for the web form that creates the first superuser when DEBUG=false
STORAGE_SERVICE_PORT8001Host port published by Docker Compose. Gunicorn inside the image listens on 8000
ALLOWED_HOSTSlocalhost, 127.0.0.1Comma-separated hostnames, no scheme
CSRF_TRUSTED_ORIGINSlocal dev origins and https://admin.shellui.comFull origins for Django admin form posts
DJANGO_ADMIN_ENABLEDtruefalse removes /admin/ routes

Identity and JWT​

VariableDefaultPurpose
IDENTITY_SERVICE_URLemptyidentity-service base URL. JWKS is {url}/.well-known/jwks.json when no document is pinned
IDENTITY_JWKS_URLderived, or http://localhost:8000/.well-known/jwks.jsonFetch URL when no document is pinned. Ignored for verification when a document is set
IDENTITY_JWKS_FILEemptyPath to a JWKS file. Relative paths are from the repository root. Wins over IDENTITY_JWKS
IDENTITY_JWKSemptyJWKS JSON in the environment, used when no file is set
IDENTITY_ISSUERemptyExpected iss. Required when DEBUG=false
IDENTITY_AUDIENCEemptyExpected aud. Required when DEBUG=false
JWKS_CACHE_TTL900Seconds to cache a fetched JWKS document. Unused for a pinned document
JWKS_TIMEOUT15Seconds for one JWKS HTTP call
JWKS_RETRIES2Extra attempts after a timeout or HTTP 5xx. HTTP 4xx is not retried
JWT_ALGORITHMSRS256Comma-separated algorithms accepted for RS256 verification
JWT_HS256_FALLBACK_SECRETemptyHS256 secret for local identity debug tokens. Set it to the identity SECRET_KEY
ALLOW_JWT_HS256_FALLBACKfalseAllow the HS256 secret when DEBUG=false

Issuer and audience checks run only when the matching variable is set. Production always sets both.

Object storage​

VariableDefaultPurpose
STORAGE_BACKENDfilesystemfilesystem or s3
MEDIA_ROOTdata/media, /app/data/media in DockerFilesystem root. Blobs live under objects/
STORAGE_KEY_PREFIXshelluiKey prefix in front of every blob path
SIGNED_URL_EXPIRES3600Maximum signed-URL lifetime in seconds. Client values above this are reduced

S3 settings apply when STORAGE_BACKEND=s3. AWS_STORAGE_BUCKET_NAME is required in that mode.

VariableDefaultPurpose
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYemptyS3 credentials
AWS_STORAGE_BUCKET_NAMEemptyBucket name
AWS_S3_REGION_NAMEus-east-1Region
AWS_S3_ENDPOINT_URLemptyCustom S3 endpoint, scheme and host only. Leave empty for AWS. MinIO in Compose is http://minio:9000
AWS_S3_CUSTOM_DOMAINemptyPublic host for the bucket, if it differs from the API endpoint. Leave empty unless you have a CDN
AWS_S3_SIGNATURE_VERSIONs3v4Signature version
AWS_S3_ADDRESSING_STYLEinferredpath for MinIO, virtual for AWS and OVH, or auto

POST /storage/v1/object/sign/… returns a cryptographic pre-signed URL on S3. On the filesystem backend it returns a plain /media/objects/… path. See Downloads and signed URLs.

Quotas and uploads​

VariableDefaultPurpose
DEFAULT_COMPANY_QUOTA_BYTES10GCompany cap applied when the quota row is created
DEFAULT_USER_QUOTA_BYTES0Default per-user cap. 0 leaves the per-user cap off
MAX_UPLOAD_BYTES5GHard cap for one upload. A bucket file_size_limit cannot raise it
DATA_UPLOAD_MAX_MEMORY_SIZE12MDjango in-memory request limit. The object is still capped by MAX_UPLOAD_BYTES

Staff and company owners can replace the company and per-user caps over the API. See Quotas.

WebDAV​

VariableDefaultPurpose
WEBDAV_ENABLEDtruefalse removes the WebDAV routes
WEBDAV_PATH_PREFIX/davURL prefix. Clients use /dav/{bucket}/…

WebDAV uses the same grants, quotas, and upload signals as the REST API. See WebDAV.

Database​

VariableDefaultPurpose
POSTGRES_DATABASE_URLemptyPostgreSQL DSN. Unset uses SQLite
SQLITE_PATHdb.sqlite3, /app/data/db.sqlite3 in DockerSQLite file
POSTGRES_SSL_REQUIREtrue when DEBUG=falseRequire TLS to PostgreSQL. Set false for a private database without TLS

Cache and scheduled jobs​

REDIS_URL is required when DEBUG=false. Redis is the shared cache and the broker for the scheduled jobs. Without it, the container logs REDIS_URL is required when DEBUG is false and exits with status 1, and manage.py check --deploy reports authapi.E004. That check still fails when SCHEDULER_ENABLED=false or when only CELERY_BROKER_URL is set.

With DEBUG=true and REDIS_URL unset, each process uses an in-memory cache and the scheduled jobs do not start. authapi.W001 remains a warning when production still uses that in-memory cache with more than one Gunicorn worker.

REDIS_URL=redis://redis:6379/0
VariableDefaultPurpose
SCHEDULER_ENABLEDtruefalse keeps the Celery worker out of this container
CELERY_BROKER_URLREDIS_URLA different Redis for the jobs. Does not replace REDIS_URL
CELERY_WORKER_CONCURRENCY2Threads in the worker
CELERY_BEAT_SCHEDULE_FILE/tmp/celerybeat-scheduleBeat schedule state file

Schedules, locks, the staff API and metrics are in Scheduled jobs.

CORS and HTTPS​

VariableDefaultPurpose
CORS_ALLOW_ALL_ORIGINStrueAllow browser calls from any origin
CORS_ALLOW_CREDENTIALSfalseMust stay false while all origins are allowed. That combination refuses to start
CORS_ALLOWED_ORIGINSlocal dev origins, plus extras you appendUsed when CORS_ALLOW_ALL_ORIGINS=false
CORS_ALLOW_PRIVATE_NETWORKtruePrivate Network Access preflight header
SECURE_SSL_REDIRECTon when DEBUG=falseRedirect HTTP to HTTPS
SECURE_HSTS_SECONDS31536000 when DEBUG=false, else 0HSTS max-age
SECURE_HSTS_INCLUDE_SUBDOMAINSon when DEBUG=falseInclude subdomains in HSTS
SECURE_HSTS_PRELOADfalseHSTS preload flag
SESSION_COOKIE_SECURE, CSRF_COOKIE_SECUREon when DEBUG=falseSecure attribute on Django admin cookies. A secure cookie is named __Host-sessionid or __Host-csrftoken, with no Domain. The CSRF cookie is always HttpOnly

API auth is the Bearer JWT, not a cookie. Permissive CORS is the default for that reason. Where the browser may return after login is the identity-service redirect allowlist, not this CORS list.

Gunicorn​

The image entrypoint runs migrations, check --deploy, then Gunicorn. In web mode it also starts the Celery worker and beat unless SCHEDULER_ENABLED=false or, with DEBUG=true, Redis is unset. worker mode starts only the scheduler. See Scheduled jobs.

VariableDefaultPurpose
GUNICORN_WORKERS2Worker processes
GUNICORN_THREADS2Threads per worker
GUNICORN_TIMEOUT120Seconds before Gunicorn kills a silent worker. Sized for large uploads
GUNICORN_GRACEFUL_TIMEOUT30Seconds a worker gets to finish requests on shutdown
GUNICORN_KEEP_ALIVE75Seconds to keep an idle connection open
GUNICORN_MAX_REQUESTS1000Requests before a worker restarts
GUNICORN_MAX_REQUESTS_JITTER200Random spread added to GUNICORN_MAX_REQUESTS

Shellui Actions​

Webhooks do not need Redis. These variables tune delivery. The catalog and the admin API are in Webhooks.

VariableDefaultPurpose
ACTIONS_OUTBOX_MAX_ATTEMPTS8Attempts before a delivery is dead
ACTIONS_WEBHOOK_TIMEOUT_SECONDS5Timeout of one webhook POST
ACTIONS_WEBHOOK_RETRY_LEASE_SECONDS120How long one retry run holds a row
ACTIONS_WEBHOOK_DISPATCH_WORKERS4Threads used for post-commit delivery
ACTIONS_WEBHOOK_SYNC_DELIVERYfalseDeliver inside the request. Leave false outside tests
ACTIONS_WEBHOOK_ALLOW_PRIVATEfalseAllow webhook URLs that resolve to private or loopback addresses. Unset stays off, including when DEBUG=true

Staff can also set allow private URLs on one rule. Changing the URL clears that flag unless a superuser sets it again.

Email notifications​

Set EMAIL_SERVICE_API_KEY to forward storage events to email-service. An empty key sends nothing. Webhook and email bodies omit sign-in links, tokens, signed URLs, and secret-shaped fields. See Email notifications.

VariableDefaultPurpose
EMAIL_SERVICE_URLhttps://email.shellui.comOrigin only. storage-service appends /api/v1/events
EMAIL_SERVICE_API_KEYemptyService key (esk_). Empty disables forwarding
EMAIL_SERVICE_ALLOW_PRIVATEfalseAllow a URL that resolves to a private or loopback address. Set this for an internal email-service URL

Event log​

VariableDefaultPurpose
EVENT_LOG_RETENTION_DAYS7Days to keep event-log rows and finished webhook and email deliveries. Values below 1 are treated as 1

purge_expired_data deletes older rows every hour inside the container. See Event log and Scheduled jobs.

Error reporting​

VariableDefaultPurpose
SENTRY_DSNemptyTurns on Sentry. No personal data is attached
SENTRY_ENVIRONMENTdevelopment or production from DEBUGSentry environment tag
SENTRY_RELEASEthe package versionSentry release tag
SENTRY_TRACES_SAMPLE_RATE0Share of requests traced. 0 reports errors only