BudgetApp/docs/superpowers/plans/2026-03-21-docker-deployment.md
Martin Svrcina 5c1ea12fc2 Add Docker deployment implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 02:43:21 +01:00

13 KiB

Docker Deployment Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Deploy BudgetApp to a home server via two Docker containers (API + frontend) behind Nginx Proxy Manager.

Architecture: A budgetapp-api container runs the .NET 8 ASP.NET Core API on port 8080. A budgetapp-web container runs nginx serving the Vue 3 static build on host port 3100, and internally proxies /api/ requests to the API container over a shared Docker bridge network. Nginx Proxy Manager on the host proxies inbound traffic to port 3100.

Tech Stack: .NET 8 / ASP.NET Core, Vue 3 / Vite / TypeScript, nginx, Docker / docker-compose


File Map

File Action Responsibility
BudgetApp.Web/src/backend/ApiConnector.ts Modify Remove hardcoded localhost URL, use relative path
BudgetApp.Api/Program.cs Modify Guard UseHttpsRedirection() for non-Production only
.dockerignore Create Exclude bin/, obj/, docs/, Web artifacts from API build context
BudgetApp.Web/.dockerignore Create Exclude node_modules/, dist/ from web build context
Dockerfile.api Create Multi-stage .NET 8 build → runtime image
BudgetApp.Web/Dockerfile.web Create Multi-stage Node/Vite build → nginx runtime image
BudgetApp.Web/nginx.web.conf Create nginx config: serve SPA static files + proxy /api/ to API container
docker-compose.yml Create Orchestrate both containers, network, port mappings, env vars
.env Create Gitignored secrets file — DB connection string

Task 1: Fix Frontend API URL

Files:

  • Modify: BudgetApp.Web/src/backend/ApiConnector.ts:3

The hardcoded https://localhost:7244/api/Transactions is baked into the Vite bundle at build time. The browser would call localhost:7244 instead of going through nginx. Change to a relative path so all calls go through the nginx proxy.

  • Step 1: Update BASE_URL to a relative path

In BudgetApp.Web/src/backend/ApiConnector.ts, change line 3:

const BASE_URL = '/api/Transactions'
  • Step 2: Verify type-check passes
cd BudgetApp.Web && npm run type-check

Expected: no errors.

  • Step 3: Verify dev build compiles
npm run build-only

Expected: dist/ created with no errors.

  • Step 4: Commit
cd ..
git add BudgetApp.Web/src/backend/ApiConnector.ts
git commit -m "fix: use relative API URL for Docker compatibility"

Task 2: Guard HTTPS Redirection

Files:

  • Modify: BudgetApp.Api/Program.cs:40

In Docker, TLS is terminated at Nginx Proxy Manager. The API container receives plain HTTP on port 8080. With UseHttpsRedirection() active, the nginx proxy call would receive a 307 redirect and fail. Wrap it to only apply outside Production.

  • Step 1: Wrap UseHttpsRedirection() in a development-only guard

In BudgetApp.Api/Program.cs, replace line 40:

// Before:
app.UseHttpsRedirection();

// After:
if (!app.Environment.IsProduction())
{
    app.UseHttpsRedirection();
}
  • Step 2: Verify the project builds
dotnet build BudgetApp.Api/BudgetApp.Api.csproj

Expected: Build succeeded.

  • Step 3: Commit
git add BudgetApp.Api/Program.cs
git commit -m "fix: disable HTTPS redirection in Production for Docker deployment"

Task 3: Create .dockerignore Files

Files:

  • Create: .dockerignore
  • Create: BudgetApp.Web/.dockerignore

Without .dockerignore, Docker sends the entire working directory as the build context — including node_modules/ (potentially hundreds of MB), bin/, obj/, and dist/. This slows builds and pollutes layers. The API and Web builds each need their own .dockerignore scoped to their respective build contexts.

  • Step 1: Create root .dockerignore for the API build context

Create .dockerignore at solution root:

# .NET build outputs
**/bin/
**/obj/

# Frontend (not needed for API build)
BudgetApp.Web/node_modules/
BudgetApp.Web/dist/

# Docs and editor files
docs/
.vs/
.vscode/
.idea/
*.user

# Git
.git/
.gitignore
  • Step 2: Create BudgetApp.Web/.dockerignore for the web build context

Create BudgetApp.Web/.dockerignore:

node_modules/
dist/
dist-ssr/
coverage/
*.local
.eslintcache
*.tsbuildinfo
  • Step 3: Commit
git add .dockerignore BudgetApp.Web/.dockerignore
git commit -m "chore: add .dockerignore files for API and web build contexts"

Task 4: Create Dockerfile.api

Files:

  • Create: Dockerfile.api

Multi-stage build: SDK image restores and publishes the app; the smaller aspnet runtime image runs it. The build context must be the solution root because BudgetApp.Api references sibling projects (BudgetApp.Storage, BudgetApp.Services, BudgetApp.PublicModels, BudgetApp.Enums).

  • Step 1: Create Dockerfile.api at solution root
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy project files and restore (layer-cached separately for faster rebuilds)
COPY BudgetApp.Api/BudgetApp.Api.csproj BudgetApp.Api/
COPY BudgetApp.Storage/BudgetApp.Storage.csproj BudgetApp.Storage/
COPY BudgetApp.Services/BudgetApp.Services.csproj BudgetApp.Services/
COPY BudgetApp.PublicModels/BudgetApp.PublicModels.csproj BudgetApp.PublicModels/
COPY BudgetApp.Enums/BudgetApp.Enums.csproj BudgetApp.Enums/
RUN dotnet restore BudgetApp.Api/BudgetApp.Api.csproj

# Copy source and publish
COPY BudgetApp.Api/ BudgetApp.Api/
COPY BudgetApp.Storage/ BudgetApp.Storage/
COPY BudgetApp.Services/ BudgetApp.Services/
COPY BudgetApp.PublicModels/ BudgetApp.PublicModels/
COPY BudgetApp.Enums/ BudgetApp.Enums/
RUN dotnet publish BudgetApp.Api/BudgetApp.Api.csproj -c Release -o /app/publish --no-restore

# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "BudgetApp.Api.dll"]
  • Step 2: Build the image to verify it compiles

Run from the solution root:

docker build -f Dockerfile.api -t budgetapp-api:test .

Expected: Successfully tagged budgetapp-api:test with no errors. The build will pull the SDK image on first run (~200 MB).

  • Step 3: Verify the container starts and /ping responds
docker run --rm -d \
  -p 8080:8080 \
  -e ASPNETCORE_ENVIRONMENT=Production \
  -e "ConnectionStrings__MainDatabase=Server=seth.local;User Id=budget;Password=<your-password>;Database=budget" \
  --name budgetapp-api-test \
  budgetapp-api:test

# Wait a few seconds, then test
sleep 3
curl http://localhost:8080/ping

Expected: {"ok":true,"time":"..."} — confirms the API starts and connects to MySQL.

  • Step 4: Stop and remove the test container
docker stop budgetapp-api-test
docker rmi budgetapp-api:test
  • Step 5: Commit
git add Dockerfile.api
git commit -m "feat: add multi-stage Dockerfile for .NET API"

Task 5: Create nginx.web.conf and Dockerfile.web

Files:

  • Create: BudgetApp.Web/nginx.web.conf
  • Create: BudgetApp.Web/Dockerfile.web

The nginx config serves the Vue SPA (with try_files for Vue Router) and proxies /api/ requests to the API container. Both controllers use [Route("api/[controller]")], so the /api/ prefix must be preserved — use proxy_pass http://budgetapp-api:8080 with no trailing slash.

  • Step 1: Create BudgetApp.Web/nginx.web.conf
server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # Proxy /api/ requests to the API container — prefix preserved
    location /api/ {
        proxy_pass http://budgetapp-api: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;
    }

    # Serve Vue SPA — fallback to index.html for client-side routing
    location / {
        try_files $uri $uri/ /index.html;
    }
}
  • Step 2: Create BudgetApp.Web/Dockerfile.web
# Stage 1: Build Vue app
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build-only

# Stage 2: Serve with nginx
FROM nginx:alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.web.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
  • Step 3: Build the web image to verify it compiles

Run from the solution root:

docker build -f BudgetApp.Web/Dockerfile.web -t budgetapp-web:test ./BudgetApp.Web

Expected: Successfully tagged budgetapp-web:test.

  • Step 4: Verify the container serves the app
docker run --rm -d -p 3100:80 --name budgetapp-web-test budgetapp-web:test
sleep 2
curl -I http://localhost:3100

Expected: HTTP/1.1 200 OK with Content-Type: text/html.

  • Step 5: Stop and remove the test container
docker stop budgetapp-web-test
docker rmi budgetapp-web:test
  • Step 6: Commit
git add BudgetApp.Web/nginx.web.conf BudgetApp.Web/Dockerfile.web
git commit -m "feat: add nginx config and Dockerfile for Vue frontend"

Task 6: Create docker-compose.yml and .env

Files:

  • Create: docker-compose.yml
  • Create: .env

The compose file wires both containers together on a shared bridge network and injects the DB connection string from .env. The .env file is already gitignored by the root .gitignore.

  • Step 1: Create .env with the DB connection string

Create .env at solution root (this file is gitignored — never commit it):

ConnectionStrings__MainDatabase=Server=seth.local;User Id=budget;Password=<your-password>;Database=budget
  • Step 2: Create docker-compose.yml at solution root
services:
  budgetapp-api:
    build:
      context: .
      dockerfile: Dockerfile.api
    ports:
      - "8080:8080"
    environment:
      ASPNETCORE_ENVIRONMENT: Production
      ConnectionStrings__MainDatabase: ${ConnectionStrings__MainDatabase}
    networks:
      - budgetapp
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:8080/ping || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s
    restart: unless-stopped

  budgetapp-web:
    build:
      context: ./BudgetApp.Web
      dockerfile: Dockerfile.web
    ports:
      - "3100:80"
    networks:
      - budgetapp
    depends_on:
      - budgetapp-api
    restart: unless-stopped

networks:
  budgetapp:
    driver: bridge
  • Step 3: Validate the compose file
docker-compose config

Expected: Resolved config printed with no errors.

  • Step 4: Commit (.env is gitignored and will not be committed)
git add docker-compose.yml
git commit -m "feat: add docker-compose orchestration for full stack deployment"

Task 7: Full Stack Integration Test

Verify both containers start together, the API is reachable internally, and the frontend serves the app with working API calls.

  • Step 1: Build and start all containers
docker-compose up -d --build

Expected: Both containers start. May take a few minutes on first build.

  • Step 2: Check container status
docker-compose ps

Expected: Both budgetapp-api and budgetapp-web show as running.

  • Step 3: Check API health via host port
curl http://localhost:8080/ping

Expected: {"ok":true,"time":"..."}.

  • Step 4: Check frontend loads via host port
curl -I http://localhost:3100

Expected: HTTP/1.1 200 OK with Content-Type: text/html.

  • Step 5: Check API is reachable through frontend's nginx proxy
curl http://localhost:3100/api/Transactions/getTransactions

Expected: JSON response from the API (not a 502 Bad Gateway).

  • Step 6: Check logs if anything fails
docker-compose logs budgetapp-api
docker-compose logs budgetapp-web
  • Step 7: Stop containers
docker-compose down

Task 8: Push to Remote

  • Step 1: Verify nothing sensitive was staged
git status

Confirm .env is NOT listed. Only committed files should appear.

  • Step 2: Push to remote
git push

NPM Setup (Manual — on the server)

After docker-compose up -d --build runs on the server:

  1. Open Nginx Proxy Manager web UI
  2. Add a new Proxy Host:
    • Domain Names: budget.yourdomain.com (or your local DNS name)
    • Scheme: http
    • Forward Hostname / IP: your server's host IP
    • Forward Port: 3100
    • Enable Websockets Support (optional, for Vite HMR if ever needed)
  3. Add SSL certificate via Let's Encrypt if desired

No second proxy host needed — /api/ traffic is handled internally by the web container's nginx.