Merge pull request #134 from aaronpo97/133-update-env-handling

refactor/update docker compose env configuration
This commit is contained in:
Aaron Po
2026-02-08 21:03:13 -05:00
committed by GitHub
14 changed files with 611 additions and 55 deletions

38
.env.example Normal file
View File

@@ -0,0 +1,38 @@
# ==============================================
# Biergarten App - Environment Variables Template
# ==============================================
#
# This file contains backend/Docker environment variables.
# Copy this to create environment-specific files:
# - .env.dev (development)
# - .env.test (testing)
# - .env.prod (production)
#
# For frontend variables, create a separate .env.local file
# in the Website/ directory. See README.md for complete docs.
#
# ==============================================
# ======================
# Database Configuration
# ======================
# SQL Server Connection Components (Recommended for Docker)
# These are used to build connection strings dynamically
DB_SERVER=sqlserver,1433
DB_NAME=Biergarten
DB_USER=sa
DB_PASSWORD=YourStrong!Passw0rd
# Alternative: Full Connection String (Local Development)
# If set, this overrides the component-based configuration above
# DB_CONNECTION_STRING=Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;
# ======================
# JWT Configuration
# ======================
# JWT Secret for signing tokens
# IMPORTANT: Generate a secure secret (minimum 32 characters)
# Command: openssl rand -base64 32
JWT_SECRET=128490218jfklsdajfdsa90f8sd0fid0safasr31jl2k1j4AFSDR

5
.gitignore vendored
View File

@@ -486,4 +486,7 @@ FodyWeavers.xsd
database database
.env.* .env
.env.dev
.env.test
.env.prod

367
README.md
View File

@@ -12,6 +12,15 @@ A social platform for craft beer enthusiasts to discover breweries, share review
- [Prerequisites](#prerequisites) - [Prerequisites](#prerequisites)
- [Quick Start (Development Environment)](#quick-start-development-environment) - [Quick Start (Development Environment)](#quick-start-development-environment)
- [Manual Setup (Without Docker)](#manual-setup-without-docker) - [Manual Setup (Without Docker)](#manual-setup-without-docker)
- [Environment Variables](#environment-variables)
- [Overview](#overview)
- [Backend Variables (.NET API)](#backend-variables-net-api)
- [Frontend Variables (Next.js)](#frontend-variables-nextjs)
- [Docker Variables](#docker-variables)
- [External Services](#external-services)
- [Generating Secrets](#generating-secrets)
- [Environment File Structure](#environment-file-structure)
- [Variable Reference Table](#variable-reference-table)
- [Testing](#testing) - [Testing](#testing)
- [Database Schema](#database-schema) - [Database Schema](#database-schema)
- [Authentication & Security](#authentication--security) - [Authentication & Security](#authentication--security)
@@ -154,17 +163,25 @@ Website/ # Next.js frontend application
2. **Configure environment variables** 2. **Configure environment variables**
Create a `.env` file in the project root: Copy the example file and customize:
```bash ```bash
# Database cp .env.example .env.dev
SA_PASSWORD=YourStrong!Passw0rd ```
DB_CONNECTION_STRING=Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;
MASTER_DB_CONNECTION_STRING=Server=localhost,1433;Database=master;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True; Required variables in `.env.dev`:
```bash
# Database (component-based for Docker)
DB_SERVER=sqlserver,1433
DB_NAME=Biergarten
DB_USER=sa
DB_PASSWORD=YourStrong!Passw0rd
# JWT Authentication # JWT Authentication
JWT_SECRET=your-secret-key-here-min-32-chars JWT_SECRET=your-secret-key-minimum-32-characters-required
``` ```
For a complete list of all backend and frontend environment variables, see the [Environment Variables](#environment-variables) section.
3. **Start the development environment** 3. **Start the development environment**
```bash ```bash
docker compose -f docker-compose.dev.yaml up -d docker compose -f docker-compose.dev.yaml up -d
@@ -181,27 +198,39 @@ Website/ # Next.js frontend application
Navigate to http://localhost:8080/swagger to explore and test API endpoints. Navigate to http://localhost:8080/swagger to explore and test API endpoints.
5. **Run the frontend** (optional) 5. **Run the frontend** (optional)
The frontend requires additional environment variables. See [Frontend Variables](#frontend-variables-nextjs) section.
```bash ```bash
cd Website cd Website
# Create .env.local with frontend variables
# (see Environment Variables section)
npm install npm install
npm run dev npm run dev
``` ```
For Website environment variables, see `Website/README.old.md`. For complete environment variable documentation, see the [Environment Variables](#environment-variables) section below.
### Manual Setup (Without Docker) ### Manual Setup (Without Docker)
#### Backend Setup
1. **Start SQL Server locally** or use a hosted instance 1. **Start SQL Server locally** or use a hosted instance
2. **Set environment variable** 2. **Set environment variables**
See [Backend Variables](#backend-variables-net-api) for details.
```bash ```bash
# macOS/Linux # macOS/Linux
export DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" export DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
export JWT_SECRET="your-secret-key-here-min-32-chars" export JWT_SECRET="your-secret-key-minimum-32-characters-required"
# Windows PowerShell # Windows PowerShell
$env:DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" $env:DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
$env:JWT_SECRET="your-secret-key-here-min-32-chars" $env:JWT_SECRET="your-secret-key-minimum-32-characters-required"
``` ```
3. **Run migrations** 3. **Run migrations**
@@ -220,6 +249,324 @@ Website/ # Next.js frontend application
dotnet run --project API/API.Core/API.Core.csproj dotnet run --project API/API.Core/API.Core.csproj
``` ```
#### Frontend Setup
1. **Navigate to Website directory**
```bash
cd Website
```
2. **Create environment file**
Create `.env.local` with required frontend variables. See [Frontend Variables](#frontend-variables-nextjs) for the complete list.
```bash
# Example minimal setup
BASE_URL=http://localhost:3000
NODE_ENV=development
# Generate secrets
CONFIRMATION_TOKEN_SECRET=$(openssl rand -base64 127)
RESET_PASSWORD_TOKEN_SECRET=$(openssl rand -base64 127)
SESSION_SECRET=$(openssl rand -base64 127)
# Add external service credentials
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_KEY=your-api-key
CLOUDINARY_SECRET=your-api-secret
# ... (see Environment Variables section for complete list)
```
3. **Install dependencies**
```bash
npm install
```
4. **Run Prisma migrations** (current frontend database)
```bash
npx prisma generate
npx prisma migrate dev
```
5. **Start the development server**
```bash
npm run dev
```
The frontend will be available at http://localhost:3000
---
## Environment Variables
### Overview
The Biergarten App uses environment variables for configuration across both backend (.NET API) and frontend (Next.js) services. This section provides complete documentation for all required and optional variables.
**Configuration Patterns:**
- **Backend**: Direct environment variable access via `Environment.GetEnvironmentVariable()`
- **Frontend**: Centralized configuration module at [src/Website/src/config/env/index.ts](src/Website/src/config/env/index.ts) with Zod validation
- **Docker**: Environment-specific `.env` files (`.env.dev`, `.env.test`, `.env.prod`)
### Backend Variables (.NET API)
The .NET API requires environment variables for database connectivity and JWT authentication. These can be set directly in your shell or via `.env` files when using Docker.
#### Database Connection
**Option 1: Component-Based (Recommended for Docker)**
Use individual components to build the connection string:
```bash
DB_SERVER=sqlserver,1433 # SQL Server address 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 a complete SQL Server connection string:
```bash
DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
```
The connection factory checks for `DB_CONNECTION_STRING` first, then falls back to building from components. See [DefaultSqlConnectionFactory.cs](src/Core/Repository/Repository.Core/Sql/DefaultSqlConnectionFactory.cs).
#### JWT Authentication
```bash
JWT_SECRET=your-secret-key-minimum-32-characters-required
```
- **Required**: Yes
- **Minimum Length**: 32 characters
- **Used For**: Signing JWT tokens for user authentication
- **Location**: [JwtService.cs](src/Core/Service/Service.Core/Services/JwtService.cs)
**Additional JWT Configuration** (in `appsettings.json`):
- `Jwt:ExpirationMinutes` - Token lifetime (default: 60)
- `Jwt:Issuer` - Token issuer (default: "biergarten-api")
- `Jwt:Audience` - Token audience (default: "biergarten-users")
#### Migration Control
```bash
CLEAR_DATABASE=true # Development/Testing only
```
- **Required**: No
- **Effect**: If set to "true", drops and recreates the database during migrations
- **Usage**: Development and testing environments only
- **Warning**: Never use in production
### Frontend Variables (Next.js)
The Next.js frontend requires environment variables for external services, authentication, and database connectivity. Create a `.env` or `.env.local` file in the `Website/` directory.
All variables are validated at runtime using Zod schemas. See [src/Website/src/config/env/index.ts](src/Website/src/config/env/index.ts).
#### Base Configuration
```bash
BASE_URL=http://localhost:3000 # Application base URL
NODE_ENV=development # Environment: development, production, test
```
#### Authentication & Sessions
```bash
# Token signing secrets (generate with: openssl rand -base64 127)
CONFIRMATION_TOKEN_SECRET=<generated-secret> # Email confirmation tokens
RESET_PASSWORD_TOKEN_SECRET=<generated-secret> # Password reset tokens
SESSION_SECRET=<generated-secret> # Session cookie signing
# Session configuration
SESSION_TOKEN_NAME=biergarten # Cookie name
SESSION_MAX_AGE=604800 # Cookie max age in seconds (604800 = 1 week)
```
#### Database (Prisma/Postgres)
**Current State**: The frontend currently uses Neon Postgres with Prisma. This will migrate to the SQL Server backend once feature parity is achieved.
```bash
POSTGRES_PRISMA_URL=postgresql://user:pass@host/db?pgbouncer=true # Pooled connection
POSTGRES_URL_NON_POOLING=postgresql://user:pass@host/db # Direct connection (migrations)
SHADOW_DATABASE_URL=postgresql://user:pass@host/shadow_db # Prisma shadow DB
```
#### Admin Account
```bash
ADMIN_PASSWORD=SecureAdminPassword123! # Initial admin account password for seeding
```
### Docker Variables
When running services in Docker, additional environment variables control container behavior:
#### ASP.NET Core
```bash
ASPNETCORE_ENVIRONMENT=Development # Development, Production
ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address
DOTNET_RUNNING_IN_CONTAINER=true # Container execution flag
```
#### SQL Server (Docker Container)
```bash
SA_PASSWORD=YourStrong!Passw0rd # SQL Server SA password (maps to DB_PASSWORD)
ACCEPT_EULA=Y # Accept SQL Server EULA
MSSQL_PID=Express # SQL Server edition (Express, Developer, etc.)
```
**Note**: `SA_PASSWORD` in the SQL Server container maps to `DB_PASSWORD` for the API application.
### External Services
The frontend integrates with several third-party services. Sign up for accounts and retrieve API credentials:
#### Cloudinary (Image Hosting)
```bash
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name # Public, client-accessible
CLOUDINARY_KEY=your-api-key # Server-side API key
CLOUDINARY_SECRET=your-api-secret # Server-side secret
```
**Setup**:
1. Sign up at [cloudinary.com](https://cloudinary.com)
2. Navigate to Dashboard
3. Copy Cloud Name, API Key, and API Secret
**Note**: The `NEXT_PUBLIC_` prefix makes the cloud name accessible in client-side code.
#### Mapbox (Maps & Geocoding)
```bash
MAPBOX_ACCESS_TOKEN=pk.your-public-token
```
**Setup**:
1. Create account at [mapbox.com](https://mapbox.com)
2. Navigate to Account → Tokens
3. Create a new token with public scopes
4. Copy the access token
#### SparkPost (Email Service)
```bash
SPARKPOST_API_KEY=your-api-key
SPARKPOST_SENDER_ADDRESS=noreply@yourdomain.com
```
**Setup**:
1. Sign up at [sparkpost.com](https://sparkpost.com)
2. Verify your sending domain or use sandbox
3. Create an API key with "Send via SMTP" permission
4. Configure sender address (must match verified domain)
### Generating Secrets
For authentication secrets (`JWT_SECRET`, `CONFIRMATION_TOKEN_SECRET`, etc.), generate cryptographically secure random values:
**macOS/Linux:**
```bash
openssl rand -base64 127
```
**Windows PowerShell:**
```powershell
[Convert]::ToBase64String((1..127 | ForEach-Object { Get-Random -Maximum 256 }))
```
**Requirements**:
- `JWT_SECRET`: Minimum 32 characters
- Session/token secrets: Recommend 127+ characters for maximum security
### Environment File Structure
The project uses multiple environment files depending on the context:
#### Backend/Docker (Root Directory)
- **`.env.example`** - Template file (tracked in Git)
- **`.env.dev`** - Development environment (gitignored)
- **`.env.test`** - Testing environment (gitignored)
- **`.env.prod`** - Production environment (gitignored)
**Setup**:
```bash
# Copy template and customize
cp .env.example .env.dev
# Edit .env.dev with your values
```
Docker Compose files reference these:
- `docker-compose.dev.yaml` → `.env.dev`
- `docker-compose.test.yaml` → `.env.test`
- `docker-compose.prod.yaml` → `.env.prod`
#### Frontend (Website Directory)
- **`.env`** or **`.env.local`** - Local development (gitignored)
**Setup**:
```bash
cd Website
# Create .env file with frontend variables
touch .env.local
```
### Variable Reference Table
| Variable | Backend | Frontend | Docker | Required | Notes |
|----------|---------|----------|--------|----------|-------|
| **Database** |
| `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 |
| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container only |
| **Authentication (Backend)** |
| `JWT_SECRET` | ✓ | | ✓ | Yes | Min 32 chars |
| **Authentication (Frontend)** |
| `CONFIRMATION_TOKEN_SECRET` | | ✓ | | Yes | Email confirmation |
| `RESET_PASSWORD_TOKEN_SECRET` | | ✓ | | Yes | Password reset |
| `SESSION_SECRET` | | ✓ | | Yes | Session signing |
| `SESSION_TOKEN_NAME` | | ✓ | | No | Default: "biergarten" |
| `SESSION_MAX_AGE` | | ✓ | | No | Default: 604800 |
| **Base Configuration** |
| `BASE_URL` | | ✓ | | Yes | App base URL |
| `NODE_ENV` | | ✓ | | Yes | development/production |
| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | Development/Production |
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | Binding address |
| **Database (Frontend - Current)** |
| `POSTGRES_PRISMA_URL` | | ✓ | | Yes | Pooled connection |
| `POSTGRES_URL_NON_POOLING` | | ✓ | | Yes | Direct connection |
| `SHADOW_DATABASE_URL` | | ✓ | | No | Prisma shadow DB |
| **External Services** |
| `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` | | ✓ | | Yes | Public, client-side |
| `CLOUDINARY_KEY` | | ✓ | | Yes | Server-side |
| `CLOUDINARY_SECRET` | | ✓ | | Yes | Server-side |
| `MAPBOX_ACCESS_TOKEN` | | ✓ | | Yes | Maps/geocoding |
| `SPARKPOST_API_KEY` | | ✓ | | Yes | Email service |
| `SPARKPOST_SENDER_ADDRESS` | | ✓ | | Yes | From address |
| **Other** |
| `ADMIN_PASSWORD` | | ✓ | | No | Seeding only |
| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test only |
| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA |
| `MSSQL_PID` | | | ✓ | No | SQL Server edition |
\* Either `DB_CONNECTION_STRING` OR the four component variables (`DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`) are required.
--- ---
## Testing ## Testing

View File

@@ -1,25 +1,27 @@
services: services:
sqlserver: sqlserver:
env_file: ".env.dev"
image: mcr.microsoft.com/mssql/server:2022-latest image: mcr.microsoft.com/mssql/server:2022-latest
platform: linux/amd64 platform: linux/amd64
container_name: dev-env-sqlserver container_name: dev-env-sqlserver
environment: environment:
ACCEPT_EULA: "Y" ACCEPT_EULA: "Y"
SA_PASSWORD: "${SA_PASSWORD}" SA_PASSWORD: "${DB_PASSWORD}"
MSSQL_PID: "Express" MSSQL_PID: "Express"
ports: ports:
- "1433:1433" - "1433:1433"
volumes: volumes:
- sqlserverdata-dev:/var/opt/mssql - sqlserverdata-dev:/var/opt/mssql
healthcheck: healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 12 retries: 12
start_period: 30s start_period: 30s
networks: networks:
- devnet - devnet
database.migrations: database.migrations:
env_file: ".env.dev"
image: database.migrations image: database.migrations
container_name: dev-env-database-migrations container_name: dev-env-database-migrations
depends_on: depends_on:
@@ -33,14 +35,17 @@ services:
APP_UID: 1000 APP_UID: 1000
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
CLEAR_DATABASE: "true" CLEAR_DATABASE: "true"
restart: "no" restart: "no"
networks: networks:
- devnet - devnet
database.seed: database.seed:
env_file: ".env.dev"
image: database.seed image: database.seed
container_name: dev-env-database-seed container_name: dev-env-database-seed
depends_on: depends_on:
@@ -54,11 +59,13 @@ services:
APP_UID: 1000 APP_UID: 1000
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
restart: "no" restart: "no"
networks: networks:
- devnet - devnet
volumes: volumes:
sqlserverdata-dev: sqlserverdata-dev:
driver: local driver: local

View File

@@ -1,18 +1,19 @@
services: services:
sqlserver: sqlserver:
env_file: ".env.dev"
image: mcr.microsoft.com/mssql/server:2022-latest image: mcr.microsoft.com/mssql/server:2022-latest
platform: linux/amd64 platform: linux/amd64
container_name: dev-env-sqlserver container_name: dev-env-sqlserver
environment: environment:
ACCEPT_EULA: "Y" ACCEPT_EULA: "Y"
SA_PASSWORD: "${SA_PASSWORD}" SA_PASSWORD: "${DB_PASSWORD}"
MSSQL_PID: "Express" MSSQL_PID: "Express"
ports: ports:
- "1433:1433" - "1433:1433"
volumes: volumes:
- sqlserverdata-dev:/var/opt/mssql - sqlserverdata-dev:/var/opt/mssql
healthcheck: healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 12 retries: 12
@@ -20,6 +21,7 @@ services:
networks: networks:
- devnet - devnet
database.migrations: database.migrations:
env_file: ".env.dev"
image: database.migrations image: database.migrations
container_name: dev-env-database-migrations container_name: dev-env-database-migrations
depends_on: depends_on:
@@ -33,14 +35,17 @@ services:
APP_UID: 1000 APP_UID: 1000
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
CLEAR_DATABASE: "true" CLEAR_DATABASE: "true"
restart: "no" restart: "no"
networks: networks:
- devnet - devnet
database.seed: database.seed:
env_file: ".env.dev"
image: database.seed image: database.seed
container_name: dev-env-database-seed container_name: dev-env-database-seed
depends_on: depends_on:
@@ -54,12 +59,16 @@ services:
APP_UID: 1000 APP_UID: 1000
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
restart: "no" restart: "no"
networks: networks:
- devnet - devnet
api.core: api.core:
env_file: ".env.dev"
image: api.core image: api.core
container_name: dev-env-api-core container_name: dev-env-api-core
depends_on: depends_on:
@@ -78,7 +87,10 @@ services:
ASPNETCORE_ENVIRONMENT: "Development" ASPNETCORE_ENVIRONMENT: "Development"
ASPNETCORE_URLS: "http://0.0.0.0:8080" ASPNETCORE_URLS: "http://0.0.0.0:8080"
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
JWT_SECRET: "${JWT_SECRET}" JWT_SECRET: "${JWT_SECRET}"
restart: unless-stopped restart: unless-stopped
networks: networks:

View File

@@ -1,16 +1,17 @@
services: services:
sqlserver: sqlserver:
env_file: ".env.prod"
image: mcr.microsoft.com/mssql/server:2022-latest image: mcr.microsoft.com/mssql/server:2022-latest
platform: linux/amd64 platform: linux/amd64
container_name: prod-env-sqlserver container_name: prod-env-sqlserver
environment: environment:
ACCEPT_EULA: "Y" ACCEPT_EULA: "Y"
SA_PASSWORD: "${SA_PASSWORD}" SA_PASSWORD: "${DB_PASSWORD}"
MSSQL_PID: "Express" MSSQL_PID: "Express"
volumes: volumes:
- sqlserverdata-prod:/var/opt/mssql - sqlserverdata-prod:/var/opt/mssql
healthcheck: healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 12 retries: 12
@@ -19,6 +20,7 @@ services:
- prodnet - prodnet
database.migrations: database.migrations:
env_file: ".env.prod"
image: database.migrations image: database.migrations
container_name: prod-env-database-migrations container_name: prod-env-database-migrations
depends_on: depends_on:
@@ -32,13 +34,16 @@ services:
APP_UID: 1000 APP_UID: 1000
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
restart: "no" restart: "no"
networks: networks:
- prodnet - prodnet
api.core: api.core:
env_file: ".env.prod"
image: api.core image: api.core
container_name: prod-env-api-core container_name: prod-env-api-core
depends_on: depends_on:
@@ -57,8 +62,10 @@ services:
ASPNETCORE_ENVIRONMENT: "Production" ASPNETCORE_ENVIRONMENT: "Production"
ASPNETCORE_URLS: "http://0.0.0.0:8080" ASPNETCORE_URLS: "http://0.0.0.0:8080"
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
JWT_SECRET: "${JWT_SECRET}" JWT_SECRET: "${JWT_SECRET}"
restart: unless-stopped restart: unless-stopped
networks: networks:

View File

@@ -1,17 +1,18 @@
services: services:
sqlserver: sqlserver:
env_file: ".env.test"
image: mcr.microsoft.com/mssql/server:2022-latest image: mcr.microsoft.com/mssql/server:2022-latest
platform: linux/amd64 platform: linux/amd64
container_name: test-env-sqlserver container_name: test-env-sqlserver
environment: environment:
ACCEPT_EULA: "Y" ACCEPT_EULA: "Y"
SA_PASSWORD: "${SA_PASSWORD}" SA_PASSWORD: "${DB_PASSWORD}"
MSSQL_PID: "Express" MSSQL_PID: "Express"
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
volumes: volumes:
- sqlserverdata-test:/var/opt/mssql - sqlserverdata-test:/var/opt/mssql
healthcheck: healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1' || exit 1"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 12 retries: 12
@@ -20,6 +21,7 @@ services:
- testnet - testnet
database.migrations: database.migrations:
env_file: ".env.test"
image: database.migrations image: database.migrations
container_name: test-env-database-migrations container_name: test-env-database-migrations
depends_on: depends_on:
@@ -33,14 +35,17 @@ services:
APP_UID: 1000 APP_UID: 1000
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
MASTER_DB_CONNECTION_STRING: "${TEST_MASTER_DB_CONNECTION_STRING}" DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
CLEAR_DATABASE: "true" CLEAR_DATABASE: "true"
restart: "no" restart: "no"
networks: networks:
- testnet - testnet
database.seed: database.seed:
env_file: ".env.test"
image: database.seed image: database.seed
container_name: test-env-database-seed container_name: test-env-database-seed
depends_on: depends_on:
@@ -54,12 +59,16 @@ services:
APP_UID: 1000 APP_UID: 1000
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
restart: "no" restart: "no"
networks: networks:
- testnet - testnet
api.specs: api.specs:
env_file: ".env.test"
image: api.specs image: api.specs
container_name: test-env-api-specs container_name: test-env-api-specs
depends_on: depends_on:
@@ -72,7 +81,10 @@ services:
BUILD_CONFIGURATION: Release BUILD_CONFIGURATION: Release
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" DB_SERVER: "${DB_SERVER}"
DB_NAME: "${DB_NAME}"
DB_USER: "${DB_USER}"
DB_PASSWORD: "${DB_PASSWORD}"
JWT_SECRET: "${JWT_SECRET}" JWT_SECRET: "${JWT_SECRET}"
volumes: volumes:
- ./test-results:/app/test-results - ./test-results:/app/test-results
@@ -81,6 +93,7 @@ services:
- testnet - testnet
repository.tests: repository.tests:
env_file: ".env.test"
image: repository.tests image: repository.tests
container_name: test-env-repository-tests container_name: test-env-repository-tests
depends_on: depends_on:
@@ -93,8 +106,6 @@ services:
BUILD_CONFIGURATION: Release BUILD_CONFIGURATION: Release
environment: environment:
DOTNET_RUNNING_IN_CONTAINER: "true" DOTNET_RUNNING_IN_CONTAINER: "true"
DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}"
JWT_SECRET: "${JWT_SECRET}"
volumes: volumes:
- ./test-results:/app/test-results - ./test-results:/app/test-results
restart: "no" restart: "no"

View File

@@ -16,4 +16,4 @@ WORKDIR /src
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/test-results RUN mkdir -p /app/test-results
WORKDIR /src/API/API.Specs WORKDIR /src/API/API.Specs
ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--no-build", "--no-restore", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"] ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"]

View File

@@ -10,11 +10,6 @@ namespace API.Specs
protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void ConfigureWebHost(IWebHostBuilder builder)
{ {
builder.UseEnvironment("Testing"); builder.UseEnvironment("Testing");
builder.ConfigureAppConfiguration((context, configBuilder) =>
{
var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
});
} }
} }
} }

View File

@@ -7,8 +7,39 @@ namespace Database.Migrations;
public static class Program public static class Program
{ {
private static readonly string? connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); private static string BuildConnectionString(string? databaseName = null)
private static readonly string? masterConnectionString = Environment.GetEnvironmentVariable("MASTER_DB_CONNECTION_STRING"); {
var server = Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
var dbName = databaseName
?? Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException("DB_NAME environment variable is not set");
var user = Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException("DB_USER environment variable is not set");
var password = Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE")
?? "True";
var builder = new SqlConnectionStringBuilder
{
DataSource = server,
InitialCatalog = dbName,
UserID = user,
Password = password,
TrustServerCertificate = bool.Parse(trustServerCertificate),
Encrypt = true
};
return builder.ConnectionString;
}
private static readonly string connectionString = BuildConnectionString();
private static readonly string masterConnectionString = BuildConnectionString("master");
private static bool DeployMigrations() private static bool DeployMigrations()
{ {

View File

@@ -3,9 +3,39 @@ using Microsoft.Data.SqlClient;
using DbUp; using DbUp;
using System.Reflection; using System.Reflection;
string BuildConnectionString()
{
var server = Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
var dbName = Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException("DB_NAME environment variable is not set");
var user = Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException("DB_USER environment variable is not set");
var password = Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
var trustServerCertificate = Environment.GetEnvironmentVariable("DB_TRUST_SERVER_CERTIFICATE")
?? "True";
var builder = new SqlConnectionStringBuilder
{
DataSource = server,
InitialCatalog = dbName,
UserID = user,
Password = password,
TrustServerCertificate = bool.Parse(trustServerCertificate),
Encrypt = true
};
return builder.ConnectionString;
}
try try
{ {
var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); var connectionString = BuildConnectionString();
Console.WriteLine("Attempting to connect to database..."); Console.WriteLine("Attempting to connect to database...");

View File

@@ -7,11 +7,36 @@ namespace DataAccessLayer.Sql
{ {
public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlConnectionFactory
{ {
private readonly string _connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") private readonly string _connectionString = GetConnectionString(configuration);
?? configuration.GetConnectionString("Default")
?? throw new InvalidOperationException( private static string GetConnectionString(IConfiguration configuration)
"Database connection string not configured. Set DB_CONNECTION_STRING env var or ConnectionStrings:Default." {
); // Check for full connection string first
var fullConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING");
if (!string.IsNullOrEmpty(fullConnectionString))
{
return fullConnectionString;
}
// Try to build from individual environment variables (preferred method for Docker)
try
{
return SqlConnectionStringHelper.BuildConnectionString();
}
catch (InvalidOperationException)
{
// Fall back to configuration-based connection string if env vars are not set
var connString = configuration.GetConnectionString("Default");
if (!string.IsNullOrEmpty(connString))
{
return connString;
}
throw new InvalidOperationException(
"Database connection string not configured. Set DB_CONNECTION_STRING or DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD env vars or ConnectionStrings:Default."
);
}
}
public DbConnection CreateConnection() public DbConnection CreateConnection()
{ {

View File

@@ -0,0 +1,50 @@
using Microsoft.Data.SqlClient;
namespace DataAccessLayer.Sql
{
public static class SqlConnectionStringHelper
{
/// <summary>
/// Builds a SQL Server connection string from environment variables.
/// Expects DB_SERVER, DB_NAME, DB_USER, DB_PASSWORD, and DB_TRUST_SERVER_CERTIFICATE.
/// </summary>
/// <param name="databaseName">Optional override for the database name. If null, uses DB_NAME env var.</param>
/// <returns>A properly formatted SQL Server connection string.</returns>
public static string BuildConnectionString(string? databaseName = null)
{
var server = Environment.GetEnvironmentVariable("DB_SERVER")
?? throw new InvalidOperationException("DB_SERVER environment variable is not set");
var dbName = databaseName
?? Environment.GetEnvironmentVariable("DB_NAME")
?? throw new InvalidOperationException("DB_NAME environment variable is not set");
var user = Environment.GetEnvironmentVariable("DB_USER")
?? throw new InvalidOperationException("DB_USER environment variable is not set");
var password = Environment.GetEnvironmentVariable("DB_PASSWORD")
?? throw new InvalidOperationException("DB_PASSWORD environment variable is not set");
var builder = new SqlConnectionStringBuilder
{
DataSource = server,
InitialCatalog = dbName,
UserID = user,
Password = password,
TrustServerCertificate = true,
Encrypt = true
};
return builder.ConnectionString;
}
/// <summary>
/// Builds a connection string to the master database using environment variables.
/// </summary>
/// <returns>A connection string for the master database.</returns>
public static string BuildMasterConnectionString()
{
return BuildConnectionString("master");
}
}
}

View File

@@ -11,4 +11,4 @@ RUN dotnet build "./Repository.Tests.csproj" -c $BUILD_CONFIGURATION -o /app/bui
FROM build AS final FROM build AS final
RUN mkdir -p /app/test-results RUN mkdir -p /app/test-results
WORKDIR /src/Repository/Repository.Tests WORKDIR /src/Repository/Repository.Tests
ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--no-build", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"] ENTRYPOINT ["dotnet", "test", "./Repository.Tests.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/repository-tests.trx"]