Add Docker deployment implementation plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Svrcina 2026-03-21 02:43:21 +01:00
parent 256b8e0ce7
commit 5c1ea12fc2
2 changed files with 497 additions and 2 deletions

View File

@ -0,0 +1,495 @@
# 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=<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**
```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=<your-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.

View File

@ -27,7 +27,7 @@ Deploy BudgetApp to a home server running Nginx Proxy Manager (NPM) and an exist
``` ```
- `budgetapp-web` exposes **port 3100** to host — NPM points here - `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-web` nginx proxies `/api/``http://budgetapp-api:8080` over internal Docker network. The `/api/` prefix is **preserved** (no trailing slash on `proxy_pass`) because both controllers use `[Route("api/[controller]")]`.
- `budgetapp-api` exposes **port 8080** (optional, for direct access/debugging) - `budgetapp-api` exposes **port 8080** (optional, for direct access/debugging)
- Both containers share a Docker bridge network named `budgetapp` - Both containers share a Docker bridge network named `budgetapp`
- API connects to the existing MySQL at `seth.local` - API connects to the existing MySQL at `seth.local`
@ -91,7 +91,7 @@ if (!app.Environment.IsProduction())
- **Build context:** `BudgetApp.Web/` - **Build context:** `BudgetApp.Web/`
- **Build output:** `dist/` copied into nginx image - **Build output:** `dist/` copied into nginx image
- **Runtime port:** 80 (mapped to host port 3100) - **Runtime port:** 80 (mapped to host port 3100)
- **API routing:** nginx proxies `location /api/``http://budgetapp-api:8080/` (prefix stripped) - **API routing:** nginx proxies `location /api/``http://budgetapp-api:8080` (prefix preserved — controllers use `[Route("api/[controller]")]`)
- **SPA routing:** `try_files $uri $uri/ /index.html` for Vue Router - **SPA routing:** `try_files $uri $uri/ /index.html` for Vue Router
- **Depends on:** `budgetapp-api` - **Depends on:** `budgetapp-api`