134 lines
3.9 KiB
Markdown
134 lines
3.9 KiB
Markdown
# Placeholder API
|
||
|
||
Generate SVG and PNG placeholder images with configurable dimensions and font size.
|
||
|
||
## Prerequisites
|
||
|
||
Local development uses uv and Python 3.12 or newer. PNG rendering also needs
|
||
Cairo, which you install with your system package manager before running `uv sync`.
|
||
|
||
Run the appropriate command for your distribution as root:
|
||
|
||
| Distribution | Install Cairo |
|
||
| --- | --- |
|
||
| [Alpine](https://pkgs.alpinelinux.org/packages?name=cairo) | `apk add cairo` |
|
||
| [Arch](https://archlinux.org/packages/extra/x86_64/cairo/) | `pacman -S cairo` |
|
||
| [Debian](https://packages.debian.org/en/trixie/libcairo2) | `apt install libcairo2` |
|
||
| [Void](https://github.com/void-linux/void-packages/blob/master/srcpkgs/cairo/template) | `xbps-install -S cairo` |
|
||
|
||
For a container setup, you only need Podman on the host. The image includes
|
||
Python, Cairo, and fonts.
|
||
|
||
## Running the app
|
||
|
||
Run the development server:
|
||
|
||
```sh
|
||
uv sync
|
||
uv run flask --app app:create_app run --debug
|
||
```
|
||
|
||
Alternatively, `uv run app` starts the development server without debug mode.
|
||
|
||
- `GET /placeholder.svg` returns an SVG placeholder.
|
||
- `GET /placeholder.png` returns a PNG placeholder.
|
||
- `GET /healthz` returns `{"status": "ok"}`.
|
||
|
||
Image endpoints accept `width`, `height`, and `font_size` query parameters.
|
||
|
||
For example, `/placeholder.png?width=320&height=180&font_size=20` returns a
|
||
320 × 180 PNG with 20-pixel text.
|
||
|
||
Run the Gunicorn server locally:
|
||
|
||
```sh
|
||
uv run gunicorn --bind 0.0.0.0:8000 --workers 2 --access-logfile - --error-logfile - 'app:create_app()'
|
||
```
|
||
|
||
Build and run the container:
|
||
|
||
```sh
|
||
podman build -t placeholder-api -f Containerfile .
|
||
podman run --rm -p 8000:8000 placeholder-api
|
||
```
|
||
|
||
The container runs as an unprivileged user and serves HTTP on port 8000.
|
||
Check it with `curl http://localhost:8000/healthz`.
|
||
Additional Gunicorn options can be passed through `GUNICORN_CMD_ARGS`.
|
||
|
||
The application factory is in `src/app/__init__.py`; API routes live in
|
||
`src/app/routes/api.py`.
|
||
|
||
## Behind Caddy
|
||
|
||
For a single Caddy proxy running on the host, add these settings to the Quadlet's
|
||
`[Container]` section:
|
||
|
||
```ini
|
||
Environment=PROXY_PASS=1
|
||
PublishPort=127.0.0.1:12345:8000
|
||
```
|
||
|
||
To serve the app under e.g., `/!/placeholder`, configure the Caddyfile:
|
||
|
||
```caddyfile
|
||
files.example.com {
|
||
handle_path /!/placeholder/* {
|
||
reverse_proxy 127.0.0.1:12345 {
|
||
header_up X-Forwarded-Prefix /!/placeholder
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Images are available at `/!/placeholder/example.png` or `.svg`, and health checks
|
||
at `/!/placeholder/healthz`. Caddy strips the prefix before forwarding.
|
||
|
||
`PROXY_PASS=1` trusts one proxy's forwarded headers for client IPs and public URLs.
|
||
It is disabled by default. Enable it only behind a trusted, public-facing Caddy
|
||
proxy, and keep the backend port bound to loopback as shown.
|
||
|
||
## Development checks
|
||
|
||
Install the Git hooks once after cloning and running `uv sync`:
|
||
|
||
```sh
|
||
uv run pre-commit install
|
||
```
|
||
|
||
Run all checks on tracked files:
|
||
|
||
```sh
|
||
uv run pre-commit run --all-files
|
||
```
|
||
|
||
The hooks run Ruff, Pyright, and pytest before commits, and validate commit
|
||
messages with Commitizen. To run just the tests:
|
||
|
||
```sh
|
||
uv run pytest -q
|
||
```
|
||
|
||
## Caching and rate limits
|
||
|
||
Successful image responses allow public caching for one day. Rendered images are
|
||
also cached in memory on the server with a one-day expiration.
|
||
|
||
Rate limits apply per client IP, per Gunicorn worker:
|
||
|
||
| Format | Requests per minute | Uncached renders per minute |
|
||
| --- | ---: | ---: |
|
||
| SVG | 900 | 300 |
|
||
| PNG | 900 | 30 |
|
||
|
||
Exceeding a limit returns HTTP 429. Server-cached images remain available after
|
||
the render limit is reached, subject to the request limit. Responses served
|
||
directly from a browser or CDN cache do not reach the app or consume its quotas.
|
||
|
||
Each Gunicorn worker maintains its own cache and rate-limit counters. Defaults
|
||
are configured in `src/app/config.py`.
|
||
|
||
## License
|
||
|
||
Licensed under the [BSD 3-Clause License](LICENSE).
|