Skip to content
loco
v1.0
★ 6.9k Get started

Configuration

This page is a dictionary of every key Loco’s configuration loader understands. It documents struct Config (src/config/mod.rs) and its sub-structs (src/config/{auth,server,database,logger,mailer,queue,cache}.rs). For a narrative walkthrough of settings and environments, see the-app/your-project.

Loading & precedence

  • Default config folder: config/ (Config::new, src/config/mod.rs:128-131). Override with the LOCO_CONFIG_FOLDER env var (read by Environment::load, src/environment.rs:59-64).

  • Config::from_folder(env, path) (src/config/mod.rs:153-174) resolves the file to load with this precedence — first file that exists wins:

    1. {path}/{env}.local.yaml
    2. {path}/{env}.yaml

    If neither exists, loading fails with Error::Message("no configuration file found in folder: ...").

  • Before parsing, the entire YAML file is rendered as a Tera template (src/config/mod.rs:170, src/tera.rs:5-8, Tera::one_off(.., autoescape=false)). get_env(name=.., default=..) used throughout the shipped config files is Tera’s own built-in function — it is not a Loco-registered function.

  • Parse failures raise Error::YAMLFile(err, path) (src/config/mod.rs:172-173).

  • Config implements Display by dumping itself back to YAML (src/config/mod.rs:191-196).

  • Config::get_jwt_config(&self) -> Result<&JWT> (src/config/mod.rs:180-188) returns an error if auth or auth.jwt is absent.

Environment resolution

environment::resolve_from_env() (src/environment.rs:32-38) picks the active environment name with this precedence:

  1. LOCO_ENV
  2. RAILS_ENV
  3. NODE_ENV
  4. fallback: "development" (DEFAULT_ENVIRONMENT, src/environment.rs:21)

Top-level Config

struct Configsrc/config/mod.rs:62-92. Every field is a top-level YAML key.

KeyTypeRequired?Notes
loggerLoggerrequiredmod.rs:64
serverServerrequiredmod.rs:65
databaseDatabaserequired, only when the with-db feature is enabled#[cfg(feature = "with-db")], mod.rs:66-67
cacheCacheConfigoptional — #[serde(default)], defaults to Nullmod.rs:68-69
queueOption<QueueConfig>optionalmod.rs:70
authOption<Auth>optionalmod.rs:71
workersWorkersoptional — #[serde(default)]mod.rs:72-73
mailerOption<Mailer>optionalmod.rs:74
initializersOption<Initializers> (= Option<BTreeMap<String, serde_json::Value>>)optionalmod.rs:75, type alias at mod.rs:106
settingsOption<serde_json::Value>optional — #[serde(default)]mod.rs:88-89; free-form app settings, surfaced at ctx.config.settings
schedulerOption<scheduler::Config>optionalmod.rs:91; struct owned by the scheduler area, not detailed here

auth

struct Authsrc/config/auth.rs:13-17.

auth:
jwt:
location: # optional, default: Bearer
from: Bearer # or: {from: Query, name: <string>} / {from: Cookie, name: <string>}
secret: <base64 secret> # required — must be valid base64
expiration: 604800 # required, u64 seconds (e.g. 7 days)
KeyTypeRequired?Notes
auth.jwtOption<JWT>optionalauth.rs:16
auth.jwt.locationOption<JWTLocationConfig>optional, default: Bearer (resolved by get_jwt_locations, src/controller/extractor/auth.rs:181-189)auth.rs:24
auth.jwt.secretStringrequiredauth.rs:26. Must be valid base64 — the JWT signer/verifier call EncodingKey/DecodingKey::from_base64_secret (src/auth/jwt.rs:83,108); a non-base64 string fails at token generation/validation time, not at config load time
auth.jwt.expirationu64 (seconds)requiredauth.rs:28

JWTLocationConfig (#[serde(untagged)], auth.rs:47-54) accepts either form:

  • Single(JWTLocation) — a single location map
  • Multiple(Vec<JWTLocation>) — a YAML list of location maps, tried in order until one yields a token

JWTLocation (#[serde(tag = "from")], auth.rs:35-44):

VariantYAML shapeNotes
Bearerfrom: Bearerreads the Authorization: Bearer <token> header
Query { name }from: Query
name: <string>
reads a query-string parameter
Cookie { name }from: Cookie
name: <string>
reads a cookie

Related, not YAML-configurable: default signing algorithm is HS512 (JWT_ALGORITHM, src/auth/jwt.rs:13), overridable in code via JWT::algorithm(..) (jwt.rs:51), not via config.

server

struct Serversrc/config/server.rs:29-45.

server:
binding: localhost # optional, default "localhost"
port: 5150 # required
host: http://localhost # required
ident: <string> # optional — overrides the `Server` response header
middlewares: {} # optional, default {} — see the middleware catalog reference
KeyTypeRequired?Notes
server.bindingStringoptional — #[serde(default = "default_binding")]"localhost"server.rs:33-34,47-49
server.porti32requiredserver.rs:36
server.hostStringrequiredserver.rs:38
server.identOption<String>optionalserver.rs:40. When set, replaces the Server header value
server.middlewaresmiddleware::Configoptional — #[serde(default)]server.rs:44. Struct is owned by the middleware area; see the middleware catalog reference for every middleware’s keys

Server::full_url() -> String returns "{host}:{port}" (server.rs:52-55).

workers

struct Workerssrc/config/server.rs:64-68.

workers:
mode: BackgroundQueue # optional, default BackgroundQueue
KeyTypeRequired?Notes
workers.modeWorkerModeoptional — Workers derives Defaultserver.rs:67

WorkerMode (server.rs:71-83):

VariantDefault?Notes
BackgroundQueueyesWorkers run asynchronously via a queue backend. Requires a configured queue
ForegroundBlockingnoWorkers run in-process and block the caller until the task completes
BackgroundAsyncnoWorkers run asynchronously in-process (async task, no external queue)

database

struct Databasesrc/config/database.rs:22-84. Only present/required when the with-db feature is enabled.

database:
uri: postgres://root:12341234@localhost:5432/myapp_development # required
enable_logging: true # required — SQLx statement logging
min_connections: 1 # required
max_connections: 1 # required
connect_timeout: 500 # required, milliseconds
idle_timeout: 500 # required, milliseconds
acquire_timeout: 500 # optional, milliseconds
auto_migrate: true # optional, default false
dangerously_truncate: false# optional, default false
dangerously_recreate: false# optional, default false
run_on_start: <sql/pragma> # optional
KeyTypeRequired?Notes
database.uriStringrequireddatabase.rs:28. E.g. postgres://... or sqlite://db.sqlite?mode=rwc
database.enable_loggingboolrequireddatabase.rs:31 — enables SQLx statement logging
database.min_connectionsu32requireddatabase.rs:34
database.max_connectionsu32requireddatabase.rs:37
database.connect_timeoutu64 (ms)requireddatabase.rs:40
database.idle_timeoutu64 (ms)requireddatabase.rs:43
database.acquire_timeoutOption<u64> (ms)optionaldatabase.rs:46
database.auto_migratebooloptional — #[serde(default)]database.rs:51-52. Runs pending migrations on boot; recommended for development, discouraged in production
database.dangerously_truncatebooloptional — #[serde(default)]database.rs:56-57. Deletes row data on boot; typically used in test
database.dangerously_recreatebooloptional — #[serde(default)]database.rs:63-64. Drops and recreates schema on boot
database.run_on_startOption<String>optionaldatabase.rs:83. Arbitrary SQL/PRAGMA statements executed once the connection is established. For SQLite, if unset, Loco applies its own PRAGMA defaults (foreign_keys=ON, journal_mode=WAL, synchronous=NORMAL, mmap_size=134217728, journal_size_limit=67108864, cache_size=2000, busy_timeout=5000)

Note: the db_min_conn()=1 / db_max_conn()=20 / db_connect_timeout()=500 / db_idle_timeout()=500 helper functions in database.rs:86-100 are not defaults for Database itself (whose numeric fields have no #[serde(default)] and are required) — they are reused as the #[serde(default = ...)] values for the Postgres/Sqlite queue configs below.

logger

struct Loggersrc/config/logger.rs:21-49.

logger:
enable: true # required
pretty_backtrace: false # optional, default false
level: debug # required — off|trace|debug|info|warn|error
format: compact # required — compact|pretty|json
override_filter: <str> # optional — EnvFilter directive string
file_appender: # optional
enable: true # required within block
non_blocking: false # optional, default false, within block
level: info # required within block
format: json # required within block
rotation: daily # required within block — minutely|hourly|daily|never
dir: ./logs # optional, default "./logs"
filename_prefix: <s> # optional
filename_suffix: <s> # optional
max_log_files: 7 # required within block
KeyTypeRequired?Notes
logger.enableboolrequiredlogger.rs:24
logger.pretty_backtracebooloptional — #[serde(default)]logger.rs:28-29. When true, forces nicely-formatted backtraces (development-friendly); turn off in performance-sensitive production deployments
logger.levellogger::LogLevelrequiredlogger.rs:34. Variants: off, trace, debug, info (enum’s own #[default], but the field itself has no #[serde(default)] so it must be present in YAML), warn, error (src/logger.rs:15-36)
logger.formatlogger::Formatrequiredlogger.rs:39. Variants: compact (#[default]), pretty, json (src/logger.rs:39-48)
logger.override_filterOption<String>optionallogger.rs:45. A tracing-subscriber EnvFilter directive string
logger.file_appenderOption<LoggerFileAppender>optionallogger.rs:48
logger.file_appender.enableboolrequired within blocklogger.rs:54
logger.file_appender.non_blockingbooloptional — #[serde(default)]logger.rs:57-58
logger.file_appender.levellogger::LogLevelrequired within blocklogger.rs:63
logger.file_appender.formatlogger::Formatrequired within blocklogger.rs:68
logger.file_appender.rotationlogger::Rotationrequired within blocklogger.rs:71. Variants: minutely, hourly (#[default]), daily, never (src/logger.rs:51-62)
logger.file_appender.dirOption<String>optional, defaults to "./logs" when unset (applied at file-appender init, src/logger.rs:113)logger.rs:76
logger.file_appender.filename_prefixOption<String>optionallogger.rs:79
logger.file_appender.filename_suffixOption<String>optionallogger.rs:82
logger.file_appender.max_log_filesusizerequired within blocklogger.rs:85

mailer

struct Mailersrc/config/mailer.rs:29-35.

# development: capture instead of sending
mailer:
stub: false # optional, default false
smtp:
enable: true # required
host: localhost # required
port: 1025 # required
secure: false # required — legacy shorthand, see below
# production: implicit TLS on port 465 (SMTPS)
mailer:
smtp:
enable: true
host: smtp.example.com
port: 465
tls: implicit # overrides `secure`; see below
auth:
user: postmaster@mg.example.com
password: "{{ get_env(name='SMTP_PASSWORD') }}"
hello_name: <string> # optional — EHLO client id
KeyTypeRequired?Notes
mailer.stubbooloptional — #[serde(default)]mailer.rs:33-34. When true, mail is captured rather than sent
mailer.smtpOption<SmtpMailer>optionalmailer.rs:31
mailer.smtp.enableboolrequiredmailer.rs:55
mailer.smtp.hostStringrequiredmailer.rs:57
mailer.smtp.portu16requiredmailer.rs:59
mailer.smtp.secureboolrequiredmailer.rs:65. Legacy shorthand: true selects STARTTLS (port 587), false selects cleartext
mailer.smtp.tlsOption<MailerTls>optional — #[serde(default)]mailer.rs:69. When set, overrides secure. Variants (#[serde(rename_all = "lowercase")], mailer.rs:38-50): starttls (opportunistic TLS, port 587 — what secure: true selects), implicit (TLS from the first byte, SMTPS, port 465 — required by providers that only accept implicit TLS), none (cleartext)
mailer.smtp.authOption<MailerAuth>optionalmailer.rs:71
mailer.smtp.auth.userStringrequired within blockmailer.rs:93
mailer.smtp.auth.passwordStringrequired within blockmailer.rs:95
mailer.smtp.hello_nameOption<String>optionalmailer.rs:73. EHLO client identifier, sent instead of the hostname

Effective TLS mode is resolved by SmtpMailer::tls_mode() (mailer.rs:76-87): if tls is set, it wins outright; otherwise secure: trueStarttls, secure: falseNone.

queue

enum QueueConfigsrc/config/queue.rs:5-14, #[serde(tag = "kind")] with variants Redis, Postgres, Sqlite.

# kind: Redis
queue:
kind: Redis
uri: redis://127.0.0.1 # required
dangerously_flush: false # optional, default false
queues: [high, low] # optional — priority order, first = most important
num_workers: 2 # optional, default 2
# reaper: # optional, disabled by default (opt-in)
# age_minutes: 10 # requeue jobs stuck in `processing` for longer than this
# interval_seconds: 60 # optional, default 60 — how often to sweep
# kind: Postgres
queue:
kind: Postgres
uri: postgres://... # required
dangerously_flush: false # optional, default false
enable_logging: false # optional, default false
max_connections: 20 # optional, default 20
min_connections: 1 # optional, default 1
connect_timeout: 500 # optional, default 500 (ms)
idle_timeout: 500 # optional, default 500 (ms)
poll_interval_sec: 1 # optional, default 1
num_workers: 2 # optional, default 2
# reaper: # optional, disabled by default (opt-in)
# age_minutes: 10 # requeue jobs stuck in `processing` for longer than this
# interval_seconds: 60 # optional, default 60 — how often to sweep
# kind: Sqlite (same shape as Postgres)
queue:
kind: Sqlite
uri: sqlite://...
poll_interval_sec: 1 # optional, default 1 (own default fn)
# ...remaining keys identical to Postgres, including the optional `reaper`
KeyTypeRequired?Notes
queue.kindtag: Redis | Postgres | Sqliterequiredqueue.rs:6-14
Redis (RedisQueueConfig, queue.rs:16-28)
queue.uriStringrequiredqueue.rs:18
queue.dangerously_flushbooloptional — #[serde(default)]queue.rs:20
queue.queuesOption<Vec<String>>optionalqueue.rs:24. Declares named priority queues; first entry is most important
queue.num_workersu32optional, default 2 (num_workers())queue.rs:26-27
queue.reaperOption<ReaperConfig>optional, default None (disabled)queue.rs:29-31. See below
Postgres (PostgresQueueConfig, queue.rs:30-57)
queue.uriStringrequiredqueue.rs:32
queue.dangerously_flushbooloptional, default falsequeue.rs:34-35
queue.enable_loggingbooloptional, default falsequeue.rs:37-38
queue.max_connectionsu32optional, default 20 (db_max_conn())queue.rs:40-41
queue.min_connectionsu32optional, default 1 (db_min_conn())queue.rs:43-44
queue.connect_timeoutu64 (ms)optional, default 500 (db_connect_timeout())queue.rs:46-47
queue.idle_timeoutu64 (ms)optional, default 500 (db_idle_timeout())queue.rs:49-50
queue.poll_interval_secu32optional, default 1 (pgq_poll_interval())queue.rs:52-53
queue.num_workersu32optional, default 2queue.rs:55-56
queue.reaperOption<ReaperConfig>optional, default None (disabled)queue.rs:57-59. See below
Sqlite (SqliteQueueConfig, queue.rs:59-86)
identical fields to Postgres, including queue.reaperpoll_interval_sec defaults via its own sqlt_poll_interval()=1 (queue.rs:81-82,92-94); all other defaults are shared with Postgres via the same db_* helper functions
ReaperConfig (queue.rs, all three backends)Opt-in visibility-timeout reaper: when set, the queue provider spawns a background task that periodically requeues jobs stuck in processing (e.g. after a worker crash), reusing the same logic as cargo loco jobs requeue. Leaving it unset keeps the previous behavior — no automatic requeue.
queue.reaper.age_minutesi64required (only if reaper is set)Requeue jobs that have been processing for longer than this many minutes
queue.reaper.interval_secondsu64optional, default 60 (default_reaper_interval_seconds())How often the reaper sweeps for stale jobs

cache

enum CacheConfigsrc/config/cache.rs:4-16, #[serde(tag = "kind")]. Default variant: Null (#[default], cache.rs:14-15) — this is what Config.cache’s #[serde(default)] produces when the cache key is omitted entirely.

cache:
kind: InMem # requires the `cache_inmem` feature
max_capacity: 33554432 # optional, default 33554432 bytes (32 MiB)
# --- or ---
cache:
kind: Redis # requires the `cache_redis` feature
uri: redis://... # required
max_size: 100 # required — max pool connections
# --- or (default) ---
cache:
kind: Null # no-op cache; used when `cache` key is omitted
KeyTypeRequired?Notes
cache.kindtag: InMem | Redis | Nullrequired if cache presentcache.rs:6-16
InMem (InMemCacheConfig, cache.rs:18-22) — feature-gated on cache_inmem
cache.max_capacityu64optional, default 33554432 (32 * 1024 * 1024, cache_in_mem_max_capacity())cache.rs:20-21,24-26
Redis (RedisCacheConfig, cache.rs:28-33) — feature-gated on cache_redis
cache.uriStringrequiredcache.rs:30
cache.max_sizeu32required — max pool connectionscache.rs:32
Null(no fields)default no-op cache

If the corresponding feature (cache_inmem / cache_redis) is not compiled in, that kind value will fail to deserialize.

initializers, settings, scheduler

  • initializers: Option<BTreeMap<String, serde_json::Value>> (mod.rs:75,106) — a free-form map consumed by app initializers (e.g. an oauth2 initializer reading initializers.oauth2). Keys and shapes are defined by whichever initializer reads them, not by Config itself.
  • settings: Option<serde_json::Value> (mod.rs:88-89) — arbitrary app-defined settings, deserialize your own type from ctx.config.settings.
  • scheduler: Option<scheduler::Config> (mod.rs:91) — struct and keys owned by the scheduler area; not detailed on this page.

Environment variables

VariablePurposeSource
LOCO_ENVSelects the active environment; highest precedencesrc/environment.rs:22,34
RAILS_ENVFalls back to this if LOCO_ENV is unsetsrc/environment.rs:23,35
NODE_ENVFalls back to this if both above are unsetsrc/environment.rs:24,36
LOCO_CONFIG_FOLDEROverrides the config/ folder Loco loads fromsrc/env_vars.rs:16, read in Environment::load (src/environment.rs:59-64)
LOCO_DATAData folder pathsrc/env_vars.rs:20
LOCO_POSTGRES_DB_OPTIONSExtra Postgres connection options (only meaningful with with-db)src/env_vars.rs:8
SCHEDULER_CONFIGPath to the scheduler config filesrc/env_vars.rs:18
RUST_BACKTRACEEffectively forced to 1 when logger.pretty_backtrace: truelogger init
any name passed to get_env(name=.., default=..) in a config YAML fileInjected into the rendered YAML by Tera’s built-in get_env function at load timesrc/tera.rs:5-8 (Tera, not Loco code)

Secrets (JWT secret, SMTP password, database uri credentials) are plain String fields with no dedicated vault type; the convention is to inject them via {{ get_env(name="...") }} at config-load time rather than hardcoding them in the YAML file.