From f206dcd5fb95d4c5d32a700e8e32f07e23e3ec21 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn Date: Wed, 29 Jul 2026 16:29:19 -0500 Subject: [PATCH] feat: add Docker Compose dev environment with nginx reverse proxy --- src/ParagonPlayground/docker-compose.dev.yml | 51 +++++++++++++++ src/ParagonPlayground/nginx/nginx.conf | 42 ++++++++++++ src/ParagonPlayground/scripts/build.ps1 | 22 +++++++ src/ParagonPlayground/scripts/clean.ps1 | 22 +++++++ src/ParagonPlayground/scripts/dev-down.ps1 | 15 +++++ src/ParagonPlayground/scripts/dev-up.ps1 | 27 ++++++++ src/ParagonPlayground/scripts/install.ps1 | 26 ++++++++ src/ParagonPlayground/scripts/setup-certs.ps1 | 65 +++++++++++++++++++ src/ParagonPlayground/scripts/test.ps1 | 27 ++++++++ 9 files changed, 297 insertions(+) create mode 100644 src/ParagonPlayground/docker-compose.dev.yml create mode 100644 src/ParagonPlayground/nginx/nginx.conf create mode 100644 src/ParagonPlayground/scripts/build.ps1 create mode 100644 src/ParagonPlayground/scripts/clean.ps1 create mode 100644 src/ParagonPlayground/scripts/dev-down.ps1 create mode 100644 src/ParagonPlayground/scripts/dev-up.ps1 create mode 100644 src/ParagonPlayground/scripts/install.ps1 create mode 100644 src/ParagonPlayground/scripts/setup-certs.ps1 create mode 100644 src/ParagonPlayground/scripts/test.ps1 diff --git a/src/ParagonPlayground/docker-compose.dev.yml b/src/ParagonPlayground/docker-compose.dev.yml new file mode 100644 index 0000000..346ce4a --- /dev/null +++ b/src/ParagonPlayground/docker-compose.dev.yml @@ -0,0 +1,51 @@ +services: + mongodb: + image: mongo:7 + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh --quiet + interval: 10s + retries: 5 + start_period: 20s + backend: + container_name: paragonplayground-backend + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "5000:8080" + environment: + - MongoDb__ConnectionString=mongodb://mongodb:27017 + - MongoDb__DatabaseName=paragon_playground + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://+:8080 + depends_on: + mongodb: + condition: service_healthy + frontend: + container_name: paragonplayground-frontend + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:3000" + volumes: + - ./frontend/src:/app/src + depends_on: + - backend + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + - ./certs:/etc/nginx/certs + depends_on: + - backend + - frontend +volumes: + mongo_data: diff --git a/src/ParagonPlayground/nginx/nginx.conf b/src/ParagonPlayground/nginx/nginx.conf new file mode 100644 index 0000000..07c11a8 --- /dev/null +++ b/src/ParagonPlayground/nginx/nginx.conf @@ -0,0 +1,42 @@ +server { + listen 443 ssl; + server_name ~^(?[a-z0-9-]+)\.paragonplayground\.localhost$; + + ssl_certificate /etc/nginx/certs/_wildcard.paragonplayground.localhost.pem; + ssl_certificate_key /etc/nginx/certs/_wildcard.paragonplayground.localhost-key.pem; + + location /api/ { + proxy_pass http://backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Organization-Slug $org_slug; + } + + location / { + proxy_pass http://frontend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +server { + listen 80; + server_name ~^(?[a-z0-9-]+)\.paragonplayground\.localhost$; + return 301 https://$host$request_uri; +} + +server { + listen 80; + server_name paragonplayground.localhost; + + location / { + return 400 "Missing organization subdomain. Use https://.paragonplayground.localhost"; + } +} diff --git a/src/ParagonPlayground/scripts/build.ps1 b/src/ParagonPlayground/scripts/build.ps1 new file mode 100644 index 0000000..4c7f636 --- /dev/null +++ b/src/ParagonPlayground/scripts/build.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Builds the .NET solution. +.PARAMETER Configuration + Build configuration (Debug | Release). Defaults to Debug. +#> +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug' +) + +$repoRoot = Join-Path $PSScriptRoot '..' +$sln = Join-Path $repoRoot 'backend\ParagonPlayground.slnx' + +if (-not (Test-Path $sln)) { + Write-Host "Solution not found at $sln" -ForegroundColor Red + exit 1 +} + +Write-Host "Building $Configuration..." -ForegroundColor Cyan +dotnet build $sln --configuration $Configuration +exit $LASTEXITCODE diff --git a/src/ParagonPlayground/scripts/clean.ps1 b/src/ParagonPlayground/scripts/clean.ps1 new file mode 100644 index 0000000..3420331 --- /dev/null +++ b/src/ParagonPlayground/scripts/clean.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Cleans .NET build artifacts (bin/obj) for all projects in the solution. +#> + +$repoRoot = Join-Path $PSScriptRoot '..' +$sln = Join-Path $repoRoot 'backend\ParagonPlayground.slnx' + +if (-not (Test-Path $sln)) { + Write-Host "Solution not found at $sln" -ForegroundColor Red + exit 1 +} + +Write-Host "Running dotnet clean..." -ForegroundColor Cyan +dotnet clean $sln --verbosity quiet + +Write-Host "Removing bin/obj directories..." -ForegroundColor Cyan +$dirs = Get-ChildItem -Path (Join-Path $repoRoot 'backend') -Include bin,obj -Recurse -Directory +$dirs | Remove-Item -Recurse -Force + +Write-Host "Cleaned $($dirs.Count) directories." -ForegroundColor Green +exit $LASTEXITCODE diff --git a/src/ParagonPlayground/scripts/dev-down.ps1 b/src/ParagonPlayground/scripts/dev-down.ps1 new file mode 100644 index 0000000..ef6d342 --- /dev/null +++ b/src/ParagonPlayground/scripts/dev-down.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Stops the development environment and removes containers/networks. +#> + +$composeFile = Join-Path $PSScriptRoot '..\docker-compose.dev.yml' + +if (-not (Test-Path $composeFile)) { + Write-Host "Compose file not found at $composeFile" -ForegroundColor Red + exit 1 +} + +Write-Host "Stopping development environment..." -ForegroundColor Cyan +docker compose -f $composeFile down +exit $LASTEXITCODE diff --git a/src/ParagonPlayground/scripts/dev-up.ps1 b/src/ParagonPlayground/scripts/dev-up.ps1 new file mode 100644 index 0000000..c29c027 --- /dev/null +++ b/src/ParagonPlayground/scripts/dev-up.ps1 @@ -0,0 +1,27 @@ +<# +.SYNOPSIS + Starts the development environment via Docker Compose. +.PARAMETER Detached + Run containers in the background. Defaults to true. +#> +param( + [switch]$Detached = $true +) + +$composeFile = Join-Path $PSScriptRoot '..\docker-compose.dev.yml' + +if (-not (Test-Path $composeFile)) { + Write-Host "Compose file not found at $composeFile" -ForegroundColor Red + exit 1 +} + +Write-Host "Starting development environment..." -ForegroundColor Cyan +$detachArg = if ($Detached) { '-d' } else { '' } +docker compose -f $composeFile up --build $detachArg + +if ($LASTEXITCODE -eq 0 -and $Detached) { + Write-Host "" + Write-Host "All services started. Run 'scripts/dev-down.ps1' to stop." -ForegroundColor Green + Write-Host "Open https://acme.paragonplayground.localhost in your browser." -ForegroundColor Green +} +exit $LASTEXITCODE diff --git a/src/ParagonPlayground/scripts/install.ps1 b/src/ParagonPlayground/scripts/install.ps1 new file mode 100644 index 0000000..92bce96 --- /dev/null +++ b/src/ParagonPlayground/scripts/install.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Restores .NET packages and installs frontend npm dependencies. +#> + +$projectRoot = Join-Path $PSScriptRoot '..' +$backendDir = Join-Path $projectRoot 'backend' +$frontendDir = Join-Path $projectRoot 'frontend' +$sln = Join-Path $backendDir 'ParagonPlayground.slnx' + +Write-Host "Restoring .NET packages..." -ForegroundColor Cyan +if (Test-Path $sln) { + dotnet restore $sln +} else { + dotnet restore $backendDir +} + +Write-Host "`nInstalling frontend npm dependencies..." -ForegroundColor Cyan +if (Test-Path $frontendDir) { + Push-Location $frontendDir + npm install + Pop-Location +} + +Write-Host "`nAll dependencies installed." -ForegroundColor Green +exit $LASTEXITCODE diff --git a/src/ParagonPlayground/scripts/setup-certs.ps1 b/src/ParagonPlayground/scripts/setup-certs.ps1 new file mode 100644 index 0000000..61dacaa --- /dev/null +++ b/src/ParagonPlayground/scripts/setup-certs.ps1 @@ -0,0 +1,65 @@ +<# +.SYNOPSIS + Generates local TLS certificates for *.paragonplayground.localhost using mkcert. + +.DESCRIPTION + Uses mkcert (https://github.com/FiloSottile/mkcert) to create a local + Certificate Authority and sign a wildcard certificate for local development. + The CA is automatically trusted by the OS and browsers (no security warnings). + + If mkcert is not installed, the script will offer to install it via winget. + +.NOTES + Run this script from an elevated (Admin) PowerShell prompt so mkcert can + install the CA into the JDK system trust store (cacerts). Without elevation + the CA is still trusted by the OS and browsers, but Java tools may reject + the certificate. +#> + +$certsDir = Join-Path $PSScriptRoot "..\certs" + +if (-not (Test-Path $certsDir)) { + New-Item -ItemType Directory -Path $certsDir -Force | Out-Null +} + +# Check if mkcert is available +$mkcert = Get-Command mkcert -ErrorAction SilentlyContinue + +if (-not $mkcert) { + Write-Host "mkcert not found." -ForegroundColor Yellow + $install = Read-Host "Install mkcert via winget? (y/n)" + + if ($install -eq "y") { + winget install FiloSottile.mkcert + + if ($LASTEXITCODE -ne 0) { + Write-Host "Installation failed. Install manually: https://github.com/FiloSottile/mkcert" -ForegroundColor Red + exit 1 + } + + $env:Path = [Environment]::GetEnvironmentVariable("Path", "User") + ";$env:Path" + } else { + Write-Host "Install mkcert manually from https://github.com/FiloSottile/mkcert and re-run." -ForegroundColor Red + exit 1 + } +} + +Write-Host "Installing local CA (may prompt for admin)..." -ForegroundColor Cyan & mkcert -install + +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to install CA." -ForegroundColor Red + exit 1 +} + +Write-Host "Generating certificate for *.paragonplayground.localhost..." -ForegroundColor Cyan & mkcert -cert-file "$certsDir/_wildcard.paragonplayground.localhost.pem" ` + -key-file "$certsDir/_wildcard.paragonplayground.localhost-key.pem" ` + "*.paragonplayground.localhost" + +if ($LASTEXITCODE -eq 0) { + Write-Host "Certificates generated in $certsDir" -ForegroundColor Green + Write-Host " cert: $certsDir\_wildcard.paragonplayground.localhost.pem" -ForegroundColor Green + Write-Host " key: $certsDir\_wildcard.paragonplayground.localhost-key.pem" -ForegroundColor Green +} else { + Write-Host "Certificate generation failed." -ForegroundColor Red + exit 1 +} diff --git a/src/ParagonPlayground/scripts/test.ps1 b/src/ParagonPlayground/scripts/test.ps1 new file mode 100644 index 0000000..5027913 --- /dev/null +++ b/src/ParagonPlayground/scripts/test.ps1 @@ -0,0 +1,27 @@ +<# +.SYNOPSIS + Runs .NET tests for the solution. +.PARAMETER Configuration + Build configuration (Debug | Release). Defaults to Debug. +.PARAMETER NoBuild + Skip build step. Defaults to false. +#> +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug', + [switch]$NoBuild +) + +$repoRoot = Join-Path $PSScriptRoot '..' +$sln = Join-Path $repoRoot 'backend\ParagonPlayground.slnx' + +if (-not (Test-Path $sln)) { + Write-Host "Solution not found at $sln" -ForegroundColor Red + exit 1 +} + +$noBuildArg = if ($NoBuild) { '--no-build' } else { '' } + +Write-Host "Running tests ($Configuration)..." -ForegroundColor Cyan +dotnet test $sln --configuration $Configuration $noBuildArg --verbosity normal +exit $LASTEXITCODE