Skip to content

Commit 7e5d604

Browse files
committed
feat: Add Docker support for streamlined deployment and serve frontend assets
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 0073575 commit 7e5d604

4 files changed

Lines changed: 94 additions & 12 deletions

File tree

.dockerignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.git
2+
.gitignore
3+
node_modules
4+
frontend/node_modules
5+
backend/venv
6+
.venv
7+
__pycache__
8+
playwright-report
9+
test-results
10+
*.log

Dockerfile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# syntax=docker/dockerfile:1
2+
3+
# =========================
4+
# Frontend build stage
5+
# =========================
6+
FROM node:20-slim AS frontend-builder
7+
WORKDIR /app/frontend
8+
9+
COPY frontend/package*.json ./
10+
RUN npm ci
11+
12+
COPY frontend/ ./
13+
RUN npm run build
14+
15+
# =========================
16+
# Backend runtime stage
17+
# =========================
18+
FROM python:3.11-slim AS runtime
19+
ENV PYTHONUNBUFFERED=1
20+
WORKDIR /app/backend
21+
22+
COPY backend/requirements.txt ./
23+
RUN pip install --no-cache-dir -r requirements.txt
24+
25+
COPY backend/ ./
26+
COPY --from=frontend-builder /app/frontend/dist /app/frontend-dist
27+
28+
ENV FRONTEND_DIST=/app/frontend-dist
29+
EXPOSE 5001
30+
31+
CMD ["hypercorn", "--bind", "0.0.0.0:5001", "app:app"]

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ Use the “Full Stack: Backend + Frontend” launch config to start backend + fr
5757
- Tasks tab should show three sample tasks (seeded by `TaskService.initialize_sample_data()`)
5858
- Create a task, mark it complete, delete it—confirm state updates instantly
5959

60+
## Docker (one command delivery)
61+
62+
Need everything in a single container? The repo now includes a multi-stage `Dockerfile` that builds the Vite frontend, copies the static assets next to the Quart app, and serves everything through Hypercorn on port `5001`.
63+
64+
```bash
65+
docker build -t quart-react-demo .
66+
docker run --rm -p 5001:5001 quart-react-demo
67+
```
68+
69+
- The container exposes only the backend port; the frontend is served by Quart from the built assets, so open `http://localhost:5001`.
70+
- Set `-e FRONTEND_DIST=/custom/path` if you mount a different build output at runtime.
71+
- Hot reloading is not part of the container flow—use the regular dev servers for iterative work and Docker for demos or deployment.
72+
6073
## Using the app
6174
- **Dashboard tab:** Streams `{"time","date","timestamp"}` via EventSource; connection errors show inline.
6275
- **Tasks tab:** Uses FluentUI `DataGrid` + dialogs; `frontend/src/features/tasks/TaskList.jsx` keeps calculations (`getTaskStats`) separate from actions (API calls).

backend/app.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,20 @@
1515
Key insight: Define operation with type hints, get REST + MCP + validation automatically!
1616
"""
1717

18-
from quart import Quart, jsonify, request
19-
from quart_cors import cors
20-
from datetime import datetime
2118
import asyncio
2219
import json
23-
from pydantic import ValidationError
24-
25-
# Import Pydantic models and service
26-
from tasks import (
27-
Task, TaskCreate, TaskUpdate, TaskFilter, TaskStats,
28-
TaskService
29-
)
20+
import os
21+
from datetime import datetime
22+
from pathlib import Path
3023

3124
# Import unified operation system
32-
from api_decorators import operation, get_mcp_tools, get_operation
33-
25+
from api_decorators import get_mcp_tools, get_operation, operation
26+
from pydantic import ValidationError
27+
from quart import Quart, jsonify, request, send_from_directory
28+
from quart_cors import cors
29+
# Import Pydantic models and service
30+
from tasks import (Task, TaskCreate, TaskFilter, TaskService, TaskStats,
31+
TaskUpdate)
3432

3533
# ============================================================================
3634
# APPLICATION SETUP
@@ -282,6 +280,28 @@ async def generate_time_events():
282280
}
283281

284282

283+
frontend_dist_path = Path(
284+
os.getenv(
285+
"FRONTEND_DIST",
286+
Path(__file__).resolve().parents[1] / "frontend" / "dist"
287+
)
288+
)
289+
290+
if frontend_dist_path.exists() and (frontend_dist_path / "index.html").exists():
291+
@app.route("/", defaults={"path": ""})
292+
@app.route("/<path:path>")
293+
async def serve_frontend(path: str):
294+
"""Serve the built frontend assets when available."""
295+
if path.startswith("api") or path.startswith("mcp"):
296+
return jsonify({"error": "Not Found"}), 404
297+
298+
candidate = frontend_dist_path / path
299+
if path and candidate.exists() and candidate.is_file():
300+
return await send_from_directory(frontend_dist_path, path)
301+
302+
return await send_from_directory(frontend_dist_path, "index.html")
303+
304+
285305
# ============================================================================
286306
# MCP JSON-RPC ENDPOINT
287307
# ============================================================================
@@ -312,6 +332,14 @@ async def mcp_json_rpc():
312332
params = data.get("params", {})
313333
request_id = data.get("id")
314334

335+
# Notifications (no response required but we acknowledge for logs)
336+
if method == "notifications/initialized":
337+
return jsonify({
338+
"jsonrpc": "2.0",
339+
"result": None,
340+
"id": request_id
341+
}), 200
342+
315343
# Initialize
316344
if method == "initialize":
317345
return jsonify({

0 commit comments

Comments
 (0)