feat: add Docker Compose dev environment with nginx reverse proxy

This commit is contained in:
Stevan Freeborn
2026-07-29 16:29:19 -05:00
parent 94f113e4cc
commit f206dcd5fb
9 changed files with 297 additions and 0 deletions
@@ -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:
+42
View File
@@ -0,0 +1,42 @@
server {
listen 443 ssl;
server_name ~^(?<org_slug>[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 ~^(?<org_slug>[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://<org-slug>.paragonplayground.localhost";
}
}
+22
View File
@@ -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
+22
View File
@@ -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
@@ -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
+27
View File
@@ -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
+26
View File
@@ -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
@@ -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
}
+27
View File
@@ -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