Add deploy tooling, docs, and Aspire config

This commit is contained in:
Holden
2026-06-20 14:33:53 -05:00
parent 95751904fa
commit 942bb4c80b
10 changed files with 349 additions and 3 deletions

22
deploy/.env.example Normal file
View File

@@ -0,0 +1,22 @@
# Copy this file to .env and fill in production values before deploying.
# Registry login used by deploy/push-images.ps1.
REGISTRY_ENDPOINT=git.recroomarchive.org
REGISTRY_REPOSITORY=recroomarchive-deploy/tenwholeyears-server
REGISTRY_USERNAME=replace-with-registry-username
REGISTRY_TOKEN=replace-with-registry-token
IMAGE_TAG=latest
# Database.
POSTGRES_PASSWORD=replace-with-long-random-postgres-password
# API configuration.
Jwt__Secret=replace-with-at-least-32-random-characters
Jwt__Issuer=https://your-api.example.com
Jwt__Audience=TenWholeYears
Jwt__ExpiryMinutes=60
Steam__ApiKey=replace-with-steam-web-api-key
Steam__AppId=480
Neutrino__ValidateSecret=true
Neutrino__Secret=replace-with-neutrino-secret
RecNet__UseForwardedHeaders=true

34
deploy/README.md Normal file
View File

@@ -0,0 +1,34 @@
## Build and Push Images
1. Copy `.env.example` to `.env` and fill in production values.
2. Run the Aspire-native push pipeline:
```powershell
.\deploy\push-images.ps1
```
Or from a POSIX shell:
```sh
sh ./deploy/push-images.sh
```
The shell script expects the Aspire CLI to be installed in that shell. In WSL, install it with the official Aspire installer:
```sh
curl -sSL https://aspire.dev/install.sh | bash
```
Or as a .NET tool:
```sh
dotnet tool install -g Aspire.Cli
```
The script loads `deploy/.env` when it exists, authenticates to `git.recroomarchive.org` using `REGISTRY_USERNAME` and `REGISTRY_TOKEN`, then runs `aspire do push`.
The included `docker-compose.yml` consumes the pushed images:
```powershell
docker compose --env-file .env up -d
```

52
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,52 @@
name: tenwholeyears-server
services:
postgres:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: recnet
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- recnet-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d recnet"]
interval: 10s
timeout: 5s
retries: 10
migrationservice:
image: ${REGISTRY_ENDPOINT:-git.recroomarchive.org}/${REGISTRY_REPOSITORY}/migrationservice:${IMAGE_TAG:-latest}
restart: "no"
environment:
ConnectionStrings__recnet: Host=postgres;Port=5432;Database=recnet;Username=postgres;Password=${POSTGRES_PASSWORD}
depends_on:
postgres:
condition: service_healthy
api:
image: ${REGISTRY_ENDPOINT:-git.recroomarchive.org}/${REGISTRY_REPOSITORY}/api:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- "${API_HTTP_PORT:-8080}:8080"
environment:
ASPNETCORE_HTTP_PORTS: 8080
ConnectionStrings__recnet: Host=postgres;Port=5432;Database=recnet;Username=postgres;Password=${POSTGRES_PASSWORD}
Jwt__Secret: ${Jwt__Secret}
Jwt__Issuer: ${Jwt__Issuer}
Jwt__Audience: ${Jwt__Audience}
Jwt__ExpiryMinutes: ${Jwt__ExpiryMinutes}
Steam__ApiKey: ${Steam__ApiKey}
Steam__AppId: ${Steam__AppId}
Neutrino__ValidateSecret: ${Neutrino__ValidateSecret}
Neutrino__Secret: ${Neutrino__Secret}
RecNet__UseForwardedHeaders: ${RecNet__UseForwardedHeaders}
depends_on:
postgres:
condition: service_healthy
migrationservice:
condition: service_completed_successfully
volumes:
recnet-postgres-data:

49
deploy/push-images.ps1 Normal file
View File

@@ -0,0 +1,49 @@
param(
[string] $Environment = "Production",
[string] $AppHostPath = "",
[string] $OutputPath = ""
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($AppHostPath)) {
$AppHostPath = Join-Path $PSScriptRoot "../src/AppHost/AppHost.csproj"
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $PSScriptRoot "aspire-output"
}
$envPath = Join-Path $PSScriptRoot ".env"
if (Test-Path $envPath) {
Get-Content $envPath | Where-Object { $_ -and $_ -notmatch '^\s*#' } | ForEach-Object {
$name, $value = $_ -split '=', 2
if (-not [string]::IsNullOrWhiteSpace($name) -and [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) {
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
}
}
}
$requiredEnvironmentVariables = @(
"REGISTRY_USERNAME",
"REGISTRY_TOKEN",
"REGISTRY_REPOSITORY"
)
$missing = $requiredEnvironmentVariables | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) }
if ($missing.Count -gt 0) {
throw "Missing required environment variables: $($missing -join ', '). Load deploy/.env values before running this script."
}
if ([string]::IsNullOrWhiteSpace($env:IMAGE_TAG)) {
$env:IMAGE_TAG = "latest"
}
$env:REGISTRY_ENDPOINT = "git.recroomarchive.org"
$env:REGISTRY_TOKEN | docker login $env:REGISTRY_ENDPOINT -u $env:REGISTRY_USERNAME --password-stdin
aspire do push `
--apphost $AppHostPath `
--environment $Environment `
--output-path $OutputPath

75
deploy/push-images.sh Normal file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env sh
set -eu
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
ENVIRONMENT=${ENVIRONMENT:-Production}
APPHOST_PATH=${APPHOST_PATH:-"$script_dir/../src/AppHost/AppHost.csproj"}
OUTPUT_PATH=${OUTPUT_PATH:-"$script_dir/aspire-output"}
env_file="$script_dir/.env"
if [ -f "$env_file" ]; then
while IFS= read -r line || [ -n "$line" ]; do
line=$(printf '%s' "$line" | tr -d '\r')
case "$line" in
""|\#*) continue ;;
esac
key=${line%%=*}
value=${line#*=}
case "$key" in
[A-Za-z_][A-Za-z0-9_]*) ;;
*) continue ;;
esac
eval "existing=\${$key-}"
if [ -n "$key" ] && [ -z "$existing" ]; then
export "$key=$value"
fi
done < "$env_file"
fi
missing=""
for name in REGISTRY_USERNAME REGISTRY_TOKEN REGISTRY_REPOSITORY; do
eval "value=\${$name-}"
if [ -z "$value" ]; then
if [ -z "$missing" ]; then
missing=$name
else
missing="$missing, $name"
fi
fi
done
if [ -n "$missing" ]; then
echo "Missing required environment variables: $missing. Load deploy/.env values before running this script." >&2
exit 1
fi
IMAGE_TAG=${IMAGE_TAG:-latest}
REGISTRY_ENDPOINT=git.recroomarchive.org
export IMAGE_TAG REGISTRY_ENDPOINT
if [ -n "${ASPIRE_CLI-}" ]; then
aspire_cli=$ASPIRE_CLI
elif command -v aspire >/dev/null 2>&1; then
aspire_cli=aspire
elif [ -x "$HOME/.aspire/bin/aspire" ]; then
aspire_cli="$HOME/.aspire/bin/aspire"
elif [ -x "$HOME/.dotnet/tools/aspire" ]; then
aspire_cli="$HOME/.dotnet/tools/aspire"
else
echo "Aspire CLI was not found in this shell." >&2
echo "Install it in WSL with: curl -sSL https://aspire.dev/install.sh | bash" >&2
echo "Or install it as a .NET tool with: dotnet tool install -g Aspire.Cli" >&2
echo "If it is installed somewhere else, rerun with: ASPIRE_CLI=/path/to/aspire sh ./deploy/push-images.sh" >&2
exit 1
fi
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY_ENDPOINT" -u "$REGISTRY_USERNAME" --password-stdin
"$aspire_cli" do push \
--apphost "$APPHOST_PATH" \
--environment "$ENVIRONMENT" \
--output-path "$OUTPUT_PATH"