Wauplin HF Staff commited on
Commit
51e559a
·
verified ·
1 Parent(s): 0b85fad

Add docker + Space deployment

Browse files
Dockerfile ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =======================
2
+ # 1️⃣ Frontend build stage
3
+ # =======================
4
+ FROM node:22-slim AS frontend-builder
5
+
6
+ # Install pnpm globally
7
+ RUN corepack enable && corepack prepare pnpm@latest --activate
8
+
9
+ # Set working directory
10
+ WORKDIR /app/frontend
11
+
12
+ # Copy package files first for caching
13
+ COPY frontend/pnpm-lock.yaml frontend/package.json ./
14
+
15
+ # Install dependencies (prod only for frontend)
16
+ RUN pnpm install --frozen-lockfile
17
+
18
+ # Copy the rest of the frontend source
19
+ COPY frontend/ ./
20
+
21
+ # Build frontend
22
+ ENV VITE_APP_ENV=production
23
+ RUN pnpm build
24
+
25
+
26
+ # =======================
27
+ # 2️⃣ Backend build stage
28
+ # =======================
29
+ FROM python:3.12-slim AS backend-builder
30
+
31
+ # Install uv (fast Python package installer)
32
+ RUN pip install --no-cache-dir uv
33
+
34
+ # Set working directory
35
+ WORKDIR /app
36
+
37
+ # Copy backend requirements and install (no dev deps)
38
+ COPY backend/pyproject.toml backend/uv.lock ./backend/
39
+ RUN cd backend && uv pip install --no-cache-dir --system .
40
+
41
+ # Copy backend source
42
+ COPY backend/ ./backend/
43
+
44
+ # Copy built frontend from stage 1
45
+ COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
46
+
47
+ # =======================
48
+ # 3️⃣ Production runtime
49
+ # =======================
50
+ FROM python:3.12-slim
51
+
52
+ # Create non-root user
53
+ RUN useradd -m appuser
54
+ WORKDIR /app
55
+
56
+ # Copy installed packages and app
57
+ COPY --from=backend-builder /usr/local /usr/local
58
+ COPY --from=backend-builder /app /app
59
+
60
+ # Set frontend path for FastAPI
61
+ ENV FRONTEND_PATH=/app/frontend/dist
62
+
63
+ # Switch to non-root user
64
+ USER appuser
65
+
66
+ # Expose port (adjust if needed)
67
+ EXPOSE 8000
68
+
69
+ # Run FastAPI via uvicorn
70
+ CMD ["uvicorn", "backend.src.app:app", "--host", "0.0.0.0", "--port", "8000"]
README.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: FastAPI + React Template
3
+ emoji: 💻
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 8000
8
+ ---
9
+
10
+ # docker-space-fastapi-react
11
+
12
+ This repo is a template to run a FastAPI server hosting a React+Tailwind app in a Hugging Face Space.
13
+
14
+ Spaces are very well suited to vibe-code demos using Cursor, Lovable, Claude Code, etc. The hardest past is often to correctly configure the tooltip to make it work in the first place. This repo is intended to solve that.
15
+
16
+ ## Getting started
17
+
18
+ The backend (Python + FastAPI) and the frontend (TS + React + Tailwind) are located respectively in the `backend/` and `frontend/` folder.
19
+
20
+ ### Backend
21
+
22
+ To run the backend, you'll need `uv` and Python3.12 installed.
23
+ Install dependencies with:
24
+
25
+ ```bash
26
+ uv venv
27
+ make install
28
+ ```
29
+
30
+ And then run the backend server with:
31
+
32
+ ```bash
33
+ make dev
34
+ ```
35
+
36
+ For more details about backend development, check out [this README](./backend/README.md).
37
+
38
+ ### Frontend
39
+
40
+ To run the frontend, you'll need nodeJS + pnpm installed.
41
+
42
+ Install dependencies with:
43
+
44
+ ```bash
45
+ pnpm install
46
+ ```
47
+
48
+ And then run the frontend app with:
49
+
50
+ ```bash
51
+ pnpm dev
52
+ ```
53
+
54
+ Once both parts are running, go to the frontend URL. You should see a green "Backend Online" message in the top-right corner. Note that this message is only visible in development mode. When running in a Space, it won't be shown to your end users.
55
+
56
+ For more details about frontend development, check out [this README](./frontend/README.md).
57
+
58
+ ## Deploy
59
+
60
+ This repo is meant to be run in a [Docker Space](https://huggingface.co/docs/hub/spaces-sdks-docker).
61
+ Configuration should be automatically done if you clone the on the Hugging Face Hub.
62
+
63
+ If you want to run it on your own, you can use Docker:
64
+
65
+ ```bash
66
+ docker build -t fastapi-react-space .
67
+ docker run -p 8000:8000 fastapi-react-space
68
+ ```
69
+
70
+ Note that when running in Docker, the app runs in production mode without hot-reloading.
backend/Makefile CHANGED
@@ -10,4 +10,7 @@ quality:
10
 
11
  style:
12
  ruff format src
13
- ruff check --fix src
 
 
 
 
10
 
11
  style:
12
  ruff format src
13
+ ruff check --fix src
14
+
15
+ preview:
16
+ FRONTEND_PATH=../frontend/dist/ uv run uvicorn src.app:app
backend/src/app.py CHANGED
@@ -1,5 +1,9 @@
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
 
3
 
4
  app = FastAPI()
5
 
@@ -12,6 +16,18 @@ app.add_middleware(
12
  allow_headers=["*"],
13
  )
14
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  @app.get("/api/health")
17
  async def health():
 
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import FileResponse
4
+ from fastapi.staticfiles import StaticFiles
5
+
6
+ from . import constants
7
 
8
  app = FastAPI()
9
 
 
16
  allow_headers=["*"],
17
  )
18
 
19
+ # Mount static files from dist directory (if configured)
20
+ if constants.SERVE_FRONTEND:
21
+ app.mount(
22
+ "/assets",
23
+ StaticFiles(directory=constants.FRONTEND_ASSETS_PATH),
24
+ name="assets",
25
+ )
26
+
27
+ @app.get("/")
28
+ async def serve_frontend():
29
+ return FileResponse(constants.FRONTEND_INDEX_PATH)
30
+
31
 
32
  @app.get("/api/health")
33
  async def health():
backend/src/constants.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ FRONTEND_PATH = os.getenv("FRONTEND_PATH")
4
+ SERVE_FRONTEND = os.getenv("FRONTEND_PATH") is not None
5
+
6
+ FRONTEND_ASSETS_PATH = os.path.join(FRONTEND_PATH, "assets") if SERVE_FRONTEND else None
7
+ FRONTEND_INDEX_PATH = (
8
+ os.path.join(FRONTEND_PATH, "index.html") if SERVE_FRONTEND else None
9
+ )
10
+
11
+
12
+ if SERVE_FRONTEND and (
13
+ not os.path.exists(FRONTEND_PATH)
14
+ or not os.path.exists(FRONTEND_ASSETS_PATH)
15
+ or not os.path.exists(FRONTEND_INDEX_PATH)
16
+ ):
17
+ raise FileNotFoundError(
18
+ f"FRONTEND_PATH {FRONTEND_PATH} has not been built correctly. Please build the frontend first by running `pnpm build` from the 'frontend/' directory."
19
+ " If you want to run the server in development mode, run `make dev` from the 'backend/' directory and `pnpm dev` from the 'frontend/' directory."
20
+ )
frontend/package.json CHANGED
@@ -3,6 +3,7 @@
3
  "private": true,
4
  "version": "0.0.0",
5
  "type": "module",
 
6
  "scripts": {
7
  "dev": "vite",
8
  "build": "tsc -b && vite build",
 
3
  "private": true,
4
  "version": "0.0.0",
5
  "type": "module",
6
+ "packageManager": "[email protected]",
7
  "scripts": {
8
  "dev": "vite",
9
  "build": "tsc -b && vite build",
frontend/pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff
 
frontend/src/App.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { useState } from "react";
2
  import reactLogo from "./assets/react.svg";
3
- import viteLogo from "/vite.svg";
4
  import { BackendHealthCheck } from "./components/BackendHealthCheck";
5
 
6
  function App() {
@@ -9,7 +9,7 @@ function App() {
9
  return (
10
  <div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
11
  <div className="max-w-4xl mx-auto p-8 text-center">
12
- {/* Backend Status Indicator */}
13
  <BackendHealthCheck />
14
 
15
  <div className="flex justify-center space-x-8 mb-8">
 
1
  import { useState } from "react";
2
  import reactLogo from "./assets/react.svg";
3
+ import viteLogo from "./assets/vite.svg";
4
  import { BackendHealthCheck } from "./components/BackendHealthCheck";
5
 
6
  function App() {
 
9
  return (
10
  <div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
11
  <div className="max-w-4xl mx-auto p-8 text-center">
12
+ {/* Backend Status Indicator - Only shown in development mode */}
13
  <BackendHealthCheck />
14
 
15
  <div className="flex justify-center space-x-8 mb-8">
frontend/{public → src/assets}/vite.svg RENAMED
File without changes
frontend/src/components/BackendHealthCheck.tsx CHANGED
@@ -9,7 +9,14 @@ interface BackendHealthCheckProps {
9
  export function BackendHealthCheck({
10
  checkInterval = APP_CONFIG.HEALTH_CHECK_INTERVAL
11
  }: BackendHealthCheckProps) {
 
 
 
 
 
12
  const [backendStatus, setBackendStatus] = useState<'loading' | 'online' | 'offline'>('loading');
 
 
13
  const healthCheckUrl = `${BACKEND_URL}/api/health`;
14
 
15
  useEffect(() => {
@@ -39,6 +46,17 @@ export function BackendHealthCheck({
39
  return () => clearInterval(interval);
40
  }, [healthCheckUrl, checkInterval]);
41
 
 
 
 
 
 
 
 
 
 
 
 
42
  const getStatusColor = () => {
43
  switch (backendStatus) {
44
  case 'online':
@@ -66,9 +84,62 @@ export function BackendHealthCheck({
66
  };
67
 
68
  return (
69
- <div className="absolute top-4 right-4 flex items-center space-x-2 bg-gray-800 rounded-lg px-3 py-2">
70
- <div className={`w-3 h-3 rounded-full ${getStatusColor()} animate-pulse`}></div>
71
- <span className="text-sm font-medium">{getStatusText()}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  </div>
73
  );
74
  }
 
9
  export function BackendHealthCheck({
10
  checkInterval = APP_CONFIG.HEALTH_CHECK_INTERVAL
11
  }: BackendHealthCheckProps) {
12
+ // Only render in development mode
13
+ if (import.meta.env.PROD) {
14
+ return null;
15
+ }
16
+
17
  const [backendStatus, setBackendStatus] = useState<'loading' | 'online' | 'offline'>('loading');
18
+ const [showTooltip, setShowTooltip] = useState(false);
19
+ const [copied, setCopied] = useState(false);
20
  const healthCheckUrl = `${BACKEND_URL}/api/health`;
21
 
22
  useEffect(() => {
 
46
  return () => clearInterval(interval);
47
  }, [healthCheckUrl, checkInterval]);
48
 
49
+ const handleCopyCode = async () => {
50
+ const codeText = `cd backend/\nmake install\nmake backend`;
51
+ try {
52
+ await navigator.clipboard.writeText(codeText);
53
+ setCopied(true);
54
+ setTimeout(() => setCopied(false), 2000);
55
+ } catch (err) {
56
+ console.error('Failed to copy text: ', err);
57
+ }
58
+ };
59
+
60
  const getStatusColor = () => {
61
  switch (backendStatus) {
62
  case 'online':
 
84
  };
85
 
86
  return (
87
+ <div className="absolute top-4 right-4">
88
+ <div
89
+ className="relative flex items-center space-x-2 bg-gray-800 rounded-lg px-3 py-2 cursor-pointer hover:bg-gray-700 transition-colors"
90
+ onMouseEnter={() => backendStatus === 'offline' && setShowTooltip(true)}
91
+ onMouseLeave={() => setShowTooltip(false)}
92
+ >
93
+ <div className={`w-3 h-3 rounded-full ${getStatusColor()} animate-pulse`}></div>
94
+ <span className="text-sm font-medium">{getStatusText()}</span>
95
+
96
+ {/* Info icon when backend is offline */}
97
+ {backendStatus === 'offline' && (
98
+ <svg className="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
99
+ <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
100
+ </svg>
101
+ )}
102
+
103
+ {/* Tooltip for offline backend */}
104
+ {showTooltip && backendStatus === 'offline' && (
105
+ <div
106
+ className="absolute top-full right-0 mt-2 w-fit bg-gray-900 text-white rounded-lg p-4 shadow-xl border border-gray-700 z-50"
107
+ onMouseEnter={() => setShowTooltip(true)}
108
+ onMouseLeave={() => setShowTooltip(false)}
109
+ >
110
+ <div className="flex items-start space-x-3">
111
+ <div className="flex-1 min-w-0">
112
+ <h3 className="text-sm font-semibold text-400 mb-3 text-left">Start the server with:</h3>
113
+ <div className="relative">
114
+ <code className="text-xs bg-gray-800 text-green-400 px-3 py-2 rounded font-mono block w-fit overflow-x-auto text-left pr-12">
115
+ cd backend/<br/>
116
+ make install<br/>
117
+ make backend
118
+ </code>
119
+ <button
120
+ onClick={handleCopyCode}
121
+ className="absolute top-1 right-1 p-1 bg-gray-700 hover:bg-gray-600 rounded text-gray-300 hover:text-white transition-colors"
122
+ title="Copy to clipboard"
123
+ >
124
+ {copied ? (
125
+ <svg className="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20">
126
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
127
+ </svg>
128
+ ) : (
129
+ <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
130
+ <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
131
+ <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
132
+ </svg>
133
+ )}
134
+ </button>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ {/* Arrow pointing up */}
139
+ <div className="absolute bottom-full right-4 w-0 h-0 border-l-4 border-r-4 border-b-4 border-transparent border-b-gray-900"></div>
140
+ </div>
141
+ )}
142
+ </div>
143
  </div>
144
  );
145
  }