# 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: ```typescript const BASE_URL = '/api/Transactions' ``` - [ ] **Step 2: Verify type-check passes** ```bash cd BudgetApp.Web && npm run type-check ``` Expected: no errors. - [ ] **Step 3: Verify dev build compiles** ```bash npm run build-only ``` Expected: `dist/` created with no errors. - [ ] **Step 4: Commit** ```bash 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: ```csharp // Before: app.UseHttpsRedirection(); // After: if (!app.Environment.IsProduction()) { app.UseHttpsRedirection(); } ``` - [ ] **Step 2: Verify the project builds** ```bash dotnet build BudgetApp.Api/BudgetApp.Api.csproj ``` Expected: `Build succeeded.` - [ ] **Step 3: Commit** ```bash 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** ```bash 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** ```dockerfile # 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: ```bash 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** ```bash docker run --rm -d \ -p 8080:8080 \ -e ASPNETCORE_ENVIRONMENT=Production \ -e "ConnectionStrings__MainDatabase=Server=seth.local;User Id=budget;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** ```bash docker stop budgetapp-api-test docker rmi budgetapp-api:test ``` - [ ] **Step 5: Commit** ```bash 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`** ```nginx 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`** ```dockerfile # 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: ```bash 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** ```bash 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** ```bash docker stop budgetapp-web-test docker rmi budgetapp-web:test ``` - [ ] **Step 6: Commit** ```bash 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=;Database=budget ``` - [ ] **Step 2: Create `docker-compose.yml` at solution root** ```yaml 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** ```bash docker-compose config ``` Expected: Resolved config printed with no errors. - [ ] **Step 4: Commit (`.env` is gitignored and will not be committed)** ```bash 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** ```bash docker-compose up -d --build ``` Expected: Both containers start. May take a few minutes on first build. - [ ] **Step 2: Check container status** ```bash docker-compose ps ``` Expected: Both `budgetapp-api` and `budgetapp-web` show as `running`. - [ ] **Step 3: Check API health via host port** ```bash curl http://localhost:8080/ping ``` Expected: `{"ok":true,"time":"..."}`. - [ ] **Step 4: Check frontend loads via host port** ```bash 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** ```bash 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** ```bash docker-compose logs budgetapp-api docker-compose logs budgetapp-web ``` - [ ] **Step 7: Stop containers** ```bash docker-compose down ``` --- ## Task 8: Push to Remote - [ ] **Step 1: Verify nothing sensitive was staged** ```bash git status ``` Confirm `.env` is NOT listed. Only committed files should appear. - [ ] **Step 2: Push to remote** ```bash 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.