From 2c386efc9c5dbcc70f62deb883ec5e6824fe2535 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 11 May 2026 02:59:53 +0200 Subject: [PATCH] Initial commit: Shlink docker compose stack Self-hosted Shlink URL shortener with the official web dashboard, backed by PostgreSQL 16. Designed to sit behind a reverse proxy that terminates TLS. - shlink server exposed on host port 8500 - shlink-web-client dashboard exposed on host port 8501 - postgres 16 kept internal, persisted via named volume - dashboard preconfigured via SHLINK_PUBLIC_URL + INITIAL_API_KEY - .env.example documents required variables; .env is gitignored Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 27 +++++++++++++++ .gitignore | 1 + README.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 56 +++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..af3942e --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# Domain used to build short URLs (no scheme) +DEFAULT_DOMAIN=s.example.com + +# Set to false if your reverse proxy does NOT terminate TLS +IS_HTTPS_ENABLED=true + +# Public URL the dashboard uses to reach the Shlink API (with scheme) +SHLINK_PUBLIC_URL=https://s.example.com + +# Initial API key created on first boot — also used to preconfigure the dashboard +# Generate with: openssl rand -hex 32 +INITIAL_API_KEY=change-me-to-a-long-random-string + +# Optional MaxMind GeoLite2 license key for IP geolocation +GEOLITE_LICENSE_KEY= + +# Timezone +TIMEZONE=UTC + +# Postgres +POSTGRES_DB=shlink +POSTGRES_USER=shlink +POSTGRES_PASSWORD=change-me + +# Host ports exposed to the reverse proxy +SHLINK_SERVER_PORT=8500 +SHLINK_DASHBOARD_PORT=8501 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..83002a8 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Shlink Docker Compose + +Self-hosted [Shlink](https://shlink.io) URL shortener with the official web dashboard, backed by PostgreSQL. Intended to run behind a reverse proxy that terminates TLS. + +## Stack + +| Service | Image | Container port | Default host port | +|---|---|---|---| +| `shlink` | `shlinkio/shlink:stable` | `8080` | `8500` | +| `shlink-dashboard` | `shlinkio/shlink-web-client:stable` | `8080` | `8501` | +| `shlink-db` | `postgres:16-alpine` | `5432` | not exposed | + +## Quick start + +```bash +cp .env.example .env +# Edit .env — set DEFAULT_DOMAIN, SHLINK_PUBLIC_URL, INITIAL_API_KEY, POSTGRES_PASSWORD +docker compose up -d +``` + +Generate strong secrets: + +```bash +openssl rand -hex 32 # INITIAL_API_KEY +openssl rand -hex 24 # POSTGRES_PASSWORD +``` + +## Environment variables + +| Variable | Required | Description | +|---|---|---| +| `DEFAULT_DOMAIN` | yes | Bare host used to build short URLs (e.g. `s.example.com`). No scheme. | +| `SHLINK_PUBLIC_URL` | yes | Full public URL of the Shlink API (e.g. `https://s.example.com`). Used by the dashboard. | +| `INITIAL_API_KEY` | yes | API key created on first boot. Also preconfigures the dashboard. | +| `POSTGRES_PASSWORD` | yes | PostgreSQL password. | +| `IS_HTTPS_ENABLED` | no | `true` (default) if your reverse proxy terminates TLS. Set `false` for plain HTTP. | +| `GEOLITE_LICENSE_KEY` | no | MaxMind GeoLite2 license key for IP geolocation. | +| `TIMEZONE` | no | Defaults to `UTC`. | +| `POSTGRES_DB` | no | Defaults to `shlink`. | +| `POSTGRES_USER` | no | Defaults to `shlink`. | +| `SHLINK_SERVER_PORT` | no | Host port for the Shlink API. Defaults to `8500`. | +| `SHLINK_DASHBOARD_PORT` | no | Host port for the dashboard. Defaults to `8501`. | + +## Reverse proxy + +Two hostnames recommended: + +- `s.example.com` → `http://127.0.0.1:8500` — Shlink API and short URL redirects. +- `shlink-admin.example.com` → `http://127.0.0.1:8501` — Dashboard UI. + +The proxy must terminate TLS and forward `X-Forwarded-Proto` / `X-Forwarded-For` so Shlink renders `https://` short URLs and logs the correct client IP. + +`DEFAULT_DOMAIN` must match the hostname your proxy serves the API on, otherwise generated short URLs will be wrong. + +## Persistence + +Two named volumes: + +- `shlink-data` — Shlink cache and runtime files. +- `shlink-db-data` — PostgreSQL data directory. + +Back both up before upgrades. + +## Common operations + +```bash +docker compose logs -f shlink # follow server logs +docker compose exec shlink shlink --help # CLI inside the container +docker compose pull && docker compose up -d # upgrade images +docker compose down # stop (data preserved) +``` + +Create an additional API key: + +```bash +docker compose exec shlink shlink api-key:generate +``` + +## Notes + +- `INITIAL_API_KEY` only takes effect on the very first boot, before the database is initialized. Changing it later does nothing — use `api-key:generate` instead. +- The dashboard ships preconfigured with one server pointed at `SHLINK_PUBLIC_URL`. The browser must be able to reach that URL; it is not a server-to-server call. +- Postgres is intentionally not exposed on the host. Connect via `docker compose exec shlink-db psql -U shlink`. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a74d000 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +services: + shlink: + image: shlinkio/shlink:stable + container_name: shlink + restart: unless-stopped + depends_on: + shlink-db: + condition: service_healthy + ports: + - "${SHLINK_SERVER_PORT:-8500}:8080" + environment: + DEFAULT_DOMAIN: ${DEFAULT_DOMAIN} + IS_HTTPS_ENABLED: ${IS_HTTPS_ENABLED:-true} + GEOLITE_LICENSE_KEY: ${GEOLITE_LICENSE_KEY:-} + INITIAL_API_KEY: ${INITIAL_API_KEY} + DB_DRIVER: postgres + DB_HOST: shlink-db + DB_PORT: 5432 + DB_NAME: ${POSTGRES_DB:-shlink} + DB_USER: ${POSTGRES_USER:-shlink} + DB_PASSWORD: ${POSTGRES_PASSWORD} + TIMEZONE: ${TIMEZONE:-UTC} + volumes: + - shlink-data:/etc/shlink/data + + shlink-db: + image: postgres:16-alpine + container_name: shlink-db + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-shlink} + POSTGRES_USER: ${POSTGRES_USER:-shlink} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - shlink-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-shlink} -d ${POSTGRES_DB:-shlink}"] + interval: 10s + timeout: 5s + retries: 5 + + shlink-dashboard: + image: shlinkio/shlink-web-client:stable + container_name: shlink-dashboard + restart: unless-stopped + depends_on: + - shlink + ports: + - "${SHLINK_DASHBOARD_PORT:-8501}:8080" + environment: + SHLINK_SERVER_URL: ${SHLINK_PUBLIC_URL} + SHLINK_SERVER_API_KEY: ${INITIAL_API_KEY} + +volumes: + shlink-data: + shlink-db-data: