Skip to content

    Chapter 58, Backend

    Professional configuration

    Configuration is everything that changes between your computer, CI, and production, without changing the code. Mistakes here are the most common cause of leaked secrets and "it works on my machine".

    Code for this chapter: backend/cap58_configuracao.py

    The rule: configuration comes from the environment

    The principle (of twelve-factor apps) is simple: the same code runs anywhere, and what changes is read from environment variables. No if ambiente == "prod" scattered throughout the code, and no passwords in versioned files.

    EnvironmentWhere the values come fromNote
    DevelopmentA file .env localOutside of Git (.gitignore)
    Tests and CIVariables defined in the jobDisposable values
    ProductionSecret manager or orchestrator variablesNever in image or repository

    Typed configuration with pydantic-settings

    Read os.environ["PORTA"] returns text, and forgetting to convert is a bug waiting to happen. The pydantic-settings reads the variables, converts to the declared type and validates, all at startup. If something is wrong, the program doesn't even start, which is exactly what you want:

    Terminal
    uv add pydantic-settings
    
    backend/cap58_configuracao.pylinhas 10 a 23
    import os
    from pathlib import Path
    from typing import Literal
    
    from pydantic import SecretStr, ValidationError, model_validator
    from pydantic_settings import BaseSettings, SettingsConfigDict
    
    
    class Configuracao(BaseSettings):
        model_config = SettingsConfigDict(env_prefix="LOJA_", extra="ignore")
    
        ambiente: Literal["dev", "test", "prod"] = "dev"
        porta: int = 8000
        chave_api: SecretStr
    

    The field chave_api has no default value, so it is mandatory. Without the variable LOJA_CHAVE_API, the configuration refuses to start and says exactly what is missing:

    backend/cap58_configuracao.pylinhas 25 a 28
    try:
        Configuracao(_env_file=None)
    except ValidationError as erro:
        print([(e["loc"], e["type"]) for e in erro.errors()])
    
    Output
    [(('chave_api',), 'missing')]
    

    By defining the variables, the conversion happens automatically. The port came in as text and became int. And the SecretStr hides the value when printing, which protects the key from leaking in logs and error messages:

    backend/cap58_configuracao.pylinhas 30 a 35
    os.environ["LOJA_CHAVE_API"] = "segredo-123"
    os.environ["LOJA_PORTA"] = "9000"
    config = Configuracao(_env_file=None)
    print(config)
    print(config.porta + 1, type(config.porta).__name__)
    print(config.chave_api.get_secret_value()[:7])
    
    Output
    ambiente='dev' porta=9000 chave_api=SecretStr('**********')
    9001 int
    segredo
    

    An impossible value to convert is rejected with a clear error, instead of exploding later with a ValueError without context:

    backend/cap58_configuracao.pylinhas 37 a 42
    os.environ["LOJA_PORTA"] = "abc"
    try:
        Configuracao(_env_file=None)
    except ValidationError as erro:
        print(erro.errors()[0]["type"])
    os.environ["LOJA_PORTA"] = "9000"
    
    Output
    int_parsing
    

    Who wins: the order of precedence

    Values can come from various places, and the order decides. From highest to lowest priority: arguments passed to the constructor, then environment variables, then the file .env, then the code defaults. This allows production to overwrite the local file without editing anything:

    backend/cap58_configuracao.pylinhas 47 a 51
    Path(".env.demo").write_text("LOJA_PORTA=7000\nLOJA_AMBIENTE=test\n", encoding="utf-8")
    print(Configuracao(_env_file=".env.demo").porta)
    del os.environ["LOJA_PORTA"]
    print(Configuracao(_env_file=".env.demo").porta)
    print(Configuracao(_env_file=".env.demo", porta=1234).porta)
    
    Output
    9000
    7000
    1234
    

    In the first line, the environment variable (9000) won over the file (7000). After removing it, the file took precedence. And the explicit argument won over both.

    Business rules over configuration

    Validations that cross fields go into a model_validator. The classic example: production requires a strong key, and the program refuses to start with a weak one:

    backend/cap58_configuracao.pylinhas 56 a 69
    class ConfiguracaoSegura(Configuracao):
        @model_validator(mode="after")
        def producao_exige_chave_forte(self):
            if self.ambiente == "prod" and len(self.chave_api.get_secret_value()) < 16:
                raise ValueError("em produção a chave precisa ter pelo menos 16 caracteres")
            return self
    
    
    os.environ["LOJA_AMBIENTE"] = "prod"
    try:
        ConfiguracaoSegura(_env_file=None)
    except ValidationError as erro:
        print(erro.errors()[0]["msg"])
    del os.environ["LOJA_AMBIENTE"]
    
    Output
    Value error, em produção a chave precisa ter pelo menos 16 caracteres
    

    The final project (chapter 62) uses the same idea: API_AMBIENTE=prod with an SQLite database is rejected at startup.

    Secrets

    PracticeWhy
    .env in the .gitignore, and a .env.example versioned without real valuesDocuments which variables exist, without leaking
    SecretStr for passwords and keysDo not appear in print, in repr or in logs
    Secrets injected at runtime, never copied to the Docker imageAn image is distributed, and the layers store what was copied
    One file per secret (secrets_dir="/run/secrets"), in Docker and KubernetesDo not become visible in the process variable list
    Rotation possible without redeployA leaked key needs to be able to be changed quickly

    A secret that entered Git is leaked

    Deleting the file in the next commit does not solve: the history keeps everything. The correct response to a committed secret is to revoke it and generate another. Cleaning the history is secondary.

    Exercise 1

    A boolean debug field

    Add debug: bool = False to a subclass of Configuracao. Show that LOJA_DEBUG=1 becomes True and that LOJA_DEBUG=talvez is rejected.