LAB11: Persistency on Kubernetes
LAB 40: Persistency on Kubernetes Workloads
Here’s a minimal file-share app with:
Python FastAPI backend (upload + list + download)
React frontend (simple UI)
Dockerfiles for both
Kubernetes: Namespace, Deployments, Services, Ingress
You can copy-paste and adapt names as you like.
App source code
1. Folder structure
fileshare-app/
├── backend/
│ ├── main.py
│ ├── requirements.txt
│ └── Dockerfile
└── frontend/
├── package.json
├── index.html
├── src/
│ ├── main.jsx
│ └── App.jsx
└── Dockerfile2. Backend (Python / FastAPI)
backend/requirements.txt
fastapi
uvicorn[standard]
python-multipartbackend/main.py
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pathlib import Path
import os
UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/data/uploads"))
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="FileShare API")
# For local dev, allow all. In prod, restrict to specific domains.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
API_PREFIX = "/api"
@app.post(f"{API_PREFIX}/upload")
async def upload_file(file: UploadFile = File(...)):
file_location = UPLOAD_DIR / file.filename
# Overwrite if exists (for demo)
with open(file_location, "wb") as f:
while chunk := await file.read(1024 * 1024):
f.write(chunk)
return {"filename": file.filename}
@app.get(f"{API_PREFIX}/files")
async def list_files():
files = [f.name for f in UPLOAD_DIR.iterdir() if f.is_file()]
return {"files": files}
@app.get(f"{API_PREFIX}/download/{{filename}}")
async def download_file(filename: str):
file_path = UPLOAD_DIR / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(path=file_path, filename=filename)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000)backend/Dockerfile
backend/DockerfileFROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Directory to store uploaded files
RUN mkdir -p /data/uploads
ENV UPLOAD_DIR=/data/uploads
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]3. Frontend (React)
Below assumes Vite + React, but it’s just plain React code, so you can drop it into any React setup.
frontend/package.json (minimal)
frontend/package.json (minimal){
"name": "fileshare-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.0",
"vite": "^5.0.0"
}
}frontend/index.html
frontend/index.html<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>FileShare</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>frontend/src/main.jsx
frontend/src/main.jsximport React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);frontend/src/App.jsx
frontend/src/App.jsximport React, { useState, useEffect } from "react";
import axios from "axios";
// When served behind Ingress, frontend and backend share same host.
// So API is just current origin + /api
const API_BASE_URL = `${window.location.origin}/api`;
export default function App() {
const [selectedFile, setSelectedFile] = useState(null);
const [files, setFiles] = useState([]);
const [uploading, setUploading] = useState(false);
const [message, setMessage] = useState("");
const loadFiles = async () => {
try {
const res = await axios.get(`${API_BASE_URL}/files`);
setFiles(res.data.files || []);
} catch (err) {
console.error(err);
setMessage("Failed to load file list");
}
};
useEffect(() => {
loadFiles();
}, []);
const handleFileChange = (e) => {
setSelectedFile(e.target.files[0] || null);
setMessage("");
};
const handleUpload = async (e) => {
e.preventDefault();
if (!selectedFile) {
setMessage("Please choose a file first");
return;
}
const formData = new FormData();
formData.append("file", selectedFile);
try {
setUploading(true);
setMessage("");
await axios.post(`${API_BASE_URL}/upload`, formData, {
headers: { "Content-Type": "multipart/form-data" }
});
setMessage("Upload successful");
setSelectedFile(null);
e.target.reset();
loadFiles();
} catch (err) {
console.error(err);
setMessage("Upload failed");
} finally {
setUploading(false);
}
};
const handleDownload = (filename) => {
// Simple way: navigate to download URL
window.location.href = `${API_BASE_URL}/download/${encodeURIComponent(
filename
)}`;
};
return (
<div style={{ maxWidth: 600, margin: "2rem auto", fontFamily: "sans-serif" }}>
<h1>FileShare</h1>
<form onSubmit={handleUpload} style={{ marginBottom: "1.5rem" }}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={uploading} style={{ marginLeft: "0.5rem" }}>
{uploading ? "Uploading..." : "Upload"}
</button>
</form>
{message && <p>{message}</p>}
<h2>Files</h2>
{files.length === 0 && <p>No files uploaded yet.</p>}
<ul>
{files.map((name) => (
<li key={name} style={{ marginBottom: "0.25rem" }}>
{name}{" "}
<button onClick={() => handleDownload(name)}>
Download
</button>
</li>
))}
</ul>
</div>
);
}frontend/Dockerfile
frontend/Dockerfile# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Serve static files
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Build and tag: Backend:
docker build -t openlabfree/fileshare-be:v1 .
docker push openlabfree/fileshare-be:v1Frontend:
docker build -t openlabfree/fileshare-fe:v1 .
docker push openlabfree/fileshare-fe:v1---
4. Kubernetes manifests (namespace, deployments, services, ingress)
Save as k8s-fileshare.yaml (for example).
Assumptions:
Ingress controller is nginx.
Hostname:
fileshare.local(map it in/etc/hoststo your ingress IP).
Apply:
Add to /etc/hosts (on your laptop):
Then open: http://fileshare.local → UI Upload → list → download should all work.
Install NGINX ingress
Check Ingress
Check which ingress controller is installed
Patch NGINX Ingress controller to NodePort
Default NodePort Range in Kubernetes
Testing
Test Backend API — list files
Test Backend API — upload file
Test Backend API — download file
Test Frontend landing page
Want a single “ping test”? (fast check)
You should see HTTP 200 or 301.
Full Flow Diagram (Text Version)
Last updated