Compare commits
10 Commits
e14e552388
...
fc2e000093
| Author | SHA1 | Date | |
|---|---|---|---|
| fc2e000093 | |||
| 256ecee7bf | |||
| d8a45c2acf | |||
| c95860630e | |||
| 6da566d96b | |||
| 23232f45a4 | |||
| b7f9be8fd9 | |||
| 5c1ea12fc2 | |||
| 256b8e0ce7 | |||
| de82bc0d61 |
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# .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
|
||||||
@ -37,7 +37,10 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
if (!app.Environment.IsProduction())
|
||||||
|
{
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
}
|
||||||
|
|
||||||
app.UseCors("AllowAll");
|
app.UseCors("AllowAll");
|
||||||
|
|
||||||
|
|||||||
7
BudgetApp.Web/.dockerignore
Normal file
7
BudgetApp.Web/.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
coverage/
|
||||||
|
*.local
|
||||||
|
.eslintcache
|
||||||
|
*.tsbuildinfo
|
||||||
13
BudgetApp.Web/Dockerfile.web
Normal file
13
BudgetApp.Web/Dockerfile.web
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 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
|
||||||
19
BudgetApp.Web/nginx.web.conf
Normal file
19
BudgetApp.Web/nginx.web.conf
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1218
BudgetApp.Web/package-lock.json
generated
1218
BudgetApp.Web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
import type { ListResponse, YearSummaryDto } from '@/models/TransactionModels'
|
import type { ListResponse, YearSummaryDto } from '@/models/TransactionModels'
|
||||||
|
|
||||||
const BASE_URL = 'https://localhost:7244/api/Transactions'
|
const BASE_URL = '/api/Transactions'
|
||||||
|
|
||||||
export const getTransactions = async (
|
export const getTransactions = async (
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
|
|||||||
26
Dockerfile.api
Normal file
26
Dockerfile.api
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# 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"]
|
||||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
495
docs/superpowers/plans/2026-03-21-docker-deployment.md
Normal file
495
docs/superpowers/plans/2026-03-21-docker-deployment.md
Normal 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.
|
||||||
141
docs/superpowers/specs/2026-03-21-docker-deployment-design.md
Normal file
141
docs/superpowers/specs/2026-03-21-docker-deployment-design.md
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# 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. 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)
|
||||||
|
- 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:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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 preserved — controllers use `[Route("api/[controller]")]`)
|
||||||
|
- **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)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
|
ConnectionStrings__MainDatabase: ${ConnectionStrings__MainDatabase}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build & Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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.
|
||||||
Loading…
Reference in New Issue
Block a user