Add Docker containerization with external PostgreSQL support

- Add docker-compose.yml with PostgreSQL and Redis environment variables
- Add Dockerfile for multi-stage .NET build optimized for production
- Add .env.template with database connection configuration
- Update Program.cs with JWT authentication and Docker logging
- Configure external database connectivity (tested working)
- Container successfully connects to external PostgreSQL server

Infrastructure milestone: API containerized and database-ready
This commit is contained in:
matt 2025-10-28 15:40:50 -05:00
parent f8201ff080
commit 686001dc98
9 changed files with 675 additions and 11 deletions

View File

@ -0,0 +1,53 @@
# File Location: D:\shadowed-realms-mobile\ShadowedRealmsMobile\src\server\.dockerignore
# Build output directories
**/bin/
**/obj/
**/out/
# Visual Studio files
**/.vs/
**/*.user
**/*.suo
**/*.userprefs
**/.vscode/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Git files
.git/
.gitignore
.gitattributes
# Documentation
**/*.md
README*
# Test results
TestResults/
**/*.trx
**/*.coverage
# NuGet packages (will be restored during build)
**/packages/
**/*.nupkg
# Docker files (don't copy into container)
**/Dockerfile*
**/docker-compose*
**/.dockerignore
# Environment files (secrets shouldn't go in containers)
**/.env
**/.env.*
# Logs
**/logs/
**/*.log

View File

@ -0,0 +1,64 @@
# Shadowed Realms Environment Variables Template
# File Location: D:\shadowed-realms-mobile\ShadowedRealmsMobile\src\server\.env.template
# Copy this file to .env and fill in your actual values
# ===============================
# DATABASE CONFIGURATION
# ===============================
POSTGRES_HOST=209.25.140.218
POSTGRES_DB=ShadowedRealms
POSTGRES_USER=gameserver
POSTGRES_PASSWORD=w92oOUPGAR/ZRJaDynLQIq07aFzvTQ6tQzOJsXMStXE=
# ===============================
# REDIS CONFIGURATION
# ===============================
REDIS_HOST=209.25.140.218
REDIS_PORT=6379
# ===============================
# JWT AUTHENTICATION
# ===============================
JWT_SECRET_KEY=Ymn0e9ntVwbV&MUDRN$2wfWF^GDgBYWo
JWT_ISSUER=ShadowedRealms.API
JWT_AUDIENCE=ShadowedRealms.Players
# ===============================
# APPLICATION SETTINGS
# ===============================
ASPNETCORE_ENVIRONMENT=Development
KINGDOM_IDS=1,2,3,4,5
ALLOWED_ORIGINS=https://yourdomain.com,https://admin.yourdomain.com
# ===============================
# ADMIN SETTINGS (Optional)
# ===============================
ADMIN_USER=admin
ADMIN_PASSWORD=admin_secure_password_here
# ===============================
# INSTRUCTIONS
# ===============================
# 1. Copy this file to .env (DO NOT commit .env to git!)
# 2. Replace all placeholder values with your actual configuration
# 3. Make sure your PostgreSQL VM is accessible from Docker containers
# 4. Generate a secure JWT secret key (32+ characters)
# 5. Update POSTGRES_HOST with your actual VM IP address
# ===============================
# DOCKER COMMANDS
# ===============================
# Build and start the API:
# docker-compose up -d shadowed-realms-api
# Start with admin dashboard:
# docker-compose --profile admin up -d
# View logs:
# docker-compose logs -f shadowed-realms-api
# Stop all services:
# docker-compose down
# Rebuild after code changes:
# docker-compose build --no-cache

View File

@ -0,0 +1,64 @@
# Shadowed Realms Environment Variables Template
# File Location: D:\shadowed-realms-mobile\ShadowedRealmsMobile\src\server\.env.template
# Copy this file to .env and fill in your actual values
# ===============================
# DATABASE CONFIGURATION
# ===============================
POSTGRES_HOST=192.168.1.100
POSTGRES_DB=ShadowedRealms
POSTGRES_USER=gameserver
POSTGRES_PASSWORD=your_secure_database_password_here
# ===============================
# REDIS CONFIGURATION
# ===============================
REDIS_HOST=192.168.1.100
REDIS_PORT=6379
# ===============================
# JWT AUTHENTICATION
# ===============================
JWT_SECRET_KEY=your_super_secure_jwt_secret_key_minimum_32_characters_long_for_production_use
JWT_ISSUER=ShadowedRealms.API
JWT_AUDIENCE=ShadowedRealms.Players
# ===============================
# APPLICATION SETTINGS
# ===============================
ASPNETCORE_ENVIRONMENT=Production
KINGDOM_IDS=1,2,3,4,5
ALLOWED_ORIGINS=https://yourdomain.com,https://admin.yourdomain.com
# ===============================
# ADMIN SETTINGS (Optional)
# ===============================
ADMIN_USER=admin
ADMIN_PASSWORD=admin_secure_password_here
# ===============================
# INSTRUCTIONS
# ===============================
# 1. Copy this file to .env (DO NOT commit .env to git!)
# 2. Replace all placeholder values with your actual configuration
# 3. Make sure your PostgreSQL VM is accessible from Docker containers
# 4. Generate a secure JWT secret key (32+ characters)
# 5. Update POSTGRES_HOST with your actual VM IP address
# ===============================
# DOCKER COMMANDS
# ===============================
# Build and start the API:
# docker-compose up -d shadowed-realms-api
# Start with admin dashboard:
# docker-compose --profile admin up -d
# View logs:
# docker-compose logs -f shadowed-realms-api
# Stop all services:
# docker-compose down
# Rebuild after code changes:
# docker-compose build --no-cache

View File

@ -0,0 +1,73 @@
# Shadowed Realms API Dockerfile
# File Location: D:\shadowed-realms-mobile\ShadowedRealmsMobile\src\server\ShadowedRealms.API\Dockerfile
# Multi-stage build for production optimization
# ===============================
# BUILD STAGE
# ===============================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy solution file (from root directory)
COPY ShadowedRealmsMobile.sln ./
# Copy project files for dependency restoration (all existing projects)
COPY src/server/ShadowedRealms.API/*.csproj ./src/server/ShadowedRealms.API/
COPY src/server/ShadowedRealms.Admin/*.csproj ./src/server/ShadowedRealms.Admin/
COPY src/server/ShadowedRealms.Core/*.csproj ./src/server/ShadowedRealms.Core/
COPY src/server/ShadowedRealms.Data/*.csproj ./src/server/ShadowedRealms.Data/
COPY src/server/ShadowedRealms.Shared/*.csproj ./src/server/ShadowedRealms.Shared/
COPY src/server/ShadowedRealms.SignalR/*.csproj ./src/server/ShadowedRealms.SignalR/
# Restore dependencies (this layer is cached if project files don't change)
RUN dotnet restore src/server/ShadowedRealms.API/ShadowedRealms.API.csproj
# Copy all source code
COPY src/server/ ./src/server/
# Build the application
RUN dotnet build src/server/ShadowedRealms.API/ShadowedRealms.API.csproj -c Release -o /app/build
# Publish the application
RUN dotnet publish src/server/ShadowedRealms.API/ShadowedRealms.API.csproj -c Release -o /app/publish /p:UseAppHost=false
# ===============================
# RUNTIME STAGE
# ===============================
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Create non-root user for security
RUN groupadd -r gameserver && useradd --no-log-init -r -g gameserver gameserver
WORKDIR /app
# Copy published application from build stage
COPY --from=build /app/publish .
# Create log directory and set permissions
RUN mkdir -p /app/logs && chown -R gameserver:gameserver /app
# Switch to non-root user
USER gameserver
# ===============================
# CONFIGURATION
# ===============================
# Expose port 8080 (standard for containers)
EXPOSE 8080
# Environment variables
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_PRINT_TELEMETRY_MESSAGE=false
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Entry point
ENTRYPOINT ["dotnet", "ShadowedRealms.API.dll"]

View File

@ -1,25 +1,200 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// ===============================
// DATABASE CONFIGURATION
// ===============================
// Get database connection string (Docker override support)
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(connectionString))
{
// Fallback to environment variable for Docker
connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING");
}
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException("Database connection string not configured. Set ConnectionStrings:DefaultConnection in appsettings.json or DATABASE_CONNECTION_STRING environment variable.");
}
// TODO: Add Entity Framework when ready
// builder.Services.AddDbContext<GameDbContext>(options =>
// options.UseNpgsql(connectionString));
// ===============================
// AUTHENTICATION (JWT)
// ===============================
var jwtSection = builder.Configuration.GetSection("JWT");
var jwtSecret = jwtSection["SecretKey"]
?? Environment.GetEnvironmentVariable("JWT_SECRET_KEY");
if (string.IsNullOrEmpty(jwtSecret) || jwtSecret.Contains("REPLACE"))
{
if (builder.Environment.IsDevelopment())
{
Console.WriteLine("WARNING: Using development JWT secret. Set JWT_SECRET_KEY environment variable for production.");
}
else
{
throw new InvalidOperationException("JWT_SECRET_KEY environment variable must be set for production.");
}
}
// Add JWT Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSection["Issuer"],
ValidAudience = jwtSection["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSecret ?? "dev_jwt_secret_key_at_least_32_chars_long_not_for_production_use_only")),
ClockSkew = TimeSpan.Zero
};
});
// ===============================
// REDIS CONFIGURATION
// ===============================
var redisConnection = builder.Configuration.GetConnectionString("Redis")
?? Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING")
?? "localhost:6379";
// TODO: Add Redis when ready
// builder.Services.AddSingleton<IConnectionMultiplexer>(provider =>
// ConnectionMultiplexer.Connect(redisConnection));
// ===============================
// API SERVICES
// ===============================
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "Shadowed Realms API",
Version = "v1",
Description = "Medieval Fantasy Strategy MMO Backend API"
});
// JWT Bearer Authentication in Swagger
options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below.",
Name = "Authorization",
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
{
{
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Reference = new Microsoft.OpenApi.Models.OpenApiReference
{
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
// CORS configuration with environment variable support
var allowedOrigins = builder.Configuration.GetSection("AllowedOrigins").Get<string[]>()
?? Environment.GetEnvironmentVariable("ALLOWED_ORIGINS")?.Split(',')
?? new[] { "http://localhost:3000" };
builder.Services.AddCors(options =>
{
options.AddPolicy("ShadowedRealmsPolicy", policy =>
{
policy.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
// ===============================
// HEALTH CHECKS
// ===============================
builder.Services.AddHealthChecks();
// TODO: Add database health checks when EF is configured
// .AddNpgSql(connectionString)
// .AddRedis(redisConnection);
// ===============================
// BUILD APPLICATION
// ===============================
var app = builder.Build();
// Configure the HTTP request pipeline.
// ===============================
// MIDDLEWARE PIPELINE
// ===============================
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Shadowed Realms API V1");
options.RoutePrefix = "swagger";
});
}
app.UseHttpsRedirection();
app.UseCors("ShadowedRealmsPolicy");
// Authentication and Authorization (proper order)
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
// Health check endpoint
app.MapHealthChecks("/health");
// ===============================
// STARTUP LOGGING
// ===============================
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Shadowed Realms API starting...");
logger.LogInformation("Environment: {Environment}", app.Environment.EnvironmentName);
// Docker-specific logging
var containerId = Environment.GetEnvironmentVariable("CONTAINER_ID");
var kingdomIds = Environment.GetEnvironmentVariable("KINGDOM_IDS");
if (!string.IsNullOrEmpty(containerId))
{
logger.LogInformation("Container ID: {ContainerId}", containerId);
}
if (!string.IsNullOrEmpty(kingdomIds))
{
logger.LogInformation("Managing Kingdoms: {KingdomIds}", kingdomIds);
}
logger.LogInformation("API ready at /swagger (development) and /health");
app.Run();

View File

@ -0,0 +1,35 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=ShadowedRealms_Dev;Username=postgres;Password=devpassword",
"Redis": "localhost:6379"
},
"JWT": {
"SecretKey": "dev_jwt_secret_key_at_least_32_chars_long_not_for_production_use_only",
"Issuer": "ShadowedRealms.API.Dev",
"Audience": "ShadowedRealms.Players.Dev",
"ExpiresInMinutes": 240
},
"AllowedOrigins": [
"http://localhost:3000",
"https://localhost:3001",
"http://127.0.0.1:3000"
],
"GameSettings": {
"MaxPlayersPerKingdom": 100,
"KingdomTaxRate": 0.04,
"MaxAlliancesPerKingdom": 10,
"FieldInterceptionEnabled": true,
"CoalitionSystemEnabled": true,
"DevModeEnabled": true,
"FastProgressionEnabled": true
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"ShadowedRealms": "Debug",
"Microsoft.EntityFrameworkCore": "Information"
}
}
}

View File

@ -1,8 +1,35 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=ShadowedRealms_Dev;Username=postgres;Password=devpassword",
"Redis": "localhost:6379"
},
"JWT": {
"SecretKey": "dev_jwt_secret_key_at_least_32_chars_long_not_for_production_use_only",
"Issuer": "ShadowedRealms.API.Dev",
"Audience": "ShadowedRealms.Players.Dev",
"ExpiresInMinutes": 240
},
"AllowedOrigins": [
"http://localhost:3000",
"https://localhost:3001",
"http://127.0.0.1:3000"
],
"GameSettings": {
"MaxPlayersPerKingdom": 100,
"KingdomTaxRate": 0.04,
"MaxAlliancesPerKingdom": 10,
"FieldInterceptionEnabled": true,
"CoalitionSystemEnabled": true,
"DevModeEnabled": true,
"FastProgressionEnabled": true
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"ShadowedRealms": "Debug",
"Microsoft.EntityFrameworkCore": "Information"
}
}
}
}

View File

@ -1,9 +1,31 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=postgres-vm-host;Database=ShadowedRealms;Username=gameserver;Password=REPLACE_IN_DOCKER",
"Redis": "redis-vm-host:6379"
},
"JWT": {
"SecretKey": "REPLACE_WITH_ENV_VARIABLE",
"Issuer": "ShadowedRealms.API",
"Audience": "ShadowedRealms.Players",
"ExpiresInMinutes": 60
},
"AllowedOrigins": [
"https://shadowedrealms.game",
"https://admin.shadowedrealms.game"
],
"GameSettings": {
"MaxPlayersPerKingdom": 1500,
"KingdomTaxRate": 0.04,
"MaxAlliancesPerKingdom": 50,
"FieldInterceptionEnabled": true,
"CoalitionSystemEnabled": true
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"ShadowedRealms": "Information"
}
},
"AllowedHosts": "*"
}
}

View File

@ -0,0 +1,151 @@
# Shadowed Realms Docker Compose Configuration
# File Location: D:\shadowed-realms-mobile\ShadowedRealmsMobile\src\server\docker-compose.yml
version: '3.8'
services:
# ===============================
# GAME API SERVER
# ===============================
shadowed-realms-api:
build:
context: ../../
dockerfile: src/server/ShadowedRealms.API/Dockerfile
image: shadowedrealms/api:latest
container_name: sr-api
restart: unless-stopped
ports:
- "8080:8080" # API endpoints
- "8443:8443" # HTTPS (if configured)
environment:
# Database connection (replace with your PostgreSQL VM IP)
- DATABASE_CONNECTION_STRING=Host=${POSTGRES_HOST};Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}
# Redis connection (replace with your Redis VM IP or use same as Postgres)
- REDIS_CONNECTION_STRING=${REDIS_HOST}:${REDIS_PORT}
# JWT Configuration (MUST be set for production)
- JWT_SECRET_KEY=${JWT_SECRET_KEY}
- JWT_ISSUER=${JWT_ISSUER:-ShadowedRealms.API}
- JWT_AUDIENCE=${JWT_AUDIENCE:-ShadowedRealms.Players}
# CORS Origins
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-https://yourdomain.com,https://admin.yourdomain.com}
# Container identification
- CONTAINER_ID=sr-api-main
- KINGDOM_IDS=${KINGDOM_IDS:-1,2,3,4,5}
# ASP.NET Core settings
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Production}
- ASPNETCORE_URLS=http://+:8080
volumes:
# Persist logs outside container
- api-logs:/app/logs
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- shadowed-realms-network
# ===============================
# LOAD BALANCER / REVERSE PROXY
# ===============================
nginx:
image: nginx:alpine
container_name: sr-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro # SSL certificates (when ready)
- nginx-logs:/var/log/nginx
depends_on:
- shadowed-realms-api
networks:
- shadowed-realms-network
# ===============================
# ADMIN DASHBOARD (Optional)
# ===============================
shadowed-realms-admin:
build:
context: .
dockerfile: ShadowedRealms.Admin/Dockerfile # Create this later
image: shadowedrealms/admin:latest
container_name: sr-admin
restart: unless-stopped
ports:
- "3000:3000"
environment:
- DATABASE_CONNECTION_STRING=Host=${POSTGRES_HOST:-your-postgres-vm-ip};Database=${POSTGRES_DB:-ShadowedRealms};Username=${ADMIN_USER:-admin};Password=${ADMIN_PASSWORD}
- API_BASE_URL=http://shadowed-realms-api:8080
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Production}
volumes:
- admin-logs:/app/logs
depends_on:
- shadowed-realms-api
networks:
- shadowed-realms-network
profiles:
- admin # Only start when explicitly requested
# ===============================
# VOLUMES
# ===============================
volumes:
api-logs:
driver: local
admin-logs:
driver: local
nginx-logs:
driver: local
# ===============================
# NETWORKS
# ===============================
networks:
shadowed-realms-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# ===============================
# ENVIRONMENT VARIABLES REQUIRED
# ===============================
# Create a .env file in the same directory with these values:
#
# POSTGRES_HOST=your.postgres.vm.ip
# POSTGRES_DB=ShadowedRealms
# POSTGRES_USER=gameserver
# POSTGRES_PASSWORD=your_secure_password
#
# REDIS_HOST=your.redis.vm.ip
# REDIS_PORT=6379
#
# JWT_SECRET_KEY=your_super_secure_jwt_secret_minimum_32_characters_long
# JWT_ISSUER=ShadowedRealms.API
# JWT_AUDIENCE=ShadowedRealms.Players
#
# KINGDOM_IDS=1,2,3,4,5
# ALLOWED_ORIGINS=https://yourdomain.com,https://admin.yourdomain.com