App

Configuration And Secrets

Use environment variables and development secrets without coupling the app to a deployment platform.

By Guillermo Alvarez - Published - Updated

Configure the listen address

GoLazy reads ADDR, then PORT, then defaults to 127.0.0.1:3000:

ADDR=127.0.0.1:4000 lazy
PORT=4000 ./sample-app

Use ADDR when you want to control the host and port together. Use PORT when the deployment platform supplies only a port.

Set CONTROL_PLANE_ADDR when GoLazy should expose liveness and readiness probes on a separate address, or when the environment should activate the default control plane for ListenAndServe.

Read app-owned configuration

Use golazy.dev/lazyconfig when several environment variables belong together:

type AppConfig struct {
    MailFrom  string `var:"MAIL_FROM" require:"for sending mail"`
    Port      int    `default:"3000"`
    Listeners []Listener
}

type Listener struct {
    Name string
    Port int
}

config, err := lazyconfig.Getenv[AppConfig]()
if err != nil {
    return err
}

For package-level config that should fail during startup when the environment is invalid, use MustGetenv:

var Config = lazyconfig.MustGetenv[AppConfig]()

Field names map to uppercase snake-case environment variables, so Port reads PORT and ListenAddr reads LISTEN_ADDR. Use var to name a variable explicitly, default for fallback values, and required:"true" or require:"for reason" for required values. If the config type implements Validate() error, GoLazy calls it before returning.

Values are trimmed before parsing. Fields can be strings, numeric types, bools, numeric or bool pointers, or []string. Unset pointer fields stay nil. []string values split on commas or whitespace. Bool fields treat yes, true, and 1 as true; no, false, 0, and unknown values are false. Bool matching is case-insensitive.

Slices use a singular field prefix. This fills two Listeners entries and sorts them by number:

LISTENER_0_NAME=http
LISTENER_0_PORT=123
LISTENER_1_NAME=smtp
LISTENER_1_PORT=25

For a single item, omit the number:

LISTENER_NAME=http
LISTENER_PORT=123

The framework does not require a specific secret manager. Production deployments should provide real configuration values through the platform or secret store.

Environment variables

PRODUCTION ENVIRONMENT

  • ADDR: full listen address for lazy or the built app, such as 0.0.0.0:8080.
  • PORT: listen port used when the platform provides a port without a host. GoLazy reads ADDR first, then PORT, then defaults to 127.0.0.1:3000.
  • CONTROL_PLANE_ADDR: optional control-plane listen address. When it differs from the app address, ListenAndServe starts a second server for control endpoints. When it matches the app address, GoLazy mounts control endpoints into the app server.

Telemetry uses OpenTelemetry-compatible OTEL_* variables. See Telemetry for activation rules and the complete environment variable list.

DEVELOPMENT ONLY

  • SAMPLE_APP_ENV: ordinary checked-in development value from mise.toml.
  • SAMPLE_APP_DEVELOPMENT_SECRET: secret-shaped checked-in example from .secrets/development.env.

Development-mode framework behavior comes from the lazydev build tag set by lazy, not from sample-app environment flags.

Keep development examples local

The sample app includes development tooling and a hidden directory for local secret examples:

mise.toml
.secrets/development.env

Use mise.toml for ordinary local values and .secrets/development.env for secret-shaped local examples. Do not depend on mise in production; the production process should receive environment variables from its runtime environment.

Pass configuration into services

Prefer explicit service constructors when configuration affects behavior:

type MailerConfig struct {
    From string
}

func NewMailerFromEnv() (*Mailer, error) {
    return NewMailer(MailerConfig{
        From: os.Getenv("MAIL_FROM"),
    })
}

Initialize the service once in Context, then expose it through typed context helpers.