Tree-docs guidebook under vibe/guidebooks/applications/ documenting the common app pattern and two contrasting archetypes, drilling into lab-ecosystem/01-factory (bidirectional): - README.md : the shared app pattern (repo = Dockerfile + chart + optional iac + CI; ArgoCD app-of-apps; the <app> join key; .fr vs .lab ingress conventions) + a two-archetype comparison. - webapp.md : canonical Go + Postgres exemplar (chart, VaultAuth/Static/Dynamic CRDs, inline iac vs the shared app_roles module, CI); notes the current nuance that the live pod still uses the static pgbouncer_auth DATABASE_URL. - url-shortener.md : Rust + SQLite-on-Longhorn-RWO counterpart (single replica, no iac/no Vault, CI mirrors the upstream image); the power-cut recovery story. erp is referenced in prose only (its own guidebook lands next). Sibling-repo code via full gitea URLs; 2 mermaid diagrams MCP-validated; zero dead links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
vibe > Guidebooks > Applications > url-shortener
url-shortener (Chhoto URL)
Status: ✅ active · Last Updated: 2026-06-23 Upstream: Applications index · lab-ecosystem hub Downstream: storage-and-recovery · Ansible recover playbooks Related: webapp (the stateless counterpart) · naming-conventions · secrets-and-vault
url-shortener is the lab's stateful application — the deliberate mirror image of webapp. Where webapp is a horizontally-scalable, Postgres-backed, Vault-credentialed reference app, url-shortener is a single-pod, SQLite-on-a-disk service that proves the lab can run a genuinely stateful workload on Longhorn block storage and recover it after a crash.
The application itself is Chhoto URL, a tiny Rust/actix-web URL shortener, mirrored into the lab from the upstream project sintan1729/chhoto-url. The lab does not fork the source — it mirrors the published Docker image (see §5 CI) — but keeps a copy of the source and a custom Helm chart in the url-shortener repo so the lab owns its packaging.
Note
A second public web app, the erp application, exists in the ecosystem but does not yet have a guidebook page; it is mentioned here in prose only and will be cross-linked once its guidebook ships.
1. The app & image
Chhoto URL is a single self-contained binary: an actix-web HTTP server with a bundled SQLite engine and a plain static frontend. There is no separate database process and no application runtime to install.
| Aspect | Detail | Source |
|---|---|---|
| Language / framework | Rust, actix-web 4.5.x |
actix/Cargo.toml |
| Database driver | rusqlite with the bundled feature — SQLite is compiled into the binary |
Cargo.toml |
| Sessions | actix-session cookie store; the session key is regenerated at boot, so a restart invalidates all logins |
actix/src/main.rs |
| Frontend | Plain HTML/CSS/JS served by actix-files from resources/ (index.html, static/styles.css, static/script.js, static/404.html) — no build step, no SPA framework |
resources/ |
| Listen port | 4567 (overridable via the port env var) |
actix/src/main.rs |
| Slug generation | Adjective-name Pair or UID styles; the lab forces UID (see §3) |
actix/src/utils.rs |
Image build
| Image | Base / approach | Result | File |
|---|---|---|---|
Dockerfile |
cargo-chef dependency caching → musl static build (x86_64-unknown-linux-musl) → FROM scratch, copying only the binary + resources/ |
A single-arch, ~6 MB image with no OS, shell, or libc | Dockerfile |
Dockerfile.multiarch |
Per-arch FROM scratch stages selected by TARGETARCH, copying pre-built musl binaries |
amd64 / arm64 / armv7 images from a local cross-compile |
Dockerfile.multiarch |
compose.yaml |
Pulls the upstream sintan1729/chhoto-url:latest, binds 4567, mounts a db volume at the SQLite path |
Local-dev only — not the deployed topology | compose.yaml |
The FROM scratch design is what makes the storage story so stark: the container has nothing except the SQLite file on its mounted volume. All durable state is one file.
2. Storage — the key contrast
This is the section that distinguishes url-shortener from every other lab app. The entire database is a single SQLite file, /data/urls.sqlite, living on a Longhorn PersistentVolumeClaim.
| PVC field | Value | Why it matters |
|---|---|---|
accessModes |
ReadWriteOnce (RWO) |
Only one node can mount the volume at a time |
resources.requests.storage |
128Mi |
A URL table is tiny; this is generous |
storageClassName |
longhorn |
Replicated block storage; the source of durability |
annotations |
helm.sh/resource-policy: keep |
The PVC (and its data) survives helm uninstall |
| Mount | /data → SQLite at /data/urls.sqlite |
Set by the ConfigMap db_url (§3) |
Source: chart/templates/pvc.yaml and the volume mount in chart/templates/deployment.yaml.
Because the volume is RWO, replicaCount is hard-pinned to 1. A second pod cannot mount the same volume, so there is no HA and no rolling update — the next pod can only start after the previous one has released the disk. This is the exact data shape that was reconstructed during the 2026-04-13 power-cut incident via Longhorn block-device recovery (see §6).
3. The chart
url-shortener ships its own Helm chart at chart/. It is deployed by ArgoCD like every other lab app, following the naming conventions.
| Concern | Setting | Source |
|---|---|---|
| Image | gitea.arcodange.lab/arcodange-org/url-shortener, tag defaults to .Chart.AppVersion (6.5.3) |
values.yaml, Chart.yaml |
| Service | ClusterIP, port 4567 |
service.yaml |
| Internal ingress | Host url.arcodange.lab on websecure; TLS via cert-manager StepClusterIssuer (step-issuer); localIp@file middleware (LAN-only) |
values.yaml |
| Public ingress | Host derived from the internal host by the .lab → .fr substitution in _helpers.tpl; PathRegexp matcher (/[^/]+) so it intercepts shortlink redirects; no localIp (it carries the crowdsec middleware instead, since the public path must be reachable) |
public-ingress.yaml, _helpers.tpl |
| ConfigMap | db_url=/data/urls.sqlite, site_url = the .fr FQDN, slug_style=UID, slug_length=4 |
config.yaml |
| Probes | Liveness + readiness HTTP GET / on the http port |
values.yaml |
| Autoscaling | Disabled. maxReplicas > 1 would fail under RWO (a second pod cannot mount the volume) |
hpa.yaml, values.yaml |
Note
The
_helpers.tplurl-shortener.fqdntemplate builds the publicsite_urlby taking the first internal ingress host and runningreplace ".lab" ".fr"— so the same value drives the internalurl.arcodange.laband the public.frredirect domain. This is the same.lab ↔ .frsplit documented in naming-conventions.
4. No iac/, no Vault — a deliberate deviation
url-shortener has no iac/ directory and no Vault CRDs, and this is intentional, not an oversight.
| Convention (see webapp) | Why url-shortener skips it |
|---|---|
iac/ OpenTofu module declaring a Postgres role |
There is no Postgres. SQLite is embedded; there is no database server to provision. |
Vault app_roles + VSO-synced dynamic credentials |
There are no credentials to issue — the app talks to a local file, not a networked database. See secrets-and-vault for the pattern url-shortener opts out of. |
For a SQLite-on-a-file app, the Helm chart is the IaC: the PVC, the ConfigMap, and the ingress fully describe the deployable surface. There is no second provisioning tier. Compare with webapp, where the Postgres role and Vault role are first-class infrastructure objects managed outside the chart.
5. CI — mirror, not build
The single workflow, .gitea/workflows/dockerimage.yaml, does not build the local actix/ source. It mirrors the upstream image.
| Step | Action |
|---|---|
| 1. Discover version | wget the upstream Cargo.toml from SinTan1729/chhoto-url on GitHub and parse version |
| 2. Login | Authenticate to gitea.arcodange.lab using the PACKAGES_TOKEN secret |
| 3. Pull | docker pull sintan1729/chhoto-url:<tag> from Docker Hub (latest and the discovered version) |
| 4. Retag | docker tag to gitea.arcodange.lab/<repo>:<tag> |
| 5. Push | docker push both latest and the version tag to the Gitea registry |
So the in-repo actix/ source and the Dockerfile/Dockerfile.multiarch exist for reference and local cross-compilation (Makefile targets build-release / docker-release use cross for amd64 / arm64 / armv7), but the cluster runs the mirrored upstream image. The multi-arch build is a manual, local-developer flow — it is not run in CI.
6. Recovery
url-shortener's SQLite-on-RWO is the canonical example that the lab's recovery tooling targets. When a node dies mid-write or the cluster loses power, the durable artifact to reconstruct is exactly this kind of Longhorn block volume.
- The concept and policy live in storage-and-recovery.
- The mechanics live in the Ansible recover playbooks: the
longhorn_data.ymlblock-device recovery flow reconstructs precisely this single-file SQLite volume, which is what was recovered in the 2026-04-13 power-cut.
Warning
Single replica + RWO means downtime on any pod move: a node drain, an upgrade, or a reschedule cannot overlap the old and new pods — the new one waits for the disk to detach. There is no application-level redundancy. The only durability is Longhorn volume replication plus backups; if the volume is lost and unbacked, the URL database is gone. Treat backup health as the single point of failure for this app.
7. Deviations from convention (vs webapp)
A side-by-side of where url-shortener (stateful) departs from webapp (the stateless reference):
| Dimension | webapp | url-shortener |
|---|---|---|
| Database | PostgreSQL (external server) | SQLite (embedded, single file) |
| Vault / secrets | app_roles + VSO-synced dynamic creds |
None — no networked credentials |
iac/ directory |
Yes (Postgres role, Vault role) | None — the Helm chart is the IaC |
| Replicas | Scalable (HPA-eligible) | 1, hard-pinned (RWO forbids more) |
| Rolling update / HA | Yes | No — single pod, downtime on move |
| CI | Builds the app source | Mirrors the upstream image |
| Recovery shape | Postgres backup/restore | Longhorn block-device recovery |
8. Deploy + storage path
%%{init: {'theme':'base'}}%%
flowchart LR
upstream["Docker Hub<br>sintan1729/chhoto-url"]
ci["Gitea Actions<br>dockerimage.yaml (mirror)"]
reg["Gitea registry<br>gitea.arcodange.lab/<br>arcodange-org/url-shortener"]
argo["ArgoCD + Helm chart"]
pod["Pod (replicaCount 1)<br>actix on :4567"]
pvc["Longhorn PVC (RWO, 128Mi)<br>keep policy"]
db["/data/urls.sqlite"]
upstream -- "pull + retag" --> ci
ci -- "push latest + version" --> reg
reg -- "image ref" --> argo
argo -- "deploy" --> pod
pod -- "mounts (RWO)" --> pvc
pvc -- "holds" --> db
classDef box fill:#1f2933,stroke:#7b8794,color:#f5f7fa;
class upstream,ci,reg,argo,pod,pvc,db box;
- Upstream — the canonical
sintan1729/chhoto-urlimage is published to Docker Hub by the original maintainer. - CI mirror — the lab's
dockerimage.yamlworkflow pulls that image, retags it, and pushes it to the Gitea registry (it does not build fromactix/). - Gitea registry —
gitea.arcodange.lab/arcodange-org/url-shortenerholds bothlatestand the version tag. - ArgoCD + Helm — the chart references the registry image (tag defaults to
appVersion) and renders the Deployment, Service, ingresses, ConfigMap, and PVC. - Pod — a single
actixpod listens on4567; HPA and rolling updates are off. - Longhorn PVC — the pod mounts the RWO volume at
/data; only one pod can hold it. - SQLite file — all durable state is the single
/data/urls.sqlitefile, which is what Longhorn block-device recovery reconstructs.
See also: webapp (the stateless, Postgres-backed contrast) · Applications index · naming-conventions · storage-and-recovery.