From 80fc813b61a1036a5bc067d44ba3857a4a01b929 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Wed, 1 Jul 2026 10:14:26 +0200 Subject: [PATCH] build: add production-parity staging environment on Docker Layer a staging overlay (docker-compose.staging.yml) on the base compose file: same Linux images and Production runtime as prod, differing only in the dev banner, capped sync, shifted ports (18080/18081), and an isolated project namespace so it never touches prod data. Wire -Staging into deploy/up.ps1 and deploy/down.ps1, add .env.staging.example, and track that template in git. Co-Authored-By: Claude Opus 4.8 --- .env.staging.example | 17 +++++++++++++++ .gitignore | 1 + deploy/down.ps1 | 17 +++++++++++---- deploy/up.ps1 | 37 +++++++++++++++++++++++--------- docker-compose.staging.yml | 44 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 .env.staging.example create mode 100644 docker-compose.staging.yml diff --git a/.env.staging.example b/.env.staging.example new file mode 100644 index 0000000..c893f60 --- /dev/null +++ b/.env.staging.example @@ -0,0 +1,17 @@ +# Copy to deploy/.env.staging and fill in. Used by: +# docker compose -p inboxintel-staging --env-file deploy/.env.staging \ +# -f docker-compose.yml -f docker-compose.staging.yml up +# (or ./deploy/up.ps1 -Staging). Kept separate from deploy/.env (production) so +# staging can never touch prod credentials, DB, or Google project. +POSTGRES_PASSWORD=change-me-staging + +# Use a SEPARATE Google OAuth client for staging with redirect URI: +# http://localhost:18081/signin-google +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +AI_MODE=Disabled +FRONTEND_ORIGIN=http://localhost:18081 + +# Staging caps the initial mailbox sync so rehearsals are fast. +MAX_MESSAGES=2000 diff --git a/.gitignore b/.gitignore index ec071ac..43dea55 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ lpt[1-9].* *.code-workspace .env.* !.env.example +!.env.staging.example .next/ dist/ build/ diff --git a/deploy/down.ps1 b/deploy/down.ps1 index deb704e..403c073 100644 --- a/deploy/down.ps1 +++ b/deploy/down.ps1 @@ -5,13 +5,22 @@ ./deploy/down.ps1 ./deploy/down.ps1 -Volumes #> -param([switch]$Volumes) +param([switch]$Volumes, [switch]$Staging) $ErrorActionPreference = 'Stop' -$root = Split-Path -Parent $PSScriptRoot -$envFile = Join-Path $PSScriptRoot '.env' +$root = Split-Path -Parent $PSScriptRoot -$composeArgs = @('compose', '--env-file', $envFile, 'down') +if ($Staging) { + $envFile = Join-Path $PSScriptRoot '.env.staging' + $composeFiles = @('-f', 'docker-compose.yml', '-f', 'docker-compose.staging.yml') + $project = @('-p', 'inboxintel-staging') +} else { + $envFile = Join-Path $PSScriptRoot '.env' + $composeFiles = @() + $project = @() +} + +$composeArgs = @('compose') + $project + @('--env-file', $envFile) + $composeFiles + @('down') if ($Volumes) { $composeArgs += '--volumes' } Push-Location $root diff --git a/deploy/up.ps1 b/deploy/up.ps1 index a6dfd5b..46f55b8 100644 --- a/deploy/up.ps1 +++ b/deploy/up.ps1 @@ -8,15 +8,28 @@ #> param( [switch]$Proxy, - [switch]$Foreground + [switch]$Foreground, + [switch]$Staging # production-shaped staging stack: separate env, ports, volumes ) $ErrorActionPreference = 'Stop' -$root = Split-Path -Parent $PSScriptRoot # repo root (deploy/ is one level down) -$envFile = Join-Path $PSScriptRoot '.env' +$root = Split-Path -Parent $PSScriptRoot # repo root (deploy/ is one level down) -if (-not (Test-Path $envFile)) { - throw "Missing $envFile. Create it from .env.example with your real secrets." +# Staging vs production: pick the env file + compose overlay + isolated project name. +if ($Staging) { + $envFile = Join-Path $PSScriptRoot '.env.staging' + $composeFiles = @('-f', 'docker-compose.yml', '-f', 'docker-compose.staging.yml') + $project = @('-p', 'inboxintel-staging') + if (-not (Test-Path $envFile)) { + throw "Missing $envFile. Create it from .env.staging.example." + } +} else { + $envFile = Join-Path $PSScriptRoot '.env' + $composeFiles = @() + $project = @() + if (-not (Test-Path $envFile)) { + throw "Missing $envFile. Create it from .env.example with your real secrets." + } } # Fail fast if a required key is absent or blank. @@ -28,19 +41,23 @@ Get-Content $envFile | ForEach-Object { $missing = $required | Where-Object { [string]::IsNullOrWhiteSpace($envMap[$_]) } if ($missing) { throw "deploy/.env is missing values for: $($missing -join ', ')" } -$composeArgs = @('compose', '--env-file', $envFile) +$composeArgs = @('compose') + $project + @('--env-file', $envFile) + $composeFiles if ($Proxy) { $composeArgs += @('--profile', 'proxy') } $composeArgs += @('up', '--build') if (-not $Foreground) { $composeArgs += '-d' } Push-Location $root try { - Write-Host "Starting InboxIntel via docker compose (env: deploy/.env)..." -ForegroundColor Cyan + Write-Host "Starting InboxIntel via docker compose (env: $envFile)..." -ForegroundColor Cyan & docker @composeArgs if (-not $Foreground) { - & docker compose --env-file $envFile ps - Write-Host "`nFrontend: http://localhost:8081 API/Swagger: http://localhost:8080/swagger" -ForegroundColor Green - Write-Host "Logs: ./deploy/logs.ps1 Stop: ./deploy/down.ps1" -ForegroundColor DarkGray + & docker compose @project --env-file $envFile @composeFiles ps + if ($Staging) { + Write-Host "`n[STAGING] Frontend: http://localhost:18081 API/Swagger: http://localhost:18080/swagger" -ForegroundColor Green + } else { + Write-Host "`nFrontend: http://localhost:8081 API/Swagger: http://localhost:8080/swagger" -ForegroundColor Green + } + Write-Host "Logs: ./deploy/logs.ps1 Stop: ./deploy/down.ps1$(if($Staging){' -Staging'})" -ForegroundColor DarkGray } } finally { Pop-Location } diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..be62449 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,44 @@ +# Staging overlay for InboxIntel. +# +# The base docker-compose.yml IS the production definition (Linux containers, +# ASPNETCORE_ENVIRONMENT=Production). This overlay layers a *staging* variant on +# top of it so you can run a production-shaped stack locally on Windows WITHOUT +# clobbering a real production deployment's data, ports, or volumes. +# +# It differs from prod only in the ways staging is meant to differ: +# - the dev/test banner is on (App__DevMode=true) +# - the initial Gmail sync is capped so a big mailbox doesn't take forever +# - ports are shifted into the 18xxx range so staging can run alongside prod +# - a distinct project name gives it its own isolated pgdata + keys volumes +# +# Run it with the -p (project name) flag so volumes/networks are namespaced: +# +# docker compose -p inboxintel-staging \ +# --env-file deploy/.env.staging \ +# -f docker-compose.yml -f docker-compose.staging.yml up --build -d +# +# (deploy/up.ps1 -Staging / up.sh --staging wrap this for you.) + +services: + postgres: + ports: + - "127.0.0.1:15432:5432" + + api: + environment: + # Same Production runtime as prod (real config binding, real build), but + # flagged as a non-production instance so the UI shows the staging banner + # and the first sync is bounded. + App__DevMode: "true" + GmailSync__MaxMessages: ${MAX_MESSAGES:-2000} + Cors__Origins__0: ${FRONTEND_ORIGIN:-http://localhost:18081} + ports: + - "127.0.0.1:18080:8080" + + frontend: + ports: + - "18081:80" + + nginx: + ports: + - "18000:80"