organize deployment configs and documentation
This commit is contained in:
+80
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user