feat: Docker deploy serving SPA and API on :8080
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
.git
|
||||||
|
.worktrees
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
data
|
||||||
|
**/__pycache__
|
||||||
|
**/*.pyc
|
||||||
|
.pytest_cache
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
frontend/.vscode
|
||||||
|
*.db
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
**/*.md
|
||||||
|
!README.md
|
||||||
|
docs
|
||||||
|
backend/tests
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Stage 1 — build Vue SPA
|
||||||
|
FROM node:20 AS frontend-build
|
||||||
|
WORKDIR /frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2 — FastAPI + built SPA
|
||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV DATA_DIR=/data \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
COPY backend/requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY backend/app ./app
|
||||||
|
COPY --from=frontend-build /frontend/dist ./frontend/dist
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# MincedPad
|
||||||
|
|
||||||
|
Self-hosted anonymous paste / file share: public wall, TTL, burn-after-read, and a password-gated admin panel. One Docker container serves the Vue SPA and FastAPI API on port **8080**.
|
||||||
|
|
||||||
|
## Deploy (Docker Compose)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://127.0.0.1:8080/
|
||||||
|
Admin: http://127.0.0.1:8080/admin
|
||||||
|
|
||||||
|
Data (SQLite + uploads) persists in `./data` on the host (`DATA_DIR=/data` in the container).
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ADMIN_PASSWORD` | `changeme` | Admin login password |
|
||||||
|
| `SECRET_KEY` | (dev default) | JWT signing secret — change in production |
|
||||||
|
| `MAX_UPLOAD_MB` | `200` | Max upload size in megabytes |
|
||||||
|
| `RATE_LIMIT_PER_SECOND` | `2` | Create/upload rate limit per IP |
|
||||||
|
| `DATA_DIR` | `/data` | SQLite + uploads directory |
|
||||||
|
|
||||||
|
Set these under `environment:` in `docker-compose.yml` (or via your orchestrator).
|
||||||
|
|
||||||
|
## Mobile
|
||||||
|
|
||||||
|
The UI is mobile-first (Element Plus responsive grid, full-width controls under 768px). Use a real phone or browser device mode for smoke checks after deploy.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
**API**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python -m venv ../.venv && source ../.venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
DATA_DIR=../data PYTHONPATH=. uvicorn app.main:app --host 0.0.0.0 --port 8080 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend** (proxies `/api` to `:8080`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Production-style (API serves `frontend/dist`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend && npm run build
|
||||||
|
cd ../backend && DATA_DIR=../data PYTHONPATH=. uvicorn app.main:app --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Use as you like for self-hosting.
|
||||||
+34
-1
@@ -1,7 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from app.api.admin import router as admin_router
|
from app.api.admin import router as admin_router
|
||||||
from app.api.public import router as public_router
|
from app.api.public import router as public_router
|
||||||
@@ -11,6 +14,20 @@ from app import models # noqa: F401 — register models with Base.metadata
|
|||||||
from app.services import cleaner as cleaner_service
|
from app.services import cleaner as cleaner_service
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_frontend_dist() -> Path | None:
|
||||||
|
"""Locate frontend/dist in Docker (/app/frontend/dist) or monorepo checkout."""
|
||||||
|
here = Path(__file__).resolve()
|
||||||
|
candidates = (
|
||||||
|
here.parents[2] / "frontend" / "dist", # .../backend/app/main.py → repo root
|
||||||
|
here.parents[1] / "frontend" / "dist", # /app/app/main.py → /app/frontend/dist
|
||||||
|
Path("/app/frontend/dist"),
|
||||||
|
)
|
||||||
|
for path in candidates:
|
||||||
|
if (path / "index.html").is_file():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _cleaner_loop() -> None:
|
async def _cleaner_loop() -> None:
|
||||||
while True:
|
while True:
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@@ -47,3 +64,19 @@ app.include_router(admin_router)
|
|||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
def health():
|
def health():
|
||||||
return {"ok": True, "name": "MincedPad"}
|
return {"ok": True, "name": "MincedPad"}
|
||||||
|
|
||||||
|
|
||||||
|
_dist = _resolve_frontend_dist()
|
||||||
|
if _dist is not None:
|
||||||
|
assets_dir = _dist / "assets"
|
||||||
|
if assets_dir.is_dir():
|
||||||
|
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
|
||||||
|
|
||||||
|
@app.api_route("/{full_path:path}", methods=["GET", "HEAD"])
|
||||||
|
async def spa(full_path: str):
|
||||||
|
if full_path == "api" or full_path.startswith("api/"):
|
||||||
|
raise HTTPException(status_code=404, detail="Not Found")
|
||||||
|
file_path = _dist / full_path
|
||||||
|
if full_path and file_path.is_file():
|
||||||
|
return FileResponse(file_path)
|
||||||
|
return FileResponse(_dist / "index.html")
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
mincedpad:
|
||||||
|
build: .
|
||||||
|
ports: ["8080:8080"]
|
||||||
|
environment:
|
||||||
|
ADMIN_PASSWORD: "changeme"
|
||||||
|
SECRET_KEY: "change-me-in-production"
|
||||||
|
MAX_UPLOAD_MB: "200"
|
||||||
|
RATE_LIMIT_PER_SECOND: "2"
|
||||||
|
DATA_DIR: "/data"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
restart: unless-stopped
|
||||||
Reference in New Issue
Block a user