Feat: Add production-grade multi-stage container image for API
Two-stage build (uv builder + python:3.12-slim runtime) with non-root user (UID 1001), no dev deps, layer-cache-optimised dep install, and graceful SIGTERM shutdown. Verified by api/tests/build/verify_production_image.sh covering build, health endpoint, non-root, stdout logging, secret-free layers, missing-env-var exit, and dep-layer cache hit. All 102 integration tests still pass; shellcheck clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
122
specs/010-api-prod-dockerfile/contracts/container.md
Normal file
122
specs/010-api-prod-dockerfile/contracts/container.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Contract: Production API Container Image
|
||||
|
||||
This document defines the observable interface of the `reactbin-api-prod` container image. Any orchestration layer (Kubernetes manifests, Docker Compose, CI pipeline) MUST be written against this contract.
|
||||
|
||||
---
|
||||
|
||||
## Network Interface
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Protocol | HTTP/1.1 |
|
||||
| Port | 8000 (TCP) |
|
||||
| Bind address | `0.0.0.0` (all interfaces inside the container) |
|
||||
|
||||
---
|
||||
|
||||
## Health Check
|
||||
|
||||
The container exposes a health check at the existing API health endpoint:
|
||||
|
||||
```
|
||||
GET /api/v1/health
|
||||
```
|
||||
|
||||
**Success response** (`200 OK`):
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
The container declares a built-in `HEALTHCHECK` with the following defaults:
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Interval | 30s |
|
||||
| Timeout | 5s |
|
||||
| Start period | 10s |
|
||||
| Retries | 3 |
|
||||
|
||||
Orchestrators that define their own probes (e.g. Kubernetes `livenessProbe` / `readinessProbe`) SHOULD use this same endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Required Environment Variables
|
||||
|
||||
All configuration is supplied at runtime via environment variables. The image contains no defaults for secret or environment-specific values.
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `JWT_SECRET_KEY` | HS256 signing key for bearer tokens | `change-me-long-random-string` |
|
||||
| `OWNER_USERNAME` | Username of the single owner account | `owner` |
|
||||
| `OWNER_PASSWORD` | Password of the single owner account | `change-me` |
|
||||
| `DATABASE_URL` | PostgreSQL connection URL (asyncpg scheme) | `postgresql+asyncpg://user:pass@host:5432/db` |
|
||||
| `S3_ENDPOINT_URL` | S3-compatible object storage endpoint | `https://s3.amazonaws.com` |
|
||||
| `S3_BUCKET_NAME` | Storage bucket name | `reactbin-prod` |
|
||||
| `S3_ACCESS_KEY_ID` | Storage access key | `AKIAIOSFODNN7EXAMPLE` |
|
||||
| `S3_SECRET_ACCESS_KEY` | Storage secret key | `wJalrXUtnFEMI/K7MDENG` |
|
||||
| `S3_REGION` | Storage region | `us-east-1` |
|
||||
|
||||
**Optional environment variables** (safe defaults apply):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `JWT_EXPIRY_SECONDS` | `86400` | Token lifetime in seconds |
|
||||
| `MAX_UPLOAD_BYTES` | `52428800` | Maximum upload file size (50 MB) |
|
||||
| `LOGIN_MAX_FAILURES` | `5` | Brute-force lock threshold |
|
||||
| `LOGIN_WINDOW_SECONDS` | `300` | Failure counting window |
|
||||
| `LOGIN_COOLDOWN_SECONDS` | `900` | Lock duration after threshold |
|
||||
| `LOGIN_TRUSTED_PROXY_IPS` | `` | Comma-separated trusted proxy CIDRs |
|
||||
| `API_BASE_URL` | _(not required at runtime)_ | Used only by client tooling |
|
||||
|
||||
**Startup failure behaviour**: If a required variable is absent, the application exits with a non-zero code before accepting any requests. The error is logged to stderr identifying the missing variable.
|
||||
|
||||
---
|
||||
|
||||
## Signal Handling
|
||||
|
||||
| Signal | Behaviour |
|
||||
|--------|-----------|
|
||||
| `SIGTERM` | Stop accepting new connections; drain in-flight requests; exit 0 within 30s |
|
||||
| `SIGKILL` | Immediate termination (OS-level; no graceful drain possible) |
|
||||
|
||||
Kubernetes should configure `terminationGracePeriodSeconds ≥ 30` to allow the full drain window.
|
||||
|
||||
---
|
||||
|
||||
## Process Identity
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| User | `appuser` |
|
||||
| UID | `1001` |
|
||||
| GID | `1001` |
|
||||
| Root privileges | None |
|
||||
|
||||
The container MUST NOT be run with `--privileged` or as UID 0.
|
||||
|
||||
---
|
||||
|
||||
## Filesystem
|
||||
|
||||
- **Working directory**: `/app`
|
||||
- **Application source**: `/app/app/`
|
||||
- **Virtual environment**: `/app/.venv/`
|
||||
- **No writable state**: The container requires no persistent local storage. All state is in PostgreSQL and S3.
|
||||
- **Read-only root**: The container is compatible with `--read-only` (no writes to the filesystem at runtime).
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
All log output is written to **stdout** (info/debug) and **stderr** (warnings/errors). No log files are written inside the container. The container runtime log driver captures all output without additional configuration.
|
||||
|
||||
---
|
||||
|
||||
## Image Tags
|
||||
|
||||
| Tag pattern | Meaning |
|
||||
|-------------|---------|
|
||||
| `reactbin-api-prod:latest` | Latest build from `master` |
|
||||
| `reactbin-api-prod:<git-sha>` | Immutable build for a specific commit |
|
||||
|
||||
Deployments SHOULD pin to a specific git SHA tag, not `latest`.
|
||||
Reference in New Issue
Block a user