feat: add containerized FastAPI health check
This commit is contained in:
@@ -10,5 +10,4 @@ __pycache__
|
||||
data
|
||||
downloads
|
||||
docs
|
||||
tests
|
||||
*.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 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
|
||||
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