17 KiB
title, last-updated, tags
| title | last-updated | tags | ||||
|---|---|---|---|---|---|---|
| Environment variables for the backend, frontend, and Docker | 2026-08-31 |
|
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.
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:
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:
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.
# Access token secret (1-hour tokens)
ACCESS_TOKEN_SECRET=<generated-secret> # Signs short-lived access tokens
# Refresh token secret (21-day tokens)
REFRESH_TOKEN_SECRET=<generated-secret> # Signs long-lived refresh tokens
# Confirmation token secret (30-minute tokens)
CONFIRMATION_TOKEN_SECRET=<generated-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)
openssl rand -base64 127
Windows - PowerShell 5.1 compatible (cryptographically secure, 127 raw bytes)
$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)
[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
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
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) doesn't need to set
it.
Frontend variables (web/frontend)
The website's core runtime variables are:
API_BASE_URL=http://api.core:8080 # Base URL for the .NET API (container DNS name in Docker)
SESSION_SECRET=<generated-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.
SMTP_HOST=mailpit # Required, no default
SMTP_PORT=1025 # Optional, defaults to 587
SMTP_USERNAME=<username> # Optional, no default
SMTP_PASSWORD=<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.csthrows on startup ifSMTP_HOSTorSMTP_FROM_EMAILis missing - Local dev: point at the
mailpitDocker 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.
SEAWEEDFS_SERVICE_URL=http://seaweedfs:8333 # Required, no default
SEAWEEDFS_PUBLIC_URL=http://localhost:8333 # Optional, defaults to SEAWEEDFS_SERVICE_URL
SEAWEEDFS_ACCESS_KEY_ID=admin # Required, no default
SEAWEEDFS_SECRET_ACCESS_KEY=secret # Required, no default
SEAWEEDFS_BUCKET=my-bucket # Required, no default
- Implementation:
S3FileStorageProviderthrows on construction if any of the four required values is missing - Local dev: point at the
seaweedfsDocker service (S3 API on port 8333) - Production: point at any S3-compatible endpoint (AWS S3, a hosted
SeaweedFS cluster, and so on);
ForcePathStyleis always enabled, which SeaweedFS requires and AWS S3 also accepts SEAWEEDFS_PUBLIC_URL: the endpoint download URLs are signed against, used byGetPresignedUrland reaching the API response asPublicUserProfileDto.AvatarUrl. A presigned URL is signed over its host, so this has to be the address the browser uses (http://localhost:8333with the dev compose file, which publishes the S3 API port), separate from the in-networkSEAWEEDFS_SERVICE_URLthe API uploads through. The two can be the same value wherever one address serves both
Docker-specific variables
SQL Server container
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_PASSWORDfor 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:
cd web
./generate-env.sh .env.dev
See 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.devweb/docker-compose.test.yaml→web/.env.testweb/docker-compose.prod.yaml→web/.env.prodweb/docker-compose.prod.seed.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_PUBLIC_URL |
✓ | ✓ | No | Endpoint download URLs are signed against; defaults to SEAWEEDFS_SERVICE_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 anInvalidOperationException - 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_URLdefaults tohttp://localhost:8080SESSION_SECRETfalls back to a development-only local secretNODE_ENVcontrols secure cookie behavior
Example configuration files
.env.dev (backend/Docker)
# Database
DB_SERVER=sqlserver,1433
DB_NAME=Biergarten
DB_USER=sa
DB_PASSWORD=Dev_Password_123!
# JWT Authentication Secrets
ACCESS_TOKEN_SECRET=<generated-with-openssl>
REFRESH_TOKEN_SECRET=<generated-with-openssl>
CONFIRMATION_TOKEN_SECRET=<generated-with-openssl>
WEBSITE_BASE_URL=http://localhost:3000
# Frontend session signing
SESSION_SECRET=<generated-with-openssl>
# 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_PUBLIC_URL=http://localhost:8333
SEAWEEDFS_ACCESS_KEY_ID=<generated-with-openssl>
SEAWEEDFS_SECRET_ACCESS_KEY=<generated-with-openssl>
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
API_BASE_URL=http://localhost:8080
SESSION_SECRET=<generated-with-openssl>
NODE_ENV=development