Refactor database connection handling and update environment variable usage across Docker configurations

This commit is contained in:
Aaron Po
2026-02-08 14:57:45 -05:00
parent b22e1e5702
commit 243931eb6a
13 changed files with 234 additions and 45 deletions

18
.env.example Normal file
View File

@@ -0,0 +1,18 @@
# ======================
# Database Configuration
# ======================
# SQL Server Connection Components
# These are used to build connection strings dynamically
DB_SERVER=sqlserver,1433
DB_NAME=Biergarten
DB_USER=sa
DB_PASSWORD=YourStrong!Passw0rd
# ======================
# JWT Configuration
# ======================
# JWT Secret for signing tokens (generate using: 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

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"]