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_URLto 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
.dockerignorefor 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/.dockerignorefor 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.apiat 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
/pingresponds
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
.envwith 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.ymlat 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 (
.envis 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:
- Open Nginx Proxy Manager web UI
- 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)
- Domain Names:
- 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.