BudgetApp/docs/superpowers/specs/2026-03-21-docker-deployment-design.md
Martin Svrcina 256b8e0ce7 Update Docker deployment spec with reviewer fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 02:29:03 +01:00

6.8 KiB

Docker Deployment Design

Date: 2026-03-21 Project: BudgetApp

Overview

Deploy BudgetApp to a home server running Nginx Proxy Manager (NPM) and an existing MySQL instance. Two Docker containers are orchestrated via docker-compose: one for the .NET 8 API and one for the Vue 3 frontend served by nginx.

Architecture

┌─────────────────────────────────────────────┐
│              docker-compose.yml              │
│                                              │
│  ┌──────────────────┐  ┌──────────────────┐ │
│  │  budgetapp-web   │  │  budgetapp-api   │ │
│  │  (nginx + Vue)   │──▶  (.NET 8 API)   │ │
│  │  port 3100:80    │  │  port 8080:8080  │ │
│  └──────────────────┘  └──────────────────┘ │
│           internal network: budgetapp        │
└─────────────────────────────────────────────┘
         │                      │
         ▼                      ▼
   NPM proxy host          seth.local MySQL
   (one entry)             (existing)
  • budgetapp-web exposes port 3100 to host — NPM points here
  • budgetapp-web nginx proxies /api/http://budgetapp-api:8080/ over internal Docker network. Note: the /api/ prefix is stripped before forwarding (both location and proxy_pass end with /), so the backend must not expect /api/ in its routes.
  • budgetapp-api exposes port 8080 (optional, for direct access/debugging)
  • Both containers share a Docker bridge network named budgetapp
  • API connects to the existing MySQL at seth.local
  • budgetapp-web has depends_on: budgetapp-api (condition: service_started) — nginx will retry failed upstream connections, so waiting for the process to start is sufficient without requiring a full health-check gate

Files to Create

File Purpose
Dockerfile.api Multi-stage .NET 8 build — solution root as context
Dockerfile.web Multi-stage Node/Vite build + nginx runtime
docker-compose.yml Wires containers, network, port mappings, env vars
nginx.web.conf nginx config — serves static files, proxies /api/ to API container
.env Gitignored secrets file — DB connection string
.dockerignore Excludes node_modules/, dist/, bin/, obj/, docs/ from build context

Files to Modify

File Change
appsettings.json Connection string already cleared; env var override used at runtime
BudgetApp.Api/Program.cs Remove or guard UseHttpsRedirection() — see below
BudgetApp.Web/src/backend/ApiConnector.ts Change hardcoded https://localhost:7244/api/Transactions to relative path /api/Transactions

Required Application Code Changes

1. ApiConnector.ts — Relative API URL

Currently ApiConnector.ts hardcodes https://localhost:7244/api/Transactions. In Docker this would be baked into the Vite bundle and the browser would try to call localhost:7244 instead of going through nginx. Change to a relative path /api/Transactions so all API calls go through nginx's proxy.

Any future frontend calls to other endpoints (e.g. UploadController at api/Upload/csv) must also use relative paths following the same pattern.

2. Program.cs — Disable HTTPS Redirection in Production

app.UseHttpsRedirection() is currently active unconditionally. Inside Docker, TLS is terminated at NPM — the API container receives plain HTTP on port 8080. With HTTPS redirection active, nginx's proxy call to http://budgetapp-api:8080 would receive a 307 redirect and fail. Guard or remove the middleware:

if (!app.Environment.IsProduction())
{
    app.UseHttpsRedirection();
}

Container Details

budgetapp-api

  • Base images: mcr.microsoft.com/dotnet/sdk:8.0 (build) → mcr.microsoft.com/dotnet/aspnet:8.0 (runtime)
  • Build context: Solution root (required — API references sibling projects)
  • Publish target: BudgetApp.Api/BudgetApp.Api.csproj
  • Runtime port: 8080 (the aspnet:8.0 image defaults to port 8080; no ASPNETCORE_URLS override required)
  • Health check: GET /ping (existing endpoint returns {ok: true})
  • Environment variables:
    • ASPNETCORE_ENVIRONMENT=Production
    • ConnectionStrings__MainDatabase — injected from .env via docker-compose
  • Note: Swagger UI is disabled in Production (gated behind IsDevelopment() in Program.cs). Access /swagger by temporarily setting ASPNETCORE_ENVIRONMENT=Development if needed.

budgetapp-web

  • Base images: node:20-alpine (build) → nginx:alpine (runtime)
  • Build context: BudgetApp.Web/
  • Build output: dist/ copied into nginx image
  • Runtime port: 80 (mapped to host port 3100)
  • API routing: nginx proxies location /api/http://budgetapp-api:8080/ (prefix stripped)
  • SPA routing: try_files $uri $uri/ /index.html for Vue Router
  • Depends on: budgetapp-api

Secrets Management

A .env file at the solution root (gitignored) holds the DB connection string using the exact ASP.NET Core env var key format to avoid any mapping indirection in docker-compose:

ConnectionStrings__MainDatabase=Server=seth.local;User Id=budget;Password=<password>;Database=budget

docker-compose passes this directly as the environment variable to the API container. ASP.NET Core's built-in env var configuration provider resolves double-underscores as nested keys automatically.

docker-compose Environment Block (API service)

environment:
  ASPNETCORE_ENVIRONMENT: Production
  ConnectionStrings__MainDatabase: ${ConnectionStrings__MainDatabase}

Build & Deploy

# On the server, in the project directory
docker-compose up -d --build

# View logs
docker-compose logs -f

# Restart after code changes
docker-compose up -d --build

NPM Setup (Manual)

After containers are running, add one proxy host in Nginx Proxy Manager:

  • Domain: budget.yourdomain.com (or local DNS name)
  • Forward hostname/IP: <host-ip>
  • Forward port: 3100

No second proxy host needed — API traffic is routed internally by the web container's nginx.

Notes

  • CORS: The API has an unconditional AllowAll CORS policy. Since all browser traffic reaches the API through nginx's internal proxy, the browser never makes a direct cross-origin request to the API — CORS does not apply to the primary flow. Tightening the CORS policy will have no effect on security in this deployment.
  • .env is already covered by .gitignore — the root .gitignore already includes .env, so no additional entry is needed.