A 12-Factor Service
A network service that follows 12-factor principles: configuration is read from the environment, with an optional config file for local development and CLI overrides for ad-hoc runs. Defaults are safe for production.
Precedence is exactly what an operator expects:
flags > env > file > built-in defaults.
src/main.rs
rust
use facet:: Facet ;
use facet_pretty:: FacetPretty ;
use figue::{ self as args, builder, Driver , FigueBuiltins };
/// A 12-factor HTTP service.
# [ derive ( Facet , Debug )]
struct App {
/// Configuration (env: `SVC__…`, file: `service.json`, CLI: `--config.…`)
# [ facet ( args :: config , args :: env_prefix = "SVC" )]
config : ServiceConfig ,
/// --help / --version / --completions / --export-jsonschemas
# [ facet ( flatten )]
builtins : FigueBuiltins ,
}
# [ derive ( Facet , Debug )]
struct ServiceConfig {
/// Interface to bind.
# [ facet ( default = "0.0.0.0" )]
host : String ,
/// TCP port. Override per-environment with `SVC__PORT`.
# [ facet ( default = 8080 )]
port : u16 ,
/// Database connection string. Required — there is no safe default.
/// Honors the conventional `$DATABASE_URL` as well as `SVC__DATABASE_URL`.
# [ facet ( args :: env_alias = "DATABASE_URL" )]
database_url : String ,
/// Worker pool size.
# [ facet ( default = 16 )]
workers : usize ,
/// Structured-logging toggle.
# [ facet ( default )]
json_logs : bool ,
/// TLS is off unless this whole block is configured.
# [ facet ( default )]
tls : Option < Tls >,
}
# [ derive ( Facet , Debug )]
struct Tls {
cert_path : String ,
key_path : String ,
}
fn main () {
let config = builder ::< App >()
. unwrap ()
. cli ( |cli| cli. args ( std:: env:: args (). skip ( 1 )))
. env ( |env| env) // real process environment
. file ( |f| f. default_paths ([
"./service.json" ,
"/etc/myservice/service.json" ,
]))
. help ( |h| h
. program_name ( env! ( "CARGO_PKG_NAME" ))
. version ( env! ( "CARGO_PKG_VERSION" ))
. description ( "Set SVC__DATABASE_URL (or $DATABASE_URL) before starting." ))
. build ();
let app: App = Driver :: new ( config). run (). unwrap ();
// `app.builtins.*` were already handled by `.unwrap()`.
eprintln! ( "effective configuration:\n{}" , app. config . pretty ());
serve ( app. config );
}
fn serve ( _cfg : ServiceConfig ) { /* … */ } Running it
bash
# Production: everything from the environment
SVC__DATABASE_URL =postgres://db/app SVC__PORT =443 myservice
# Local dev: a file supplies the boring parts, env supplies the secret
cat > service.json <<'JSON'
{ "config": { "port": 3000, "json_logs": false } }
JSON
DATABASE_URL =postgres://localhost/app_dev myservice
# Ad-hoc: override one thing without touching env or file
DATABASE_URL =postgres://localhost/app_dev myservice --config.workers 4Why this shape works
- Secrets stay in the environment.
database_urlhas no default, so a missing value is a loud, explained startup error — never a silent fallback.env_aliaslets it also accept the platform-conventional$DATABASE_URL. - The file is optional. No file? No problem — env + defaults still produce a valid config. The file is a developer convenience, not a requirement.
tls: Option<Tls>means TLS is genuinely off until an operator configures both paths; it is never half-synthesized from defaults.- Operators can introspect.
myservice --export-jsonschemas ./schemasgives them an editor-validatedservice.json, and any startup error prints the full provenance dump showing exactly which source set each value.