7.1 KiB
Shorty — URL shortener + QR designer stack
Production-ready, self-hosted stack:
- Kutt for link shortening: short links at
https://mifi.me, admin UI athttps://link.mifi.me - QR Designer at
https://qr.mifi.dev: styled QR codes with optional Kutt shortening, logo upload, export (SVG, PNG, PDF). Protected by Traefik BasicAuth.
Designed for Docker/Portainer with Traefik. Uses pnpm everywhere; no Tailwind.
Prerequisites
- Traefik with:
- External network
marina-net(create withdocker network create marina-netif needed) - Cert resolver (e.g.
letsencryptorlets-encrypt— adjust labels indocker-compose.ymlto match your Traefik)
- External network
- DNS: A records for
mifi.me,link.mifi.me,qr.mifi.devpointing to the host running Traefik - Bind mount paths on the host (create if missing):
/mnt/config/docker/kutt/postgres— Kutt Postgres data/mnt/config/docker/kutt/redis— Kutt Redis data/mnt/config/docker/qr/db— qr-api SQLite directory/mnt/config/docker/qr/uploads— qr-api uploads (logos)
Kutt setup
- Deploy the stack (see below). On first run, open
https://link.mifi.meand create an admin account. - In Kutt admin: Settings → API (or Account → API), create an API key.
- Set
KUTT_API_KEYin the environment for qr-api (and optionally for local dev). The QR app uses this to shorten URLs via the backend; qr-api is not exposed publicly.
Deploy (Portainer)
Option A — Registry + webhook (recommended for CI/CD)
Use prebuilt images and redeploy on push via webhook:
- In Portainer: Stacks → Add stack. Use docker-compose.portainer.yml (paste or pull from repo).
- Set env vars:
- Required:
DB_PASSWORD,JWT_SECRET - Optional:
REGISTRY(defaultgit.mifi.dev),IMAGE_TAG(defaultlatest),KUTT_API_KEY
- Required:
- Deploy. Then in the stack: Webhooks → add webhook. Copy the URL and add it as secret
portainer_webhook_urlin Woodpecker (repo secrets). On each push tomain, the pipeline builds multi-arch images, pushes to the registry, and triggers this webhook to redeploy the stack.
Option B — Build from source
For one-off or local deploys without CI:
- In Portainer: Stacks → Add stack. Use docker-compose.yml (builds qr-api and qr-web from Dockerfiles).
- Set required env vars:
DB_PASSWORD,JWT_SECRET, and optionallyKUTT_API_KEY. - Deploy. No ports are exposed; Traefik handles ingress.
Env vars and .env.example
Copy .env.example to .env and set values for Docker Compose / production:
- DB_PASSWORD (required) — Postgres password for Kutt
- JWT_SECRET (required) — Kutt JWT secret (use a long random string)
- KUTT_API_KEY (optional) — Kutt API key for qr-api shorten feature (create in Kutt UI first)
For local dev inside the devcontainer, env vars for qr-api (DB_PATH, UPLOADS_PATH, KUTT_API_KEY, QR_API_URL) are set in .devcontainer/devcontainer.json so you don’t need a .env file to run qr-api and qr-web with pnpm.
Local run (Docker Compose)
From repo root, after copying .env.example to .env and setting values:
docker compose up -d
For local dev without Traefik, you can add a ports override for qr_web (e.g. 3000:3000) and access the QR app at http://localhost:3000. Kutt would need its own ports if you want to test shortening locally.
Development with Devcontainer
Yes — run locally inside the devcontainer. The devcontainer is the intended environment for development and testing.
- Open the repo in VS Code/Cursor and use Dev Containers: Reopen in Container (or Codespaces).
pnpm installruns automatically. Env vars for qr-api are set indevcontainer.json(DB_PATH,UPLOADS_PATH,KUTT_API_KEY,QR_API_URL) so you can run qr-api and qr-web without a.envfile.- In the container, start the apps:
- qr-api:
pnpm --filter qr-api dev(listens on 8080) - qr-web:
pnpm --filter qr-web dev(listens on 3000)
- qr-api:
- Open the forwarded ports (3000 = qr-web, 8080 = qr-api). Data and uploads are stored under
.data/in the repo (gitignored). - For full stack (Kutt + qr-api + qr-web in Docker), run
docker compose upfrom the host (or from inside the container if Docker-in-Docker is enabled). SetDB_PASSWORD,JWT_SECRET, and optionallyKUTT_API_KEYin.envfor that.
Ports 3000 and 8080 are forwarded by the devcontainer.
Repo structure
docker-compose.yml— Compose that builds qr-api and qr-web from source (local or one-off Portainer).docker-compose.portainer.yml— Compose that uses registry images; for Portainer + CI/CD webhook redeploys.qr-api/— Node/TS Express API: SQLite projects, uploads, shorten proxy to Kutt. Not exposed via Traefik.qr-web/— Next.js (App Router) + Mantine QR designer; proxies all API calls to qr-api server-side..woodpecker/ci.yml— CI: install, format check, lint, test, build on PR/push/tag..woodpecker/deploy.yml— Deploy: build qr-api and qr-web (multi-arch amd64/arm64), push to registry, trigger Portainer webhook. Runs on push/tag tomainafter CI..devcontainer/— Devcontainer for local dev.
Security
- qr-api is only on the
backendnetwork; only qr-web (and other backend services) can reach it. No Traefik router for qr-api. - qr-web is exposed at
qr.mifi.devwith Traefik BasicAuth (htpasswd usermifi). Set your own password and update the middleware label if needed. - Kutt is public at
mifi.meandlink.mifi.me; use Kutt’s own auth (admin account, API keys).
qr-border-plugin (optional)
The QR designer uses qr-code-styling for dots, corners, colors, and error correction. The optional qr-border-plugin (from lefe.dev marketplace) adds border styling but depends on @lefe-dev/lefe-verify-license, which may involve licensing/watermark behavior. This stack uses qr-code-styling only by default; you can add qr-border-plugin from npm or GitHub if desired and document any license terms.
CI/CD (Woodpecker)
- CI (
.woodpecker/ci.yml): on every PR/push tomain/tag —pnpm install --frozen-lockfile, format check, lint, test, build. Requirespnpm-lock.yamlcommitted. - Deploy (
.woodpecker/deploy.yml): on push/tag tomainafter CI — buildsshorty-qr-apiandshorty-qr-webas multi-arch (linux/amd64, linux/arm64), pushes togit.mifi.dev/mifi-holdings/shorty-qr-apiandshorty-qr-web, then POSTs the Portainer webhook to redeploy the stack.
Secrets (repo or org): gitea_registry_username, gitea_package_token, portainer_webhook_url.
Local build/push test (before committing):
From repo root: build with buildx and push to your registry, or run docker compose -f docker-compose.yml build to verify builds.
Code style and tooling
- TypeScript everywhere.
- Prettier: tabWidth 4, spaces (no tabs), singleQuote, trailingComma all, semi.
- ESLint for TS/React/Next in qr-web; shared root config for qr-api.
- pnpm only;
pnpm-lock.yamlis committed and is the single lockfile (no package-lock.json or yarn.lock). - Tests: Vitest (qr-api and qr-web).