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.


chevron-rightApp source codehashtag

1. Folder structure

fileshare-app/
├── backend/
│   ├── main.py
│   ├── requirements.txt
│   └── Dockerfile
└── frontend/
    ├── package.json
    ├── index.html
    ├── src/
    │   ├── main.jsx
    │   └── App.jsx
    └── Dockerfile

2. Backend (Python / FastAPI)

backend/requirements.txt

fastapi
uvicorn[standard]
python-multipart

backend/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

FROM 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)

{
  "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

<!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

import 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

import 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

# 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:v1

Frontend:

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/hosts to your ingress IP).

Apply:

Add to /etc/hosts (on your laptop):

Then open: http://fileshare.localarrow-up-right → UI Upload → list → download should all work.


  1. Install NGINX ingress

  1. Check Ingress

  1. Check which ingress controller is installed

  1. 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