feat: add containerized FastAPI health check
This commit is contained in:
@@ -10,5 +10,4 @@ __pycache__
|
|||||||
data
|
data
|
||||||
downloads
|
downloads
|
||||||
docs
|
docs
|
||||||
tests
|
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
FROM python:3.12-slim-bookworm AS base
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN groupadd --gid 10001 app \
|
||||||
|
&& useradd --uid 10001 --gid app --no-create-home --shell /usr/sbin/nologin app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir --requirement requirements.txt
|
||||||
|
|
||||||
|
FROM base AS test
|
||||||
|
|
||||||
|
COPY requirements-dev.txt pyproject.toml ./
|
||||||
|
RUN pip install --no-cache-dir --requirement requirements-dev.txt
|
||||||
|
|
||||||
|
COPY --chown=app:app app ./app
|
||||||
|
COPY --chown=app:app tests ./tests
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
CMD ["pytest"]
|
||||||
|
|
||||||
|
FROM base AS runtime
|
||||||
|
|
||||||
|
COPY --chown=app:app app ./app
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"]
|
||||||
@@ -49,7 +49,41 @@ Each stage should produce a small, working, reviewable commit.
|
|||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
Local setup instructions will be added with the first application container.
|
The current milestone is a minimal FastAPI application running in a hardened
|
||||||
|
local container. Docker publishes it only on the PC's loopback interface, so it
|
||||||
|
is not exposed to other devices on the LAN.
|
||||||
|
|
||||||
|
Build and start it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Check its status and logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs --tail=50 web
|
||||||
|
```
|
||||||
|
|
||||||
|
Open <http://127.0.0.1:8000> or request the health endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --fail http://127.0.0.1:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the automated tests in an ephemeral container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose run --rm --build test
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop the application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
Runtime data, downloaded media, local environment files, and secrets must not
|
Runtime data, downloaded media, local environment files, and secrets must not
|
||||||
be committed to Git.
|
be committed to Git.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Media Ingest application package."""
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Media Ingest",
|
||||||
|
description="Private broadcast media-ingest service.",
|
||||||
|
version="0.1.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", tags=["system"])
|
||||||
|
def root() -> dict[str, str]:
|
||||||
|
return {"name": "Media Ingest", "status": "running"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health", tags=["system"])
|
||||||
|
def health() -> dict[str, str]:
|
||||||
|
"""Report whether the application process can serve requests."""
|
||||||
|
return {"status": "healthy"}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: runtime
|
||||||
|
image: media-ingest:local
|
||||||
|
init: true
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8000:8000"
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- python
|
||||||
|
- -c
|
||||||
|
- >-
|
||||||
|
import urllib.request;
|
||||||
|
urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,mode=1777
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
test:
|
||||||
|
profiles:
|
||||||
|
- test
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: test
|
||||||
|
image: media-ingest-test:local
|
||||||
|
init: true
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp:size=64m,mode=1777
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-ra"
|
||||||
|
cache_dir = "/tmp/pytest-cache"
|
||||||
|
pythonpath = ["."]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py312"
|
||||||
|
line-length = 100
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
httpx2==2.7.0
|
||||||
|
pytest==9.1.1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fastapi==0.139.2
|
||||||
|
uvicorn==0.51.0
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_returns_healthy() -> None:
|
||||||
|
response = client.get("/health")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"status": "healthy"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_root_identifies_application() -> None:
|
||||||
|
response = client.get("/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"name": "Media Ingest", "status": "running"}
|
||||||
Reference in New Issue
Block a user