Environment files
Environment files (.env) define how a product connects to its infrastructure (database hostnames, ports, credentials, feature-specific service URLs, and runtime flags). They are the source of truth for runtime configuration.
Table of contents
- Why they matter
- File hierarchy
- Dev vs prod separation
- Merge flow
- Key variables
- Who reads them
- See also
Why they matter
The other sources of truth define what a product is made of (pyproject.toml), which combinations are valid (UVL), and what state features are in (manifest). Environment files define how it runs. That covers which hostname the database listens on, which port Redis exposes, what credentials to use, and whether to seed the database on startup.
Without environment files, Docker containers cannot connect to each other, the framework cannot build a database URI, and features that depend on external services (mail, Redis, storage) cannot function.
File hierarchy
Each product and feature can declare environment files. During deployment, these are merged into a single configuration.
Product level
| File | Purpose | Git-tracked |
|---|---|---|
docker/.env.dev.example |
Template for development | Yes |
docker/.env.prod.example |
Template for production (credentials as <SET>) |
Yes |
docker/.env |
Active dev config (generated from example) | No |
docker/.env.deploy |
Active prod config (generated by product:deploy) |
No |
docker/.env.deploy.example |
Merged template (generated by product:build) |
No |
Feature level
| File | Purpose | Git-tracked |
|---|---|---|
docker/.env.example |
Default feature env template | Yes |
docker/.env.dev.example |
Dev-specific overrides | Yes |
docker/.env.prod.example |
Prod-specific overrides | Yes |
docker/.env |
Active config (generated by product:env) |
No |
Dev vs prod separation
Development and production use separate files to prevent configuration collisions.
- Dev reads from
docker/.env, which contains dev hostnames (e.g.,sample_splent_app_db), default passwords, and seeding enabled. - Prod reads from
docker/.env.deploy, which contains deploy hostnames (e.g.,sample_splent_app_db_deploy), real credentials, and seeding disabled.
The production entrypoint syncs .env.deploy to .env on startup so the framework’s load_dotenv() picks up the correct values.
Never commit
.envor.env.deployfiles with real credentials. Only commit.exampletemplates.
Merge flow
Development (product:env --merge --dev)
product/.env.dev.example + feature1/.env.example + feature2/.env.example
↓
product/docker/.env
Product values take priority (via setdefault). Features only add keys that the product doesn’t already define.
Production (product:build)
product/.env.prod.example + feature1/.env.example + feature2/.env.example
↓
product/docker/.env.deploy.example
Feature values override product values (via update). This allows features to inject their required variables (e.g., REDIS_URL, MAIL_SERVER).
Key variables
| Variable | Set by | Used by |
|---|---|---|
SPLENT_APP |
Product | CLI, framework, startup scripts |
SPLENT_ENV |
Developer/CI | CLI (feature resolution), startup scripts |
WORKING_DIR |
Docker | CLI, framework (path resolution) |
MARIADB_HOSTNAME |
Product env | Framework (database URI), startup scripts |
MARIADB_DATABASE |
Product env | Framework, migration scripts |
MARIADB_USER / MARIADB_PASSWORD |
Product env | Framework, health checks, startup scripts |
REDIS_URL |
Redis feature | Framework (session store), app code |
REDIS_HOST_PORT |
Redis feature | Docker Compose (port mapping) |
MAIL_SERVER / MAIL_PORT |
Mail feature | Framework (Flask-Mail), app code |
RUN_DB_SEED |
Product env | Startup script 04_handle_migrations.sh |
GUNICORN_WORKERS |
Operator | Production entrypoint (default 2*nproc+1) |
GUNICORN_TIMEOUT |
Operator | Production entrypoint (default 120) |
SELENIUM_GRID_URL |
Developer/CI | Framework (E2E driver; attach to a Selenium Grid) |
SELENIUM_BROWSER |
Developer/CI | Framework (E2E driver; chrome or firefox, default chrome) |
SELENIUM_TARGET_URL |
Developer/CI | Framework (base URL for E2E tests; required with a grid) |
SPLENT_LOCUSTFILES |
Developer/CI | Framework (Locust bootstrap; override locustfile discovery) |
Who reads them
| Consumer | What it reads | Why |
|---|---|---|
product:env |
.env.example templates |
Generates and merges .env files |
product:build |
.env.prod.example from product + features |
Creates .env.deploy.example |
product:deploy |
.env.deploy.example |
Creates .env.deploy, prompts for <SET> values |
| Docker Compose | env_file: directive |
Injects vars into containers |
Framework load_dotenv |
<product>/docker/.env |
Overrides process env vars |
| Startup scripts | $MARIADB_*, $RUN_DB_SEED |
Database connection, seeding |
| Health checks | $$MARIADB_USER, $$MARIADB_PASSWORD |
Docker Compose CMD-SHELL |
Precedence in the CLI
Since CLI 1.13.0, an explicitly exported variable always wins. When SPLENT_APP is set, importing the CLI loads the product’s .env over the workspace one, but never over the process environment.
shell environment > <product>/docker/.env > workspace .env
Earlier versions let the product .env override the shell, which made the CLI unusable from the host. A product .env carries container paths, so whatever WORKING_DIR the operator exported was clobbered on import.
See also
- pyproject.toml. Feature declarations and versions
- product:env. Generate and merge env files
- product:build. Merge env + compose for production
- product:deploy. Deploy with
.env.deploy