Scaffold FastAPI + Vue3 placeholder app with local docker compose stack
This commit is contained in:
commit
60f8399f85
10 changed files with 238 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
.env
|
||||
11
Dockerfile.dev
Normal file
11
Dockerfile.dev
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||
1
Procfile
Normal file
1
Procfile
Normal file
|
|
@ -0,0 +1 @@
|
|||
web: uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
59
README.md
Normal file
59
README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# chart
|
||||
|
||||
FastAPI backend + Vue 3 (from CDN, no build step) served at
|
||||
<https://chart.amow.com>.
|
||||
|
||||
Currently a placeholder: the frontend calls `/api/hello` and prints the JSON.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Then open <http://localhost:8000>. The source tree is bind-mounted and uvicorn
|
||||
runs with `--reload`, so edits to `main.py` or `static/` take effect without a
|
||||
rebuild. Rebuild only when `requirements.txt` changes.
|
||||
|
||||
Without Docker:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv && . .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `main.py` | FastAPI app — JSON under `/api`, serves the SPA at `/` |
|
||||
| `static/` | `index.html`, `app.js`, `style.css` — Vue 3 loaded from unpkg |
|
||||
| `requirements.txt` | Python deps |
|
||||
| `Procfile` | Start command; **nixpacks needs this** or the deploy has nothing to run |
|
||||
| `Dockerfile.dev`, `docker-compose.yml` | Local dev only — production does not use them |
|
||||
|
||||
## Deployment
|
||||
|
||||
Push to `main` → Forgejo webhook → Coolify rebuilds with nixpacks → live in
|
||||
roughly 25 seconds.
|
||||
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
Two things to know before changing the deploy config:
|
||||
|
||||
- The Coolify domain is registered as `https://chart.amow.com:8000`. The port
|
||||
suffix is how Coolify decides the Traefik target port; drop it and every
|
||||
request 502s while the container looks perfectly healthy. It does not appear
|
||||
in the public URL.
|
||||
- Ignore a 502 in the first minute after a deploy — that's the rolling container
|
||||
swap, and it clears on its own.
|
||||
|
||||
Manual redeploy:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer $(cat ~/.coolify-token)" \
|
||||
"http://127.0.0.1:8000/api/v1/deploy?uuid=$COOLIFY_UUID&force=true"
|
||||
```
|
||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Local development stack. Production (chart.amow.com) is built by Coolify
|
||||
# with nixpacks straight from this repo — it does not use this file.
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
# Bind-mount the source so --reload picks up edits without a rebuild.
|
||||
- .:/app
|
||||
environment:
|
||||
- PYTHONDONTWRITEBYTECODE=1
|
||||
- PYTHONUNBUFFERED=1
|
||||
33
main.py
Normal file
33
main.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""chart.amow.com — FastAPI backend.
|
||||
|
||||
Placeholder app: serves the Vue 3 single-page frontend from static/ and a
|
||||
couple of JSON endpoints under /api. Replace the endpoints as the real app
|
||||
takes shape; the serving/deploy wiring below does not need to change.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
STATIC_DIR = BASE_DIR / "static"
|
||||
|
||||
app = FastAPI(title="chart")
|
||||
|
||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "chart"}
|
||||
|
||||
|
||||
@app.get("/api/hello")
|
||||
def hello():
|
||||
return {"msg": "backend is live"}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fastapi
|
||||
uvicorn[standard]
|
||||
25
static/app.js
Normal file
25
static/app.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const { createApp, ref } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const result = ref('');
|
||||
const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
async function ping() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const res = await fetch('/api/hello');
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
result.value = JSON.stringify(await res.json(), null, 2);
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { result, error, loading, ping };
|
||||
},
|
||||
}).mount('#app');
|
||||
26
static/index.html
Normal file
26
static/index.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>chart</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>chart</h1>
|
||||
<p class="sub">FastAPI + Vue 3 — placeholder</p>
|
||||
|
||||
<div class="card">
|
||||
<button @click="ping" :disabled="loading">
|
||||
{{ loading ? 'calling…' : 'call /api/hello' }}
|
||||
</button>
|
||||
<pre v-if="result">{{ result }}</pre>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
62
static/style.css
Normal file
62
static/style.css
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #ffffff;
|
||||
--fg: #16181d;
|
||||
--muted: #6b7280;
|
||||
--line: #e4e6eb;
|
||||
--accent: #2f6feb;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #14161a;
|
||||
--fg: #e8eaed;
|
||||
--muted: #9aa1ab;
|
||||
--line: #2a2e35;
|
||||
--accent: #6d9bf5;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 3rem 1.5rem;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
#app { max-width: 40rem; margin: 0 auto; }
|
||||
|
||||
h1 { margin: 0; font-size: 1.75rem; letter-spacing: -0.01em; }
|
||||
|
||||
.sub { margin: 0.25rem 0 2rem; color: var(--muted); }
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled { opacity: 0.6; cursor: default; }
|
||||
|
||||
pre {
|
||||
margin: 1rem 0 0;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
border-radius: 6px;
|
||||
background: color-mix(in srgb, var(--fg) 6%, transparent);
|
||||
}
|
||||
|
||||
.error { color: #d24b4b; }
|
||||
Loading…
Reference in a new issue