Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.

Database credentials, API keys, feature flags: every app has settings that change between your laptop, staging, and production, and that should never be hardcoded. Symfony handles this with three layers, environments, .env files, and parameters, and knowing which to use for what keeps your secrets out of the codebase and your config tidy.

Environments

Symfony runs in an environment, usually dev, prod, or test, set by the APP_ENV variable. Each environment can load different configuration: verbose errors and the profiler in dev, cached and locked down in prod. You mostly just set APP_ENV correctly per machine and let the framework apply the right config.

The .env file chain

Configuration values start life in .env files, and Symfony loads several in a defined order so you can layer them:

# .env  (committed, holds sensible defaults)
APP_ENV=dev
DATABASE_URL="mysql://app:pass@127.0.0.1:3306/app"
STRIPE_SECRET=

The key files are .env, which you commit with defaults, and .env.local, which you do not commit and which holds the real values for that one machine. There are also .env.dev and .env.prod for environment-specific values. The rule of thumb: commit .env with placeholders, put real secrets in .env.local (which is gitignored), and never commit a real key.

Using env vars in configuration

Inside Symfony's YAML config you read an environment variable with the env() syntax, and you can coerce its type with a processor:

# config/packages/some_bundle.yaml
some_bundle:
    api_key: '%env(STRIPE_SECRET)%'
    timeout: '%env(int:API_TIMEOUT)%'   # cast to int

This is how bundles get their values from the environment without you wiring anything by hand.

Parameters for fixed values

Environment variables are for things that change per environment. For values that are fixed parts of the app, a page size, a from-address, a label, use a parameter instead:

# config/services.yaml
parameters:
    app.items_per_page: 20

services:
    _defaults:
        bind:
            int $itemsPerPage: '%app.items_per_page%'

The bind makes that parameter available to any service that type-hints int $itemsPerPage. You can also inject either a parameter or an env var straight into a service with the autowire attribute:

public function __construct(
    #[Autowire('%env(STRIPE_SECRET)%')] private string $stripeSecret,
) {}

Real secrets in production

For production secrets, .env.local on the server works, but Symfony also ships a secrets vault, an encrypted store you manage with php bin/console secrets:set STRIPE_SECRET. It keeps secrets encrypted in the repo and decrypted at runtime with a key that lives only on the server. For anything sensitive in production, it is worth using over plain env files.

Wrapping up

Symfony config is layered: the APP_ENV environment decides which configuration loads, the .env chain holds values with .env.local for your real, uncommitted secrets, and you read those in config with %env(VAR)%. Use parameters for fixed app values and bind or the autowire attribute to inject either into services. For production secrets, reach for the encrypted secrets vault. Keep real keys out of .env and in .env.local or the vault, and your configuration stays both flexible and safe.