Skip to content

YAML Config File

The YAML config file provides a convenient way to set all club options in a single file instead of individual environment variables. Environment variables always take precedence over values in the config file.

Priority order: Environment variable > YAML config file > Default value

File Location

club looks for the config file in this order:

  1. The path specified by the CONFIG environment variable
  2. /etc/club/config.yaml (default location)
Terminal window
# Explicit path
export CONFIG=/opt/club/config.yaml
# Or use the default location
# /etc/club/config.yaml

Configuration Loading Order

When the server starts, configuration is loaded in this sequence:

  1. If CONFIG is set, load the YAML file it points to
  2. Otherwise, check for /etc/club/config.yaml (skipped silently if absent)
  3. Apply environment variables as overrides
  4. Validate required fields and backend-specific requirements
  5. Fail fast with a clear error message if misconfigured

Environment Variable Substitution

Use {{ENV_VAR_NAME}} syntax to reference environment variables inside YAML values. This keeps secrets out of the config file while still centralising your configuration.

jwt_secret: "{{JWT_SECRET}}"
db:
postgres_url: "{{POSTGRES_URL}}"
blob:
s3:
access_key: "{{S3_ACCESS_KEY}}"
secret_key: "{{S3_SECRET_KEY}}"

If a referenced environment variable is not set, the substitution results in an empty string. The server’s validation step will catch missing required values and report a clear error.

Key shape: flat or nested

The loader accepts both flat snake_case keys at the top level and nested sections for db, blob, search. Both forms are equivalent; pick whichever reads better for your deployment.

# Flat
db_backend: postgres
postgres_url: "{{POSTGRES_URL}}"
# Nested (equivalent)
db:
backend: postgres
postgres_url: "{{POSTGRES_URL}}"

Full Annotated Example

# =============================================================================
# club configuration file
# =============================================================================
# Values can reference environment variables: {{ENV_VAR_NAME}}
# Environment variables always override values in this file.
# =============================================================================
# ---------------------------------------------------------------------------
# Server
# ---------------------------------------------------------------------------
# Public URL of the club server (required).
# Must include scheme (https://). No trailing slash.
server_url: "https://packages.example.com"
# IP address to bind the HTTP server to. Default: 0.0.0.0
host: "0.0.0.0"
# Port the server listens on inside the container. Default: 8080
# (Named "port" here; the equivalent env var is LISTEN_PORT, intentionally
# distinct from docker-compose's host-side PORT convention.)
port: 8080
# Log verbosity: debug, info, warning, error. Default: info
log_level: info
# ---------------------------------------------------------------------------
# Authentication & sessions
# ---------------------------------------------------------------------------
# JWT signing secret (required). Minimum 32 characters.
# Generate with: openssl rand -hex 32
jwt_secret: "{{JWT_SECRET}}"
# Web session TTL (hours). Default: 1
session_ttl_hours: 1
# Default API token expiry (days). Default: 365
token_expiry_days: 365
# bcrypt cost factor (10-14). Default: 12
bcrypt_cost: 12
# Enable the /signup page and public signup endpoint. Default: false
signup_enabled: false
# Trust X-Forwarded-Proto / X-Forwarded-For. Enable only behind a proxy
# that strips client-supplied copies. Default: false
trust_proxy: false
# Extra origins permitted on public, unauthenticated state-changing
# endpoints (login, signup, setup). SERVER_URL is trusted implicitly.
# allowed_origins: "https://www.packages.example.com,https://packages.internal.example.com"
# ---------------------------------------------------------------------------
# Database (Metadata Store)
# ---------------------------------------------------------------------------
db:
# sqlite | postgres. Default: sqlite
backend: sqlite
# SQLite file path. Default: /data/club.db
sqlite_path: /data/club.db
# Required when backend is postgres.
# postgres_url: "{{POSTGRES_URL}}"
# ---------------------------------------------------------------------------
# Blob Storage
# ---------------------------------------------------------------------------
blob:
# filesystem | s3 | gcs. Default: filesystem
backend: filesystem
# Filesystem root. Default: /data/packages
path: /data/packages
# S3 configuration (uncomment when backend is s3).
# Works for AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces, Backblaze B2,
# and GCS via its S3 interop endpoint.
# s3:
# # Optional for AWS (uses default endpoint). Required for everything else.
# endpoint: "https://s3.amazonaws.com"
# bucket: "club-packages"
# region: "us-east-1"
# access_key: "{{S3_ACCESS_KEY}}"
# secret_key: "{{S3_SECRET_KEY}}"
# GCS (native) configuration (uncomment when backend is gcs).
# Auth priority: credentials_file > credentials_json > Application
# Default Credentials. Under ADC there's no private key in process,
# so downloads proxy through the server instead of redirecting.
# gcs:
# bucket: "my-project.appspot.com"
# credentials_file: "/secrets/sa.json"
# # credentials_json: "{{GCS_CREDENTIALS_JSON}}"
# ---------------------------------------------------------------------------
# Search
# ---------------------------------------------------------------------------
search:
# sqlite (FTS5) | meilisearch. Default: sqlite
backend: sqlite
# Meilisearch configuration (uncomment when backend is meilisearch)
# meilisearch:
# url: "http://meilisearch:7700"
# key: "{{MEILISEARCH_KEY}}"
# ---------------------------------------------------------------------------
# Upload
# ---------------------------------------------------------------------------
# Temp dir for upload processing. Default: /tmp/club-uploads
temp_dir: /tmp/club-uploads
# Max tarball size in bytes. Default: 104857600 (100 MiB)
max_upload_bytes: 104857600
# Dartdoc output directory. Default: /data/docs
dartdoc_path: /data/docs

YAML Key to Environment Variable Mapping

The YAML keys map directly to their corresponding environment variables:

YAML KeyEnvironment Variable
server_urlSERVER_URL
hostHOST
portLISTEN_PORT
log_levelLOG_LEVEL
jwt_secretJWT_SECRET
session_ttl_hoursSESSION_TTL_HOURS
token_expiry_daysTOKEN_EXPIRY_DAYS
bcrypt_costBCRYPT_COST
signup_enabledSIGNUP_ENABLED
trust_proxyTRUST_PROXY
allowed_originsALLOWED_ORIGINS
db.backendDB_BACKEND
db.sqlite_pathSQLITE_PATH
db.postgres_urlPOSTGRES_URL
blob.backendBLOB_BACKEND
blob.pathBLOB_PATH
blob.s3.endpointS3_ENDPOINT
blob.s3.bucketS3_BUCKET
blob.s3.regionS3_REGION
blob.s3.access_keyS3_ACCESS_KEY
blob.s3.secret_keyS3_SECRET_KEY
blob.gcs.bucketGCS_BUCKET
blob.gcs.credentials_fileGCS_CREDENTIALS_FILE
blob.gcs.credentials_jsonGCS_CREDENTIALS_JSON
search.backendSEARCH_BACKEND
search.meilisearch.urlMEILISEARCH_URL
search.meilisearch.keyMEILISEARCH_KEY
temp_dirTEMP_DIR
max_upload_bytesMAX_UPLOAD_BYTES
dartdoc_pathDARTDOC_PATH
static_files_pathSTATIC_FILES_PATH

Example: PostgreSQL + S3 Config

A production config using PostgreSQL for metadata and S3 for blob storage:

server_url: "https://packages.example.com"
log_level: info
trust_proxy: true
jwt_secret: "{{JWT_SECRET}}"
token_expiry_days: 90
db:
backend: postgres
postgres_url: "{{POSTGRES_URL}}"
blob:
backend: s3
s3:
bucket: "club-packages"
region: "us-east-1"
access_key: "{{S3_ACCESS_KEY}}"
secret_key: "{{S3_SECRET_KEY}}"
search:
backend: sqlite

Example: MinIO Config

Using MinIO as an S3-compatible blob store:

server_url: "https://packages.internal.example.com"
trust_proxy: true
jwt_secret: "{{JWT_SECRET}}"
db:
backend: postgres
postgres_url: "{{POSTGRES_URL}}"
blob:
backend: s3
s3:
endpoint: "http://minio:9000"
bucket: "club-packages"
region: "us-east-1"
access_key: "{{S3_ACCESS_KEY}}"
secret_key: "{{S3_SECRET_KEY}}"
search:
backend: meilisearch
meilisearch:
url: "http://meilisearch:7700"
key: "{{MEILISEARCH_KEY}}"

Example: GCS (native) on GKE

Using the native GCS backend with Application Default Credentials from a workload-identity service account:

server_url: "https://packages.example.com"
trust_proxy: true
jwt_secret: "{{JWT_SECRET}}"
db:
backend: postgres
postgres_url: "{{POSTGRES_URL}}"
blob:
backend: gcs
gcs:
bucket: "my-project.appspot.com"
# credentials_file / credentials_json both unset — use ADC.
# Downloads proxy through the server (no V4 signing without a private key).