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
+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`.