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>
123 lines
4.0 KiB
Markdown
123 lines
4.0 KiB
Markdown
# 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`.
|