--- title: Environment variables for the backend, frontend, and Docker last-updated: 2026-08-31 tags: - environment-variables - configuration - secrets - docker --- This document covers the environment variables used by the Biergarten stack. ## Overview The application uses environment variables for: - **.NET API backend** - database connections, token secrets, runtime settings - **React Router website** - API base URL and session signing - **Docker containers** - environment-specific orchestration ## Configuration patterns ### Backend (.NET API) Direct environment variable access via `Environment.GetEnvironmentVariable()`. ### Frontend (`web/frontend`) The website reads runtime values from the server environment for its auth and API integration. In Docker, the `frontend` service is wired into the same `web/.env.dev` / `.env.test` / `.env.prod` files as the backend; running it locally with `npm run dev` reads from the shell environment. ### Docker Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml: - `.env.dev` - Development - `.env.test` - Testing - `.env.prod` - Production ### Generating an env file `web/generate-env.sh` copies `.env.example` to a target file and replaces the secret-bearing values (`DB_PASSWORD`, `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SESSION_SECRET`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SEAWEEDFS_ACCESS_KEY_ID`, `SEAWEEDFS_SECRET_ACCESS_KEY`) with freshly randomized ones via `openssl`. Non-secret values are left untouched. The output file is written with `600` permissions. ```bash cd web ./generate-env.sh .env.dev ``` Pass the output path, and optionally a template path, as arguments (default output: `.env`, default template: `.env.example`). The script prompts before overwriting an existing file. Randomized `SMTP_USERNAME`/`SMTP_PASSWORD` are dev placeholders; replace them with real provider credentials for staging/production. ## Backend variables (.NET API) ### Database connection **Option 1: Component-Based (Recommended for Docker)** Build connection string from individual components: ```bash DB_SERVER=sqlserver,1433 # SQL Server host and port DB_NAME=Biergarten # Database name DB_USER=sa # SQL Server username DB_PASSWORD=YourStrong!Passw0rd # SQL Server password DB_TRUST_SERVER_CERTIFICATE=True # Optional, defaults to True ``` **Option 2: Full Connection String (Local Development)** Provide complete connection string: ```bash DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" ``` **Priority**: `DB_CONNECTION_STRING` is checked first. If not found, connection string is built from components. **Implementation**: See `DefaultSqlConnectionFactory.cs` ### JSON Web Token (JWT) authentication secrets (backend) The backend uses separate secrets for different token types to enable independent key rotation and validation isolation. ```bash # Access token secret (1-hour tokens) ACCESS_TOKEN_SECRET= # Signs short-lived access tokens # Refresh token secret (21-day tokens) REFRESH_TOKEN_SECRET= # Signs long-lived refresh tokens # Confirmation token secret (30-minute tokens) CONFIRMATION_TOKEN_SECRET= # Signs email confirmation tokens # Website base URL (used in confirmation emails) WEBSITE_BASE_URL=https://thebiergarten.app # Base URL for the website ``` **Security Requirements**: - Use at least 32 characters per secret - Use secrets generated from 127 or more random bytes in production (base64 encoding turns 127 bytes into a longer string, well over 127 characters) - Generate secrets with a cryptographically secure random function - Never reuse secrets across token types or environments - Rotate secrets periodically in production **Generate Secrets**: **macOS/Linux - Generate a cryptographically secure base64 secret (127 raw bytes)** ```bash openssl rand -base64 127 ``` **Windows - PowerShell 5.1 compatible (cryptographically secure, 127 raw bytes)** ```powershell $bytes = New-Object byte[] 127 $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() $rng.GetBytes($bytes) [Convert]::ToBase64String($bytes) $rng.Dispose() ``` **Windows - PowerShell 7+ compatible (cryptographically secure, 127 raw bytes)** ```powershell [Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(127)) ``` **Token Expiration**: - **Access tokens**: 1 hour - **Refresh tokens**: 21 days - **Confirmation tokens**: 30 minutes (Defined in `TokenServiceExpirationHours` class) **JWT Implementation**: - **Algorithm**: HS256 (HMAC-SHA256) - **Handler**: Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler - **Validation**: Token signature, expiration, and malformed token checks ### Migration control ```bash CLEAR_DATABASE=true ``` - **Required**: No - **Default**: `false` - **Effect**: If set to `true`, drops and recreates the database during migrations - **Usage**: Development and testing environments only - **Warning**: Never use in production ### ASP.NET Core configuration ```bash ASPNETCORE_ENVIRONMENT=Development # Development, Production, Staging ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address and port DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution ``` `ASPNETCORE_URLS` is hardcoded directly in `docker-compose.dev.yaml` and `docker-compose.prod.yaml` (not sourced from the `.env` file), so it's required only for Docker. Outside Docker, `dotnet run` falls back to the `http` profile in `API.Core/Properties/launchSettings.json` (`http://localhost:5069`), so manual backend setup (see [Getting Started](getting-started.md#manual-backend-setup)) doesn't need to set it. ## Frontend variables (`web/frontend`) The website's core runtime variables are: ```bash API_BASE_URL=http://api.core:8080 # Base URL for the .NET API (container DNS name in Docker) SESSION_SECRET= # Cookie session signing secret NODE_ENV=development # Standard Node runtime mode PORT=3000 # Port the built server listens on ``` The `frontend` Docker service hardcodes `API_BASE_URL` to `http://api.core:8080` (the backend's in-network service name) and `PORT` to `3000`; only `SESSION_SECRET` is sourced from the env file, and `NODE_ENV` is set per compose file (`development` in dev, `production` in prod). ### Frontend variable details #### `API_BASE_URL` - **Required**: Yes for local development - **Default in code**: `http://localhost:8080` - **Used by**: `web/frontend/app/features/auth/auth.server.ts`, `web/frontend/app/features/breweries/breweries.server.ts` - **Purpose**: Routes website auth and API calls to the .NET API #### `SESSION_SECRET` - **Required**: Strongly recommended in all environments - **Default in local code path**: `dev-secret-change-me` - **Used by**: React Router cookie session storage in `auth.server.ts` - **Purpose**: Signs and validates the website session cookie #### `NODE_ENV` - **Required**: No - **Typical values**: `development`, `production`, `test` - **Purpose**: Controls secure cookie behavior and runtime mode #### `PORT` - **Required**: No - **Default**: `3000` - **Purpose**: Port the built server (`build/server/index.js`) binds to ### SMTP configuration (backend) Read by `Features.Emails/Services/SmtpEmailProvider.cs` for sending confirmation and account emails. ```bash SMTP_HOST=mailpit # Required, no default SMTP_PORT=1025 # Optional, defaults to 587 SMTP_USERNAME= # Optional, no default SMTP_PASSWORD= # Optional, no default SMTP_USE_SSL=false # Optional, defaults to true SMTP_FROM_EMAIL=noreply@thebiergarten.app # Required, no default SMTP_FROM_NAME=The Biergarten App # Optional, defaults to "The Biergarten" ``` - **Implementation**: `Features.Emails/Services/SmtpEmailProvider.cs` throws on startup if `SMTP_HOST` or `SMTP_FROM_EMAIL` is missing - **Local dev**: point at the `mailpit` Docker service (SMTP on port 1025, web UI on http://localhost:8025) - **Production**: point at a real provider (SendGrid, Mailgun, Amazon SES, and so on) ### File storage configuration (backend) Read by `Features.PhotoUpload/Services/S3FileStorageProvider.cs` for uploading and retrieving photos via an S3-compatible object store. ```bash SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333 # Required, no default SEAWEEDFS_ACCESS_KEY_ID=admin # Required, no default SEAWEEDFS_SECRET_ACCESS_KEY=secret # Required, no default SEAWEEDFS_BUCKET=my-bucket # Required, no default ``` - **Implementation**: `S3FileStorageProvider` throws on construction if any of these four values is missing - **Local dev**: point at the `seaweedfs` Docker service (S3 API on port 8333) - **Production**: point at any S3-compatible endpoint (AWS S3, a hosted SeaweedFS cluster, and so on); `ForcePathStyle` is always enabled, which SeaweedFS requires and AWS S3 also accepts ## Docker-specific variables ### SQL Server container ```bash SA_PASSWORD=YourStrong!Passw0rd # SQL Server SA password ACCEPT_EULA=Y # Accept SQL Server EULA (required) MSSQL_PID=Express # SQL Server edition (Express, Developer, Enterprise) ``` **Password Requirements**: - Minimum 8 characters - Uppercase, lowercase, digits, and special characters - Maps to `DB_PASSWORD` for application containers ## Environment file structure ### Backend/Docker (`web/` directory) ``` web/.env.example # Template (tracked in Git) web/.env.dev # Development config (gitignored) web/.env.test # Testing config (gitignored) web/.env.prod # Production config (gitignored) ``` **Setup**: ```bash cd web ./generate-env.sh .env.dev ``` See [Generating an env file](#generating-an-env-file) above — `generate-env.sh` randomizes the secret-bearing values, so it's the preferred way to create any `.env.dev`/`.env.test`/`.env.prod` file. A plain `cp web/.env.example web/.env.dev` works too, but leaves the placeholder secrets from the template in place; edit them manually if you go that route. **Docker Compose Mapping**: - `web/docker-compose.dev.yaml` → `web/.env.dev` - `web/docker-compose.test.yaml` → `web/.env.test` - `web/docker-compose.prod.yaml` → `web/.env.prod` ## Variable reference table | Variable | Backend | Frontend | Docker | Required | Notes | | ----------------------------- | :-----: | :------: | :----: | :------: | ---------------------------- | | `DB_SERVER` | ✓ | | ✓ | Yes\* | SQL Server address | | `DB_NAME` | ✓ | | ✓ | Yes\* | Database name | | `DB_USER` | ✓ | | ✓ | Yes\* | SQL username | | `DB_PASSWORD` | ✓ | | ✓ | Yes\* | SQL password | | `DB_CONNECTION_STRING` | ✓ | | | Yes\* | Alternative to components | | `DB_TRUST_SERVER_CERTIFICATE` | ✓ | | ✓ | No | Defaults to `True` | | `ACCESS_TOKEN_SECRET` | ✓ | | ✓ | Yes | Access token signing | | `REFRESH_TOKEN_SECRET` | ✓ | | ✓ | Yes | Refresh token signing | | `CONFIRMATION_TOKEN_SECRET` | ✓ | | ✓ | Yes | Confirmation token signing | | `WEBSITE_BASE_URL` | ✓ | | | Yes | Website URL for emails | | `SMTP_HOST` | ✓ | | ✓ | Yes | SMTP server host | | `SMTP_PORT` | ✓ | | ✓ | No | Defaults to `587` | | `SMTP_USERNAME` | ✓ | | ✓ | No | SMTP auth username | | `SMTP_PASSWORD` | ✓ | | ✓ | No | SMTP auth password | | `SMTP_USE_SSL` | ✓ | | ✓ | No | Defaults to `true` | | `SMTP_FROM_EMAIL` | ✓ | | ✓ | Yes | Email sender address | | `SMTP_FROM_NAME` | ✓ | | ✓ | No | Defaults to `The Biergarten` | | `SEAWEEDFS_SERVICE_URL` | ✓ | | ✓ | Yes | S3-compatible endpoint URL | | `SEAWEEDFS_ACCESS_KEY_ID` | ✓ | | ✓ | Yes | S3 access key | | `SEAWEEDFS_SECRET_ACCESS_KEY` | ✓ | | ✓ | Yes | S3 secret key | | `SEAWEEDFS_BUCKET` | ✓ | | ✓ | Yes | S3 bucket name | | `API_BASE_URL` | | ✓ | ✓ | Yes | Website-to-API base URL | | `SESSION_SECRET` | | ✓ | ✓ | Yes | Website session signing | | `NODE_ENV` | | ✓ | ✓ | No | Runtime mode | | `PORT` | | ✓ | ✓ | No | Frontend server port | | `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag | | `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment | | `ASPNETCORE_URLS` | ✓ | | ✓ | Yes\*\* | API binding address | | `SA_PASSWORD` | | | ✓ | Yes | SQL Server container | | `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA | | `MSSQL_PID` | | | ✓ | No | SQL Server edition | | `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag | \* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided. \*\* Required for Docker, where it's hardcoded in the compose files. Outside Docker, `dotnet run` falls back to the `applicationUrl` in `launchSettings.json`. ## Validation ### Backend validation Variables are validated at startup: - Missing required variables (`ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`, `SMTP_HOST`, `SMTP_FROM_EMAIL`, `SEAWEEDFS_SERVICE_URL`, `SEAWEEDFS_ACCESS_KEY_ID`, `SEAWEEDFS_SECRET_ACCESS_KEY`, `SEAWEEDFS_BUCKET`, DB connection values) cause the application to fail with an `InvalidOperationException` - No minimum length is enforced on the token secrets in code; the "minimum 32 characters" guidance above is a recommendation, not an enforced check ### Frontend validation The website relies on runtime defaults for local development and on the server environment when deployed. - `API_BASE_URL` defaults to `http://localhost:8080` - `SESSION_SECRET` falls back to a development-only local secret - `NODE_ENV` controls secure cookie behavior ## Example configuration files ### `.env.dev` (backend/Docker) ```bash # Database DB_SERVER=sqlserver,1433 DB_NAME=Biergarten DB_USER=sa DB_PASSWORD=Dev_Password_123! # JWT Authentication Secrets ACCESS_TOKEN_SECRET= REFRESH_TOKEN_SECRET= CONFIRMATION_TOKEN_SECRET= WEBSITE_BASE_URL=http://localhost:3000 # Frontend session signing SESSION_SECRET= # SMTP (Mailpit in dev) SMTP_HOST=mailpit SMTP_PORT=1025 SMTP_USE_SSL=false SMTP_FROM_EMAIL=noreply@thebiergarten.app SMTP_FROM_NAME=The Biergarten App # SeaweedFS (S3-compatible storage) SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333 SEAWEEDFS_ACCESS_KEY_ID= SEAWEEDFS_SECRET_ACCESS_KEY= SEAWEEDFS_BUCKET=my-bucket # Migration CLEAR_DATABASE=true # ASP.NET Core ASPNETCORE_ENVIRONMENT=Development ASPNETCORE_URLS=http://0.0.0.0:8080 # SQL Server Container SA_PASSWORD=Dev_Password_123! ACCEPT_EULA=Y MSSQL_PID=Express ``` ### Frontend local runtime example ```bash API_BASE_URL=http://localhost:8080 SESSION_SECRET= NODE_ENV=development ```