Skip to main content

Sprint — Docker + Nginx for MCP & Copilot stack

Goal: Containerize dwd-mcp-fastmcp and dwd-copilot-server on msi-laptop-3, wire them to existing dwd-api-fastapi, expose via Nginx on port 80 (443 later), and verify server-to-server URLs use internal paths (not the public LAN IP).

Status: Phase 1 complete (local builds verified). Phase 2+ pending on server.

Repos:

RepoRoleTech
dwd-api-fastapiForecast API + MAF /agentPython / Gunicorn
dwd-mcp-fastmcpMCP tools for agentPython / Gunicorn / FastMCP
dwd-copilot-serverCopilotKit runtime → proxies to /agentNode 22 / Next.js

Architecture (target)

Browser (localhost:3000 or LAN)


Nginx :443 (www.hydrotrek.org → 74.83.141.228)
├── /gqc/api/fastapi/ → 127.0.0.1:8001 → dwd-api-fastapi :8080
├── /gqc/api/mcp → 127.0.0.1:8002 → dwd-mcp-fastmcp :8081 (upstream /mcp)
└── /gqc/api/copilotkit → 127.0.0.1:8003 → dwd-copilot-server :8082

/ and non-client paths → 404

Docker network (server-to-server — NOT via LAN IP):
dwd-copilot-server ──AGUI_AGENT_URL──► dwd-api-fastapi:8080/agent
dwd-api-fastapi ──MCP_SERVER_URL──► dwd-mcp-fastmcp:8081/mcp
dwd-mcp-fastmcp ──DWD_API_BASE_URL──► ASP.NET (external; see below)

Port allocation

Align container PORT with your request (8080 → 8081 → 8082). Host bindings stay on 127.0.0.1 only (existing convention).

ServiceContainer portHost bind (127.0.0.1)Nginx path(s)
dwd-api-fastapi80808001/gqc/api/fastapi/
dwd-mcp-fastmcp80818002/gqc/api/mcp (→ upstream /mcp)
dwd-copilot-server80828003/gqc/api/copilotkit

Record in README.md deployed APIs table when live.


URL / cross-reference matrix

Server-to-server (use Docker Compose service names)

Do not use http://74.83.141.228/... or the public hostname between containers (hairpin, unnecessary exposure).

FromEnv varTarget value (container network)
dwd-copilot-serverAGUI_AGENT_URLhttp://dwd-api-fastapi:8080/agent
dwd-api-fastapiMCP_SERVER_URLhttp://dwd-mcp-fastmcp:8081/mcp
dwd-api-fastapiMCP_SERVER_NAMEHydroTrek MCP (unchanged)

Use a shared Compose network (e.g. dwd-stack) so hostnames resolve.

Note: 127.0.0.1 inside a container is that container only. For server-to-server, use service names above, not localhost.

Nginx → upstream (host localhost)

Nginx proxy_passUpstream
FastAPI routeshttp://127.0.0.1:8001
/mcphttp://127.0.0.1:8002
/api/copilotkithttp://127.0.0.1:8003

Browser / Vite frontend (developer workstation)

Client callsURL (LAN test)
Forecast / RESThttps://www.hydrotrek.org/gqc/api/fastapi/graph-data
CopilotKithttps://www.hydrotrek.org/gqc/api/copilotkit
Agent (direct; usually not browser)Copilot server proxies to FastAPI

Frontend env (e.g. VITE_*) should point at https://www.hydrotrek.org/gqc/api/..., not raw container ports or 74.83.141.228 (cert hostname mismatch on IP).

ASP.NET dwd-api-aspnet (external to this stack)

Runs on developer Windows PC (e.g. :7033). Server containers cannot use localhost:7033 — use Windows LAN IP (see 13-cors-and-browser-api.md).

ServiceEnv varDev value example
dwd-api-fastapiDWD_API_BASE_URLhttps://192.168.0.XX:7033/api/
dwd-mcp-fastmcpDWD_API_BASE_URLhttps://192.168.0.XX:7033 (no /api/ suffix)

Phase 0 — Prerequisites

  • dwd-api-fastapi running: curl http://127.0.0.1:8001/health
  • Nginx LAN config working for FastAPI (11-nginx-lan-setup.md)
  • Docker + compose on server (08-docker-install-and-users.md)
  • Git deploy keys for all three repos (07-code-management-and-git.md)
  • OPENAI_API_KEY in /etc/dwd-api-fastapi/dwd-api-fastapi.env (required for /agent)
  • Clones under /opt/apis/ for all three repos
  • Docker files committed & pushed for dwd-api-fastapi (already started); same pattern for two new repos

Phase 1 — Docker files in each repo

1A — dwd-mcp-fastmcp

  • Dockerfile (Python 3.11-slim, gunicorn + uvicorn.workers.UvicornWorker, PORT=8081)
  • .dockerignore
  • GET /health verified in image
  • Local build test: docker build -t dwd-mcp-fastmcp .
  • Compose / network config → hydrotrek-dwd-suite/docker/ (not in submodule)

Reference: templates/docker/dwd-mcp-fastmcp/ (Dockerfile only)

1B — dwd-copilot-server

  • Dockerfile (Node 22.22.2, npm run build:standalone, PORT=8082)
  • .dockerignore
  • Production start command: node server.js from standalone output
  • Local build test (Docker Desktop on workstation)
  • Compose / network config → hydrotrek-dwd-suite/docker/ (not in submodule)

Reference: repos/dwd-copilot-server/README.md, templates/docker/dwd-copilot-server/ (Dockerfile only)

1C — Stack compose in hydrotrek-dwd-suite

  • Single stack compose in repos/hydrotrek-dwd-suite/docker/
  • docker-compose.yml (local dev — submodule build contexts)
  • docker-compose.prod.yml/etc/dwd-stack/docker-compose.yml on server
  • dwd-mcp-fastmcp added as submodule in suite
  • Cross-service env wired in compose (MCP_SERVER_URL, AGUI_AGENT_URL)
  • Nginx template: [docker/nginx/dwd-gqc-lan.conf]

Decision: Cross-repo Docker orchestration lives in hydrotrek-dwd-suite only. Submodule repos keep Dockerfile + .dockerignore for builds. Server production compose path: /etc/dwd-stack/docker-compose.yml.


Phase 2 — Server layout (/etc/)

PathPurpose
/etc/dwd-stack/docker-compose.ymlfrom hydrotrek-dwd-suite/docker/docker-compose.prod.yml
/etc/dwd-api-fastapi/dwd-api-fastapi.envOPENAI_API_KEY, DWD_API_BASE_URL, …
/etc/dwd-mcp-fastmcp/dwd-mcp-fastmcp.envDWD_API_BASE_URL, …
/etc/dwd-copilot-server/dwd-copilot-server.envoptional CORS extras
/etc/systemd/system/dwd-stack.serviceoptional boot unit for full stack

Env templates

  • Create dirs (750, root:apisvc)
  • Env files mode 640
  • Copy stack compose to /etc/dwd-stack/docker-compose.yml
  • MCP_SERVER_URL and AGUI_AGENT_URL set in compose (not env files)

Phase 3 — Deploy containers

Order: stack compose up (MCP + FastAPI + Copilot together), or bring up MCP first then docker compose up -d for the rest.

cd /etc/dwd-stack
sudo docker compose up -d --build
curl -s http://127.0.0.1:8002/health # mcp
curl -s http://127.0.0.1:8001/health # fastapi + mcp_connected
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8003/ # copilot

3A — MCP

  • Container healthy
  • 127.0.0.1:8002 only (not 0.0.0.0:8002)

3B — FastAPI (with MCP)

  • MCP_SERVER_URL in stack compose (Docker DNS URL)
  • curl -s http://127.0.0.1:8001/healthmcp_configured: true, mcp_connected: true

3C — Copilot server

  • AGUI_AGENT_URL in stack compose
  • Build (may take longer — Next standalone)
  • curl -s http://127.0.0.1:8003/ or hit /api/copilotkit OPTIONS/POST smoke test

Phase 4 — Nginx (single LAN IP, port 80)

Replace or extend templates/nginx/dwd-api-fastapi-lan.conf with path-based routing:

LocationUpstream
/gqc/api/fastapi/127.0.0.1:8001
/gqc/api/copilotkit127.0.0.1:8003
/gqc/api/mcp127.0.0.1:8002
/404
  • WebSocket / long timeout headers where needed (Copilot + agent)
  • proxy_http_version 1.1 for /mcp and /api/copilotkit
  • sudo nginx -t && sudo systemctl reload nginx
  • Remove or narrow default_server if only one combined config

HTTPS (later): duplicate blocks on 443 with same location paths.

Template: repos/hydrotrek-dwd-suite/docker/nginx/dwd-gqc-lan.conf (mirror: templates/nginx/dwd-gqc-lan.conf)


Phase 5 — Integration tests

Run from server first, then browser.

Health chain

curl -s http://127.0.0.1/health                    # via nginx → fastapi
curl -s http://127.0.0.1:8002/health # mcp direct
curl -s http://127.0.0.1:8001/health | jq .agent # mcp_connected
  • FastAPI /healthagent.enabled: true (with OPENAI_API_KEY)
  • FastAPI /healthmcp_configured / mcp_connected true
  • MCP /health returns dwd_api_base_url

Through Nginx (LAN IP)

curl -s https://www.hydrotrek.org/gqc/api/fastapi/health
curl -s -X OPTIONS https://www.hydrotrek.org/gqc/api/copilotkit -H "Origin: http://localhost:3000"
curl -s -o /dev/null -w "%{http_code}\n" https://www.hydrotrek.org/

End-to-end (manual)

  • Vite :3000https://www.hydrotrek.org/gqc/api/fastapi/graph-data
  • Vite CopilotKit → https://www.hydrotrek.org/gqc/api/copilotkit → agent responds
  • Agent tool call hits MCP (check MCP container logs for tool execution)
  • No server logs showing requests to 74.83.141.228 between containers (only Docker DNS)

Log commands

sudo docker compose -f /etc/dwd-stack/docker-compose.yml logs -f --tail=30
sudo docker compose -f /etc/dwd-stack/docker-compose.yml logs -f dwd-mcp-fastmcp --tail=30
sudo tail -f /var/log/nginx/error.log

Phase 6 — Documentation & git

  • Update README.md deployed APIs table (ports 8002, 8003)
  • Add docs/15-deploy-dwd-stack.md (copy-paste runbook, post-sprint)
  • Add 15-api-reverse-proxy-base-paths-todo.md (ROOT_PATH, Swagger, Copilot preflight)
  • Push Docker files: submodule Dockerfiles + hydrotrek-dwd-suite/docker/ stack
  • Record env var final values (redact secrets)

Phase 7 — Deferred (server room)

  • Subdomain routing instead of path-based / default_server
  • SSL on 443
  • UFW: 80/443 on Ethernet only (09-wifi-and-ssh-network-setup.md)
  • Production CORS origins (hydrotrek.com, etc.)

Risks / notes

TopicNote
Next.js image sizeCopilot standalone build; multi-stage Dockerfile
MCP + FastAPI startup orderFastAPI discovers MCP at lifespan startup — restart FastAPI after MCP is up
localhost vs service namesBrowser uses server IP; containers use Compose DNS
ASP.NET on WindowsStill needs LAN IP from server; not part of this stack
OpenAI keyRequired for /agent; without it copilot connects but agent disabled
Single vs split composeShared network required for http://dwd-api-fastapi:8080 URLs

Sprint checklist (quick)

[ ] Phase 0 — prerequisites
[x] Phase 1 — Dockerfiles in both repos (+ push)
[ ] Phase 2 — /etc/ env + compose
[ ] Phase 3 — MCP up → FastAPI recreate → Copilot up
[ ] Phase 4 — Nginx path routing
[ ] Phase 5 — health + browser + MCP tool smoke test
[ ] Phase 6 — docs + README ports