organize deployment configs and documentation

This commit is contained in:
2026-07-10 12:57:21 +00:00
parent 47795011b6
commit ff2290f0f1
14 changed files with 387 additions and 375 deletions
-141
View File
@@ -1,141 +0,0 @@
# Production migration and rollback
This runbook moves the titan deployment from the Python source tree at
`/root/repo/urlshortener` to the published `sodium/ushort` image. The final
server-side application directory is `/root/compose/ushort`; no source checkout
is required on titan.
Do not cut over until the exact image tag has been built for both `linux/amd64`
and `linux/arm64`, pushed, and recorded by digest.
## 1. Prepare without affecting production
Create this layout on titan:
```text
/root/compose/ushort/
├── docker-compose.yml
├── config.toml
└── data/
```
Copy `deploy/compose.production.yml` to the server as `docker-compose.yml`.
Convert the live JSON
settings to TOML using `config.example.toml` as the reference, with these
cutover-specific rules:
- keep the live `base_url`, limits, retention, and rate-limit values;
- set `db_path = "data/urlshort.db"`;
- set `production = false` so the binary serves its embedded frontend; and
- generate a new API key, because the old key existed in the legacy Git
history.
Keep `config.toml` owned by `1001:1001` with mode `0400`, matching the
container identity. Do not copy it into Git or a container image.
Validate the deployment file before stopping anything:
```bash
cd /root/compose/ushort
chown 1001:1001 config.toml
chmod 0400 config.toml
docker-compose config
docker pull sodium/ushort:0.1.1
```
## 2. Take a consistent database copy
The final copy must be made while the Python service is stopped so no committed
row is missed and no SQLite journal is in flight:
```bash
cd /root/repo/urlshortener
docker-compose stop urlshort
cp -a data/urlshort.db /root/compose/ushort/data/urlshort.db
cp -a data/urlshort.db /root/compose/ushort/data/urlshort.db.pre-rust
chown -R 1001:1001 /root/compose/ushort/data
chmod 0750 /root/compose/ushort/data
chmod 0640 /root/compose/ushort/data/urlshort.db*
```
Do not delete or alter the original database during the initial soak period.
## 3. Start and validate ushort locally
```bash
cd /root/compose/ushort
docker-compose up -d
docker-compose ps
docker-compose logs --tail=100 ushort
curl -i http://127.0.0.1:18082/s/api/health
curl -I http://127.0.0.1:18082/s/
```
Expected results are HTTP 200 health JSON and HTTP 200 HTML. Confirm the
container does not restart and the database remains writable.
## 4. Switch nginx
Replace the old `include /root/repo/urlshortener/nginx.conf;` in the `xcel.me`
vhost with the location blocks from the new `nginx.conf`. Those blocks proxy
the frontend as well as API/redirect traffic to `127.0.0.1:18082`; they do not
refer to a source or static-file directory.
Test before reload:
```bash
docker exec nginx nginx -t
docker exec nginx nginx -s reload
```
Then validate through the public endpoint:
```bash
curl -i https://xcel.me/s/api/health
curl -I https://xcel.me/s/
curl -I https://xcel.me/s/static/app.js
```
Check that `/s/` is `no-store`, static assets are `no-cache`, an existing short
code still returns the original 302 target, and its visit count increments once.
## 5. Soak and clean up
During the soak period, monitor:
```bash
cd /root/compose/ushort
docker-compose ps
docker-compose logs --tail=200 ushort
```
After the rollback window closes:
1. retain a protected database backup;
2. remove the obsolete `/root/repo/urlshortener` source tree;
3. remove its `/srv/urlshortener` bind mount from the global nginx Compose
file; and
4. recreate nginx and re-run `nginx -t`.
At that point, application source exists only in Gitea, while titan contains
only the Compose file, TOML config, and data under `/root/compose/ushort`.
## Rollback
If validation fails before public traffic is enabled:
```bash
cd /root/compose/ushort
docker-compose down
cd /root/repo/urlshortener
docker-compose start urlshort
```
Restore the old nginx include and reload nginx.
If public writes occurred after cutover, stop ushort first and copy its current
database back to the legacy data path before starting Python; otherwise those
new short URLs would be lost. The schema is deliberately identical, so no
reverse schema migration is required.
+29 -217
View File
@@ -1,234 +1,46 @@
# ushort
`ushort` is a compact URL shortener written in Rust and backed by SQLite. It is
a behavior-compatible replacement for the original stdlib-only Python service:
the routes, base-path handling, response shapes, database schema, retention,
API-key checks, throttling, redirects, and frontend are preserved.
`ushort` is a compact, self-contained URL shortener written in Rust and backed
by SQLite. It serves an embedded web interface and JSON API from a single
statically linked executable.
The frontend under `static/` is embedded into the executable at compile time.
The final Linux image contains only one statically linked executable and runs
without Python, a shell, a package manager, or external static files.
It supports deployment at either a dedicated host such as `s.example.com` or
under a path such as `example.com/s`. Images are available for Linux AMD64 and
ARM64.
## Quick start
Requirements: Docker with the Compose plugin.
```bash
cp config.example.toml config.toml
# Edit base_url and api_key, then:
docker compose up --build -d
# Set a private api_key in config.toml.
mkdir -p data
USHORT_UID="$(id -u)" USHORT_GID="$(id -g)" \
docker compose -f deploy/docker-compose.build.yml up --build -d
```
The root `docker-compose.yml` is the local-development definition and builds
the checked-out source. `deploy/compose.production.yml` is the source-free
server definition and pulls a versioned image; copy it to the deployment host
as `docker-compose.yml`.
The example starts ushort at <http://localhost:18082/s/> and stores its SQLite
database in `./data/urlshort.db`.
The service listens on `127.0.0.1:18082` on the host. Its SQLite database is
stored at `./data/urlshort.db` and survives container replacement.
The container runs as UID/GID `1001:1001` by default. Override
`USHORT_UID`/`USHORT_GID` if needed, and ensure `./data` is writable by that
identity.
## Configuration
TOML is the preferred format; see `config.example.toml`. Existing JSON files
remain supported, including the old `short_length` alias for
`min_short_length`.
Check it and stop it with:
```bash
ushort config.toml
ushort legacy-config.json
curl http://localhost:18082/s/api/health
docker compose -f deploy/docker-compose.build.yml down
```
| Key | Required | Default | Description |
|---|---:|---:|---|
| `base_url` | yes | — | Public URL. Its path becomes the routing prefix. |
| `api_key` | yes | — | Secret used by list and delete operations. |
| `host` | no | `0.0.0.0` | Bind address. |
| `port` | no | `8080` | Bind port. |
| `db_path` | no | `data/urlshort.db` | SQLite database path. |
| `retention_days` | no | `0` | Default lifetime for new URLs; zero never expires. |
| `min_short_length` | no | `6` | Initial generated code length. |
| `max_short_length` | no | `32` | Maximum generated and accepted code length. |
| `max_url_length` | no | `2048` | Maximum original URL length. |
| `max_retention_days` | no | `3650` | Maximum per-URL retention value. |
| `rate_limit_requests` | no | `60` | Requests allowed per client/window. |
| `rate_limit_window` | no | `60` | Sliding-window length in seconds. |
| `production` | no | `false` | When true, disable embedded frontend routes for compatibility with deployments that serve static files separately. |
For a production server, use the published image through
`deploy/docker-compose.prod.yml`; it does not build or require a source
checkout.
The same binary supports both common reverse-proxy layouts:
## Documentation
- `base_url = "https://s.example.com"` serves the frontend, API, and short
codes at the subdomain root, producing `https://s.example.com/aB3xYz`.
- `base_url = "https://example.com/go/links"` limits routing to `/go/links`
and produces `https://example.com/go/links/aB3xYz`. Requests outside that
prefix return 404.
A trailing slash on `base_url` is accepted in either layout and is normalized
when short URLs are generated.
Real API keys belong only in the deployment's ignored `config.toml`, never in
the repository or container image.
## API
All paths below are relative to the path in `base_url`. Trailing slashes are
accepted by every frontend, API, metadata, deletion, and short-code route,
including when a query string follows the slash. Every response includes the
legacy CORS and security headers.
### Health and frontend
- `GET /` serves the byte-identical embedded frontend when `production=false`.
- `GET /api/health` returns HTTP 200:
```json
{"status":"ok","service":"url-shortener"}
```
When the configured base path is non-empty, a GET of the bare path (for
example `/s`) redirects to `/s/`.
### Create a short URL
`POST /api/shorten` and `GET /api/shorten` are both supported and do not
require an API key. Fields may be supplied as query parameters or in a JSON
body; query parameters take precedence.
| Field | Required | Description |
|---|---:|---|
| `url` | yes | Absolute `http://` or `https://` URL. |
| `retention_days` | no | Per-URL lifetime, or zero to keep forever. |
Success is HTTP 201 with only the short URL as UTF-8 plain text:
```text
https://example.com/s/aB3xYz
```
Codes begin at `min_short_length`. After ten collisions at one length the
service tries the next length, through `max_short_length`.
### List URLs
`GET /api/urls?api_key=<key>` returns all rows newest first. A missing or
invalid key returns HTTP 403 with an empty body.
```json
{
"count": 1,
"urls": [
{
"short_code": "aB3xYz",
"short_url": "https://example.com/s/aB3xYz",
"original_url": "https://example.org",
"created_at": 1710000000,
"visit_count": 5,
"retention_days": 30
}
]
}
```
### Metadata and lookup
- `GET /api/urls/<code>` returns one metadata object without authentication.
- `GET /api/lookup?url=<encoded-url>` returns the newest matching metadata
object without authentication.
- A missing record returns HTTP 404 with an empty body.
### Delete
`DELETE /api/urls/<code>?api_key=<key>` returns:
- HTTP 204 with an empty body on success;
- HTTP 404 with an empty body when the code does not exist; or
- HTTP 403 with an empty body when authentication fails.
### Redirect
`GET /<code>` atomically increments `visit_count` and responds with HTTP 302 to
the stored original URL.
## Data compatibility
The existing database can be mounted directly; no export/import is required.
The schema remains:
```sql
CREATE TABLE urls (
short_code TEXT PRIMARY KEY,
original_url TEXT NOT NULL,
created_at INTEGER NOT NULL,
visit_count INTEGER NOT NULL DEFAULT 0,
retention_days INTEGER NOT NULL DEFAULT 0
);
```
Older databases missing `retention_days` are upgraded in place with a default
of zero. Expired records are removed on startup and before each GET, POST, or
DELETE request, using the original strict expiry boundary.
Before a production cutover, make a filesystem-consistent backup of the
database (or use SQLite's backup command) and test the copy with the new image.
## Frontend
The existing `index.html`, JavaScript, CSS, locally hosted fonts, sorting,
pagination, theme selection, URL lookup, copy/delete actions, and admin flow
are unchanged. Compile-time embedding makes those files part of the standalone
artifact; no bind mount or separate web root is needed.
For a path deployment such as the existing `/s`, place the location blocks
from `nginx.conf` in the public vhost. For a dedicated subdomain, use
`deploy/nginx-subdomain.conf` in its `server` block. Both examples proxy the
frontend, API, and redirects while retaining `no-store` HTML and `no-cache`
asset policies.
## Build and test
The repository intentionally does not require a host Rust installation.
```bash
docker build -t ushort:local .
docker compose up -d
```
Run the Rust test suite in an isolated build stage:
```bash
docker build --target tester .
```
Run the real HTTP trailing-slash smoke test against any disposable deployment:
```bash
sh tests/smoke.sh https://s.example.com "$API_KEY"
sh tests/smoke.sh https://example.com/s "$API_KEY"
```
The Dockerfile builds against musl and uses `scratch` for the runtime image.
Docker Buildx can publish both required architectures from the same source:
```bash
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t sodium/ushort:latest \
--push .
```
## Security model
- SQL statements use bound parameters.
- Short codes are restricted to ASCII alphanumerics.
- URL, retention, code, and request-body sizes are bounded.
- API-key comparison is constant-time; protected operations return no body on
authentication failure.
- Per-IP sliding-window throttling applies to API calls and redirects; embedded
frontend files do not consume API quota. The app uses `X-Real-IP`, then the
first `X-Forwarded-For` value, then the TCP peer. Keep the container port
bound to loopback and let the trusted reverse proxy overwrite those headers.
- CORS preflight uses `OPTIONS`; it does not consume rate-limit quota.
- The runtime image is read-only, drops Linux capabilities, and runs as a
numeric non-root user through Compose.
- `SIGINT`/`SIGTERM` cleanly unblock the server for prompt container shutdown.
- [Configuration](docs/configuration.md)
- [API reference](docs/api.md)
- [Embedded frontend](docs/frontend.md)
- [Data and database compatibility](docs/data.md)
- [Production deployment and rollback](docs/deployment.md)
- [Development, testing, and releases](docs/development.md)
- [Security model](docs/security.md)
+1 -1
View File
@@ -1,5 +1,5 @@
# Public URL, including the optional routing prefix.
base_url = "http://localhost:8080/s"
base_url = "http://localhost:18082/s"
# Replace this in the deployment-only config.toml. Do not commit real secrets.
api_key = "change-this-secret-key"
@@ -1,14 +1,14 @@
services:
ushort:
image: sodium/ushort:latest
image: ushort:local
build:
context: .
context: ..
command: ["/app/config.toml"]
ports:
- "127.0.0.1:18082:8080"
volumes:
- "${USHORT_CONFIG:-./config.toml}:/app/config.toml:ro"
- ./data:/app/data
- "${USHORT_CONFIG:-../config.toml}:/app/config.toml:ro"
- ../data:/app/data
user: "${USHORT_UID:-1001}:${USHORT_GID:-1001}"
read_only: true
security_opt:
@@ -16,4 +16,3 @@ services:
cap_drop:
- ALL
restart: unless-stopped
+4 -6
View File
@@ -1,12 +1,11 @@
# URL Shortener — nginx location blocks
# Drop these into the existing server {} block. The Rust binary serves both
# the API and its embedded, byte-identical frontend on 127.0.0.1:18082.
# URL Shortener — path-prefix deployment
# Use inside a server {} block with base_url configured for /s.
location = /s {
return 301 /s/;
}
# Preserve the existing no-store policy for the HTML entry point.
# HTML entry point.
location = /s/ {
proxy_pass http://127.0.0.1:18082;
proxy_http_version 1.1;
@@ -20,7 +19,7 @@ location = /s/ {
add_header Cache-Control "no-store";
}
# Preserve the existing revalidation policy for CSS, JavaScript, and fonts.
# CSS, JavaScript, and fonts.
location ^~ /s/static/ {
proxy_pass http://127.0.0.1:18082;
proxy_http_version 1.1;
@@ -44,4 +43,3 @@ location /s/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
@@ -1,8 +1,7 @@
# URL Shortener — dedicated subdomain location blocks
# Use inside the server {} block for a host such as s.example.com. Configure
# base_url = "https://s.example.com" in config.toml.
# URL Shortener — dedicated-subdomain deployment
# Use inside a server {} block with base_url configured for that host.
# Preserve the no-store policy for the HTML entry point.
# HTML entry point.
location = / {
proxy_pass http://127.0.0.1:18082;
proxy_http_version 1.1;
@@ -16,7 +15,7 @@ location = / {
add_header Cache-Control "no-store";
}
# Preserve the revalidation policy for CSS, JavaScript, and fonts.
# CSS, JavaScript, and fonts.
location ^~ /static/ {
proxy_pass http://127.0.0.1:18082;
proxy_http_version 1.1;
+80
View File
@@ -0,0 +1,80 @@
# API reference
All paths are relative to the path in `base_url`. Every frontend, API,
metadata, deletion, and short-code route accepts trailing slashes, including
when a query string follows the slash.
## Health and frontend
- `GET /` serves the embedded frontend when `production=false`.
- `GET /api/health` returns HTTP 200:
```json
{"status": "ok", "service": "url-shortener"}
```
With a non-empty base path, requesting the bare path redirects to its trailing
slash form; for example, `/s` redirects to `/s/`.
## Create a short URL
`POST /api/shorten` and `GET /api/shorten` are supported without an API key.
Fields may be supplied as query parameters or in a JSON body; query parameters
take precedence.
| Field | Required | Description |
|---|---:|---|
| `url` | yes | Absolute `http://` or `https://` URL. |
| `retention_days` | no | Per-URL lifetime, or zero to keep forever. |
Success is HTTP 201 with only the short URL as UTF-8 plain text:
```text
https://example.com/s/aB3xYz
```
Codes begin at `min_short_length`. After ten collisions at one length, ushort
tries the next length through `max_short_length`.
## List URLs
`GET /api/urls?api_key=<key>` returns all rows newest first. A missing or
invalid key returns HTTP 403 with an empty body.
```json
{
"count": 1,
"urls": [
{
"short_code": "aB3xYz",
"short_url": "https://example.com/s/aB3xYz",
"original_url": "https://example.org",
"created_at": 1710000000,
"visit_count": 5,
"retention_days": 30
}
]
}
```
## Metadata and lookup
- `GET /api/urls/<code>` returns one metadata object without authentication.
- `GET /api/lookup?url=<encoded-url>` returns the newest matching metadata
object without authentication.
- A missing record returns HTTP 404 with an empty body.
## Delete
`DELETE /api/urls/<code>?api_key=<key>` returns:
- HTTP 204 with an empty body on success;
- HTTP 404 with an empty body when the code does not exist; or
- HTTP 403 with an empty body when authentication fails.
## Redirect
`GET /<code>` atomically increments `visit_count` and responds with HTTP 302 to
the stored original URL.
Every response includes the compatibility CORS and security headers.
+44
View File
@@ -0,0 +1,44 @@
# Configuration
ushort accepts TOML and legacy JSON configuration files. TOML is preferred;
[`config.example.toml`](../config.example.toml) is the maintained reference.
```bash
ushort config.toml
ushort legacy-config.json
```
## Options
| Key | Required | Default | Description |
|---|---:|---:|---|
| `base_url` | yes | — | Public URL. Its path becomes the routing prefix. |
| `api_key` | yes | — | Secret used by list and delete operations. |
| `host` | no | `0.0.0.0` | Bind address. |
| `port` | no | `8080` | Bind port. |
| `db_path` | no | `data/urlshort.db` | SQLite database path. |
| `retention_days` | no | `0` | Default lifetime for new URLs; zero never expires. |
| `min_short_length` | no | `6` | Initial generated code length. |
| `max_short_length` | no | `32` | Maximum generated and accepted code length. |
| `max_url_length` | no | `2048` | Maximum original URL length. |
| `max_retention_days` | no | `3650` | Maximum per-URL retention value. |
| `rate_limit_requests` | no | `60` | Requests allowed per client/window. |
| `rate_limit_window` | no | `60` | Sliding-window length in seconds. |
| `production` | no | `false` | Disable embedded frontend routes when a separate server provides them. |
The legacy `short_length` option remains an alias for `min_short_length`.
## Public URL layouts
The same binary supports both common reverse-proxy layouts:
- `base_url = "https://s.example.com"` serves everything at the subdomain
root and produces URLs such as `https://s.example.com/aB3xYz`.
- `base_url = "https://example.com/go/links"` routes only under `/go/links`
and produces `https://example.com/go/links/aB3xYz`.
A trailing slash on `base_url` is accepted and normalized. Requests outside a
configured non-empty path prefix return 404.
Keep real API keys only in the ignored deployment `config.toml`. Never commit
them or include them in a container image.
+31
View File
@@ -0,0 +1,31 @@
# Data and database compatibility
ushort stores data in SQLite. The database can be mounted directly across
upgrades; no export or import is required.
```sql
CREATE TABLE urls (
short_code TEXT PRIMARY KEY,
original_url TEXT NOT NULL,
created_at INTEGER NOT NULL,
visit_count INTEGER NOT NULL DEFAULT 0,
retention_days INTEGER NOT NULL DEFAULT 0
);
```
Older databases missing `retention_days` are upgraded in place with a default
of zero. Expired records are removed on startup and before each GET, POST, or
DELETE request, using a strict expiry boundary.
## Backups
Take a consistent backup before every production update. SQLite's online
backup command avoids copying a database while a WAL transaction is active:
```bash
sqlite3 data/urlshort.db ".backup data/urlshort.db.pre-update"
sqlite3 data/urlshort.db.pre-update "PRAGMA integrity_check;"
```
Retain the backup until the updated release has passed health, redirect,
write, and database-integrity checks.
+90
View File
@@ -0,0 +1,90 @@
# Production deployment and rollback
The production host needs only a Compose file, `config.toml`, and persistent
data. It does not need a source checkout.
## Files
Create an application directory like this:
```text
/opt/ushort/
├── docker-compose.yml
├── config.toml
└── data/
```
Copy [`deploy/docker-compose.prod.yml`](../deploy/docker-compose.prod.yml) to
the server as `docker-compose.yml`. For repeatable releases, replace its image
tag with the verified multi-architecture digest:
```yaml
image: docker.io/sodium/ushort:0.1.1@sha256:<verified-index-digest>
```
Set `db_path = "data/urlshort.db"` and normally keep `production = false` so
the executable serves its embedded frontend. Keep `config.toml` owned by the
container identity (`1001:1001`) with mode `0400`; the `data` directory must be
writable by the same identity.
## Start and validate
```bash
cd /opt/ushort
docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 ushort
curl -i http://127.0.0.1:18082/s/api/health
```
Adjust the health path to match `base_url`.
## Reverse proxy
Choose one nginx snippet and include it inside the public `server {}` block:
- [`deploy/nginx.path.conf`](../deploy/nginx.path.conf) for a path such as
`https://example.com/s`;
- [`deploy/nginx.subdomain.conf`](../deploy/nginx.subdomain.conf) for a host
such as `https://s.example.com`.
Both snippets proxy the frontend, API, and redirects to `127.0.0.1:18082`.
They apply `no-store` to the HTML entry point and `no-cache` to static assets.
The path snippet is written for `/s`; replace each `/s` location when using a
different configured prefix.
Test nginx before reloading it:
```bash
nginx -t
nginx -s reload
```
Then verify the public health endpoint, frontend, static assets, and an
existing short-code redirect. [`tests/smoke.sh`](../tests/smoke.sh) exercises
the complete trailing-slash route set against a disposable record.
## Update
1. Verify the published image contains both AMD64 and ARM64 manifests.
2. Pull the exact version and digest before changing the running service.
3. Create and integrity-check an online SQLite backup as described in
[Data and database compatibility](data.md).
4. Update the image reference and run `docker compose up -d`.
5. Check health, logs, restart count, database integrity, and public routes.
## Rollback
Restore the previous image reference and run:
```bash
docker compose up -d
```
The schema is release-compatible. If the failed release changed or damaged
data, stop the service and restore the pre-update SQLite backup before bringing
the previous image back up. Preserve any legitimate writes made after the
backup before replacing the database.
+58
View File
@@ -0,0 +1,58 @@
# Development, testing, and releases
The repository does not require a host Rust installation. The Dockerfile uses
musl and produces a `scratch` runtime image containing only the executable.
## Local source build
[`deploy/docker-compose.build.yml`](../deploy/docker-compose.build.yml) builds
the checked-out source. Its build context and bind mounts point back to the
repository root.
```bash
cp config.example.toml config.toml
mkdir -p data
USHORT_UID="$(id -u)" USHORT_GID="$(id -g)" \
docker compose -f deploy/docker-compose.build.yml up --build -d
```
The production definition,
[`deploy/docker-compose.prod.yml`](../deploy/docker-compose.prod.yml), never
builds source. It pulls the published version instead.
## Automated tests
Run all Rust targets in the isolated tester stage:
```bash
docker build --target tester .
```
Run the real HTTP trailing-slash suite against a disposable deployment:
```bash
sh tests/smoke.sh https://s.example.com "$API_KEY"
sh tests/smoke.sh https://example.com/s "$API_KEY"
```
The smoke test creates and removes one temporary short URL.
## Multi-architecture release
Buildx can publish AMD64 and ARM64 from the same source:
```bash
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t sodium/ushort:latest \
--push .
```
Inspect and record the OCI index digest before deployment:
```bash
docker buildx imagetools inspect sodium/ushort:latest
```
The embedded frontend test verifies every file under `static/` byte-for-byte,
so no separate web-root packaging step is required.
+20
View File
@@ -0,0 +1,20 @@
# Embedded frontend
The frontend under `static/` is embedded into the executable at compile time.
No separate web root or static-file bind mount is required.
It includes URL creation and lookup, short-link metadata, copy and delete
actions, API-key administration, sorting, pagination, theme selection, and
locally hosted fonts. Relative asset URLs and the browser-derived API prefix
allow the same files to work at either a subdomain root or a nested path.
Set `production = false` for the normal self-contained deployment. Setting it
to `true` disables the embedded frontend routes for installations that serve
those files separately.
The nginx examples apply `no-store` to the HTML entry point and `no-cache` to
static assets. Embedded static requests do not consume the API rate-limit
quota.
Automated tests compare every embedded asset with its source file byte for
byte. See [Development, testing, and releases](development.md).
+22
View File
@@ -0,0 +1,22 @@
# Security model
- SQL statements use bound parameters.
- Short codes are restricted to ASCII alphanumerics.
- URL, retention, code, and request-body sizes are bounded.
- API-key comparison is constant-time. Protected operations return no body on
authentication failure.
- Per-IP sliding-window throttling applies to API calls and redirects. Embedded
frontend files do not consume API quota.
- Client identity uses `X-Real-IP`, then the first `X-Forwarded-For` value,
then the TCP peer. Bind the container port to loopback and let only a trusted
reverse proxy overwrite those headers.
- CORS preflight uses `OPTIONS` and does not consume rate-limit quota.
- The runtime Compose definitions use a read-only root filesystem, drop Linux
capabilities, enable `no-new-privileges`, and run as a numeric non-root user.
- The runtime image contains no shell, package manager, Python runtime, or
external static files.
- `SIGINT` and `SIGTERM` cleanly unblock the server for prompt shutdown.
Keep `api_key` out of Git, image layers, command output, and monitoring labels.
The supplied nginx configurations forward trusted proxy headers and keep the
application port bound to `127.0.0.1`.