Self-hosted „Link in Bio"-Plattform auf Basis von LinkStack (PHP/Laravel, AGPL-3.0) — als reproduzierbare Container-Solution mit CI/CD, Infrastructure-as-Code und eigenem, ins Image gebackenem Theme-Provisioning (Corporate-Theme + kuratierte Themes) verpackt.
Öffentliche Seite + Admin:
https://links.bauer-group.com(Admin unter/admin)
- Ein einziges Image —
FROM linkstackorg/linkstack(Alpine + Apache2 + PHP 8.3), erweitert um Theme-Provisioning und einen Wrapper-Entrypoint. - Themes ohne manuelle Uploads — das Corporate-Theme und alle kuratierten
Stock-Themes werden ins Image gebacken und beim Boot idempotent in das
/htdocs-Volume gespiegelt („bake-and-sync"). Kein Klicken im Admin-UI mehr. - BAUER GROUP Corporate-Theme — vollständig gescaffoldetes Theme V2 mit zentralen CSS-Variablen; Marken-Farben/Fonts/Footer sind ein Ein-Datei-Swap.
- SQLite — keine separate Datenbank, das gesamte
/htdocs-Volume ist die Backup-Einheit. - Reverse-Proxy-ready — Traefik v3 (HTTP→HTTPS, Security-Header, Let's Encrypt)
oder Coolify;
FORCE_HTTPSwird optional automatisch in/htdocs/.envgesetzt. - IaaC & CI/CD — Compose-Triplet, semantic-release, GHCR + Docker Hub,
Base-Image-Digest-Monitor, Dependabot — delegiert an
bauer-group/automation-templates.
┌──────────────────────────────────────────┐
Internet ──HTTPS──► │ Reverse Proxy (Traefik v3 / Coolify) │
│ TLS-Terminierung, Security-Header │
└───────────────────┬──────────────────────┘
│ HTTP :80
┌───────────────────▼──────────────────────┐
│ linkstack (Apache + PHP 8.3) │
│ ┌────────────────────────────────────┐ │
Image-intern │ │ Entrypoint: bauergroup-provision.sh │ │
/opt/linkstack/ │ │ 1. Theme-Sync ─► /htdocs/themes │ │
themes/ ───sync───► │ │ 2. .env-Bridge (FORCE_HTTPS) │ │
(bake-and-sync) │ │ 3. exec docker-entrypoint.sh │ │
│ └────────────────────────────────────┘ │
│ Volume: /htdocs │
│ (SQLite-DB, Uploads, Themes, .env) │
└──────────────────────────────────────────┘
Warum bake-and-sync? /htdocs ist ein gemountetes Named-Volume. Themes, die
direkt nach /htdocs/themes/ ins Image kopiert werden, würden ab dem zweiten Boot
vom Volume verdeckt (Docker befüllt nur ein leeres Volume aus dem Image).
Deshalb werden die Themes image-intern unter /opt/linkstack/themes/ abgelegt und
vom Entrypoint bei jedem Boot in das Volume gespiegelt — idempotent, überschreibt
nur die gebündelten Themes, löscht niemals selbst hochgeladene Themes.
cp .env.example .env
# LINKSTACK_DOMAIN in .env setzen (für dev optional)
docker compose -f docker-compose.development.yml up -d --build
# → http://localhost:8080 → Setup-Wizard: SQLite (Default) wählen, Admin anlegen| Variante | Datei | Edge / TLS |
|---|---|---|
| Traefik (Production) | docker-compose.traefik.yml |
Selbstverwaltetes Traefik v3, Let's Encrypt |
| Coolify (Production) | docker-compose.coolify.yml |
Coolify verwaltet Routing/TLS (keine traefik.*-Labels) |
| Development | docker-compose.development.yml |
Direkter Port, kein HTTPS — nicht ins Internet stellen |
# Traefik
docker compose -f docker-compose.traefik.yml up -d
# Coolify: Compose-Datei im Coolify-UI hinterlegen, Domain im Dashboard setzenAlle Werte sind in .env.example dokumentiert. Es gibt keine
Pflicht-Secrets (SQLite; der Admin-Account entsteht im Wizard).
| Variable | Zweck | Default |
|---|---|---|
STACK_NAME |
Prefix für Container/Volume/Netzwerk | linkstack |
LINKSTACK_DOMAIN |
Öffentliche Domain (muss zur DNS passen) | links.bauer-group.com |
LINKSTACK_IMAGE_TAG |
GHCR-Image-Tag (stable/latest/SemVer) | stable |
LINKSTACK_VERSION |
Upstream-Base-Image (Build-Arg, dev) | latest |
PHP_MEMORY_LIMIT / UPLOAD_MAX_FILESIZE |
PHP-Tuning | 512M / 8M |
LINKSTACK_MANAGE_ENV |
Config-Bootstrap in /htdocs/.env aktiv (Merge) |
true (prod) |
LINKSTACK_FORCE_HTTPS |
Setzt FORCE_HTTPS hinter Proxy |
true (prod) |
LINKSTACK_DISPLAY_CREDIT |
„Powered by LinkStack" anzeigen | false (aus) |
LINKSTACK_SMTP_* |
SMTP-Bootstrap (nur wenn _HOST gesetzt) |
— |
PROXY_NETWORK |
Externes Traefik-Netz | EDGEPROXY |
IP_WHITELIST |
Traefik IP-Allow-List (Default: alle) | 0.0.0.0/0,::/0 |
- Gebündelte Themes liegen im Repo unter
app/linkstack/themes/:bauer-group/— das Corporate-Theme (Theme V2).vendor/<name>-<version>/— kuratierte Stock-Themes, unzip-vendored.themes.lock.json— Provenienz (SHA-256 der Quell-Zips) für reproduzierbare Builds.
- Warum ein Sync? Die Themes sind ins Image gebacken — aber unter
/opt/linkstack/themes/, nicht unter/htdocs/themes/, wo LinkStack sie liest./htdocsist ein persistentes Volume; Docker befüllt es nur beim ersten Anlegen aus dem Image, spätere Image-Updates würden verdeckt. Der Entrypoint kopiert die gebündelten Themes daher bei jedem Boot ins Volume: Image-verwaltete Themes werden aktualisiert, selbst hochgeladene Themes bleiben unangetastet. Kein Marker, kein Schalter — deterministisch.
Das Theme definiert nur das Aussehen (Palette + Font-Stack), in
app/linkstack/themes/bauer-group/. Die BAUER GROUP Palette (Orange #FF8500 +
Warm-Gray, hell/dunkel, WCAG-AA Link-Text) ist in extra/custom-head.blade.php
und skeleton-auto.css gesetzt; Font = system-ui-Stack (keine Webfonts). Bei
Marken-Änderungen die Werte dort anpassen, version in themes.lock.json +
readme.md erhöhen und Image neu bauen.
Bei LINKSTACK_MANAGE_ENV=true (prod-Default) merged der Entrypoint bei jedem Boot
eine kuratierte Auswahl an Keys in LinkStacks /htdocs/.env — nur diese Keys,
alle anderen (APP_KEY, Admin-Settings, …) bleiben unverändert. LinkStack cached
Config nicht, die Werte greifen sofort. Verwaltet werden:
FORCE_HTTPS(hinter TLS-Proxy) — ausLINKSTACK_FORCE_HTTPS.- „Powered by LinkStack"-Credit auf allen Seiten —
DISPLAY_CREDIT+DISPLAY_CREDIT_FOOTERausLINKSTACK_DISPLAY_CREDIT(Default: aus). - SMTP —
MAIL_*ausLINKSTACK_SMTP_*(nur wennLINKSTACK_SMTP_HOSTgesetzt ist; Passwörter werden korrekt escaped/gequotet).
Booleans werden unquoted geschrieben (Laravel-Cast), Strings gequotet.
LINKSTACK_MANAGE_ENV=false übergibt die volle Kontrolle ans Admin-Panel.
Bleibt im Daten-Volume und wird im Admin gesetzt (via linkstack-backup.py
gesichert):
- Avatar / Profil-Logo: BAUER GROUP Logo als Profilbild hochladen (Studio → Appearance). Pro Benutzer, im Volume gespeichert.
- Footer-Links (Impressum / Datenschutz): Admin → Settings → Footer
(
DISPLAY_FOOTER_*); Labels/Ziele im EnvEditor editierbar.
Theme-Ordner unter app/linkstack/themes/vendor/<name-version>/ ablegen, Eintrag
(inkl. SHA-256 des Quell-Zips) in themes.lock.json ergänzen, Image neu bauen.
Nutzer können zusätzlich jederzeit eigene Themes über das Admin-UI hochladen —
diese bleiben beim Resync erhalten.
Da SQLite genutzt wird, liegt der komplette Zustand (DB, Uploads, Themes, .env)
im Volume ${STACK_NAME}-data (/htdocs).
# Snapshot des Volumes erstellen
python scripts/linkstack-backup.py backup
# Wiederherstellen
python scripts/linkstack-backup.py restore linkstack-backup-YYYYmmdd-HHMMSS.tar.gzUpgrade (Container-Weg, empfohlen statt In-App-Updater): vorher Backup ziehen,
LINKSTACK_VERSION/LINKSTACK_IMAGE_TAG anheben, neu bauen/pullen, neu starten.
Der Entrypoint spiegelt die gebündelten Themes beim Boot neu ein; von Nutzern
hochgeladene Themes bleiben erhalten.
- HTTPS ist Pflicht: hinter dem Proxy
LINKSTACK_FORCE_HTTPS=true(Default in prod) — der Entrypoint schreibtFORCE_HTTPSnach/htdocs/.env. - Echte Client-IP hinter dem Proxy: Apache
mod_remoteipist aktiviert und vertraut den internen Proxy-Ranges, sodassX-Forwarded-ForinREMOTE_ADDRlandet. Wichtig, weil LinkStack seinen Login-Brute-Force-Schutz (email|ip) und das API-Rate-Limit (60/min pro IP) an der Client-IP festmacht — ohne den Fix teilen sich alle Requests die Proxy-IP und die Limits kollabieren. Port 80 ist nur im Docker-Netz erreichbar (kein Host-Port), daher istX-Forwarded-Fornicht von außen fälschbar (sieheapp/linkstack/conf/remoteip.conf). - Container läuft non-root (
apache:apache, aus dem Upstream-Image). - Traefik-Variante liefert HSTS,
X-Frame-Options: SAMEORIGIN,nosniff, Referrer-Policy sowie eine optionale IP-Allow-List. Hinweis: dieipallowlist-Middleware nutzt die direkte Verbindungs-IP zu Traefik (korrekt, wenn Traefik am Edge steht); hinter einem zusätzlichen L4/L7-LB dieipStrategy.depthsetzen. - Nach dem Setup: Default-Passwort ändern. Der Wizard legt den Admin an;
Upstream-Demos nennen teils
root/password— nicht produktiv verwenden. - Keine Secrets im Repo (SQLite,
.envist gitignored).
CS-LinkStack/
├── app/linkstack/
│ ├── Dockerfile # FROM linkstackorg/linkstack + Themes + Entrypoint
│ ├── .dockerignore
│ ├── rootfs/usr/local/bin/bauergroup-provision.sh # Wrapper-Entrypoint (bake-and-sync)
│ └── themes/
│ ├── themes.lock.json # Provenienz (SHA-256) — reproduzierbare Builds
│ ├── bauer-group/ # Corporate-Theme (Theme V2)
│ └── vendor/ # kuratierte Stock-Themes (unzipped)
├── scripts/linkstack-backup.py # Volume-Snapshot/-Restore
├── docker-compose.traefik.yml # Production (Traefik v3)
├── docker-compose.coolify.yml # Production (Coolify)
├── docker-compose.development.yml # Dev (lokaler Build, direkter Port)
├── .env.example # Konfigurations-Contract (keine Secrets)
├── .github/ # CI/CD (automation-templates), Dependabot, CODEOWNERS
├── LICENSE # MIT (Wrapper)
└── NOTICE # Drittanbieter-Lizenzen (AGPL/GPLv3/MIT)
Der Wrapper (dieses Repository) steht unter der MIT-Lizenz. Das erzeugte Image bündelt LinkStack (AGPL-3.0) sowie Themes unter MIT/GPLv3 — Details und Hinweise (inkl. zweier Themes mit Non-Commercial-Einschränkung) in NOTICE.