Shlink과 Docker Compose로 URL 단축기 구축
VPS에서 Shlink 5.1과 shlink-web-client 4.8을 Docker Compose로 운영하는 방법을 설명합니다. DNS, Postgres, API key, HTTPS, QR code와 클릭 통계를 설정합니다.
구축할 항목
셀프 호스팅 URL 단축기는 긴 링크를 사용자가 소유한 짧은 링크로 변환하고, 해당 링크의 모든 클릭을 집계하는 소규모 서버입니다. Shlink을 선택하는 것이 좋습니다. 오픈 소스이며 Docker 이미지로 릴리스되고, 데이터베이스와 함께 하나의 컨테이너로 전체 기능을 제공합니다. 이 가이드에서는 실제 단축 도메인을 사용하고 HTTPS, API key, QR code 및 클릭 통계를 지원하도록 VPS에서 Shlink을 배포합니다.
상용 단축기처럼 작동하게 하는 구성 요소는 2개입니다. API server는 redirect 요청에 응답하고 데이터를 저장합니다. web client는 별도의 static app이며 브라우저에서 해당 API와 통신합니다. 두 구성 요소를 모두 실행하거나, API만 실행하고 command line에서 사용할 수 있습니다.
여기에 제시한 버전 번호는 2026년 7월 기준 최신 버전입니다. Shlink 5.1 및 shlink-web-client 4.8입니다.
먼저 짧은 도메인을 서버로 연결합니다
도메인이 제품입니다. s.example.com/abc123은 사람들이 보는 링크이므로 짧은 이름을 선택하고, 아무것도 설치하기 전에 도메인을 결정합니다. Shlink는 모든 단축 URL에 도메인을 저장합니다. 나중에 도메인을 변경하면 이미 배포한 모든 링크가 작동하지 않습니다.
짧은 도메인에 대해 DNS A 레코드 1개를 생성하고, 이를 VPS의 공용 IPv4 주소로 지정합니다. 서버에 IPv6이 있으면 AAAA 레코드도 추가합니다. 그런 다음 계속하기 전에 도메인이 올바르게 확인되는지 검증합니다.
dig +short s.example.com A출력에는 서버 주소가 표시되어야 합니다. 출력이 비어 있으면 레코드가 아직 전파되지 않은 것입니다. 이 상태에서는 이후의 모든 단계가 원인을 알기 어려운 방식으로 실패합니다. 확인되지 않는 이름에는 TLS (transport layer security) 인증서를 발급할 수 없기 때문입니다.
compose 파일
Shlink에는 데이터베이스가 필요합니다. 테스트에는 SQLite를 사용할 수 있지만, 계속 유지할 데이터에는 Postgres가 적합합니다. 방문 행이 누적되며, Postgres가 인덱스와 동시 쓰기를 더 잘 처리하기 때문입니다. 다음 내용을 /opt/shlink/compose.yaml에 저장합니다.
services:
shlink:
image: shlinkio/shlink:stable
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
DEFAULT_DOMAIN: s.example.com
IS_HTTPS_ENABLED: "true"
DB_DRIVER: postgres
DB_HOST: database
DB_NAME: shlink
DB_USER: shlink
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
- database
database:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: shlink
POSTGRES_USER: shlink
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- shlink_db:/var/lib/postgresql/data
web-client:
image: shlinkio/shlink-web-client:stable
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
volumes:
shlink_db:게시된 두 포트는 모두 127.0.0.1에 바인딩됩니다. 따라서 다음 섹션에서 reverse proxy를 구성하기 전까지는 인터넷에서 접근할 수 없습니다. Docker는 호스트 방화벽보다 우선하여 자체 포워딩 규칙을 적용합니다. 따라서 방화벽이 닫힌 것처럼 보여도 일반적인 8080:8080 행만 사용하면 앱이 외부에 노출됩니다. loopback 주소에 바인딩하면 이를 방지할 수 있습니다. 이 방식은 같은 방법으로 실행하는 모든 앱에 적용되며, VPS에서 Docker Compose를 사용하는 가이드에서 더 자세히 설명합니다.
데이터베이스 비밀번호는 compose 파일 옆의 .env 파일에서 가져옵니다. 따라서 YAML에 비밀번호가 기록되지 않습니다.
sudo mkdir -p /opt/shlink
printf 'DB_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee /opt/shlink/.env
sudo chmod 600 /opt/shlink/.env서비스를 시작하고 API가 실행되는 과정을 확인합니다.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlink처음 시작할 때 데이터베이스 마이그레이션이 실행되므로 이후 시작보다 시간이 더 걸립니다. 시작이 완료되면 서비스가 로컬에서 응답하는지 확인합니다.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/health200은 API가 실행 중이고 데이터베이스 연결이 작동한다는 의미입니다. 여기서 500가 발생하는 원인은 거의 항상 데이터베이스입니다. .env의 DB_PASSWORD이 Postgres를 생성할 때 사용한 값과 일치하지 않기 때문입니다. Postgres image는 빈 데이터 디렉터리를 초기화할 때만 POSTGRES_PASSWORD를 읽습니다. 이후에 비밀번호를 수정해도 volume을 삭제하고 다시 시작하기 전에는 적용되지 않습니다.
Shlink 앞에서 HTTPS 종료
Shlink은 포트 8080에서 일반 HTTP를 제공합니다. TLS는 reverse proxy에서 처리해야 하며, 중요한 설정은 원래 호스트 이름을 전달하는 것입니다. Shlink은 Host 헤더를 읽어 단축 코드가 속한 도메인을 결정합니다. 따라서 proxy가 이 값을 다시 작성하면 존재하는 링크에서도 404 응답이 반환되고, 방문 통계가 잘못된 도메인에 연결됩니다.
server {
server_name s.example.com;
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}그런 다음 인증서를 발급합니다. 갱신 timer를 포함한 전체 절차는 Ubuntu 24.04에서 nginx용 Certbot 가이드에 설명되어 있습니다.
sudo certbot --nginx -d s.example.comcompose 파일의 IS_HTTPS_ENABLED: "true"는 Shlink이 반환하는 단축 URL에 https://를 출력하도록 설정합니다. 이 설정만으로 TLS가 활성화되지는 않습니다. HTTPS proxy 뒤에서 false로 유지하면 API가 반환하는 모든 링크가 http:// 링크가 된 후 redirect되므로, 왕복이 한 번 더 발생하고 web client에서 잘못 표시됩니다.
API 키 생성
키가 없으면 API와 통신할 수 없습니다. 컨테이너 내부의 CLI를 통해 키를 생성합니다.
sudo docker compose exec shlink shlink api-key:generate --name "web client"이 명령은 키를 한 번만 출력합니다. 키는 해시된 상태로 저장되므로 다시 표시할 수 없습니다. shlink api-key:list은 각 키의 이름과 활성화 여부를 표시하지만 키 자체는 표시하지 않습니다. shlink api-key:disable와 이름을 사용하여 키를 폐기합니다.
모든 REST 호출은 X-Api-Key 헤더에 키를 포함합니다.
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlsshortUrls 키가 포함된 JSON 객체가 반환되면 키가 정상적으로 작동하는 것입니다. INVALID_API_KEY을 포함하는 401는 키가 잘못되었거나 비활성화되었거나 만료되었음을 의미합니다.
명령줄에서 짧은 링크 만들기
CLI는 링크를 만드는 가장 빠른 방법이며, 스크립트에서도 쉽게 사용할 수 있습니다.
sudo docker compose exec shlink shlink short-url:create https://example.com/a/very/long/path
sudo docker compose exec shlink shlink short-url:create https://example.com/docs --custom-slug docs --tag reference--custom-slug를 사용하면 생성된 코드 대신 읽기 쉬운 링크를 얻습니다. 슬러그는 도메인별로 고유하므로 이미 사용 중인 슬러그로 다시 시도하면 첫 번째 링크를 조용히 덮어쓰지 않고 실패합니다. --tag는 여러 번 지정할 수 있으며, 나중에 통합 통계를 확인할 링크를 그룹화할 때 태그를 사용합니다.
존재하는 항목을 나열한 다음, 한 링크의 트래픽을 확인합니다.
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits는 클릭당 한 행을 출력하며, 날짜, 리퍼러 및 user agent를 표시합니다. GEOLITE_LICENSE_KEY 환경 변수를 설정하지 않으면 국가 및 도시 열은 비어 있습니다. 이 환경 변수는 Shlink가 GeoLite2 database를 다운로드할 때 사용하는 무료 MaxMind key입니다. 이 변수가 없더라도 방문 기록은 계속 저장되지만 위치는 확인되지 않습니다.
웹 클라이언트와 QR 코드
웹 클라이언트는 이제 127.0.0.1:8081에서 실행되며 별도의 프록시 항목이 필요합니다. 공개하지 않으려면 SSH 터널을 사용해도 됩니다. 처음 로드할 때 서버 URL과 API key를 입력하라는 메시지가 표시됩니다. https://s.example.com와 생성한 key를 입력합니다. 클라이언트는 두 값을 브라우저 저장소에 보관하고 API를 직접 호출하므로 데이터가 다른 사람의 시스템을 거치지 않습니다.
QR 코드는 별도의 설정이 필요하지 않습니다. 짧은 URL에 /qr-code을 추가하면 API가 이미지를 반환합니다.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size은 픽셀 단위의 너비이며 50부터 1000까지 지정할 수 있고 기본값은 300입니다. format는 png 또는 svg입니다. margin는 코드 주변의 여백을 픽셀 단위로 지정하며, 완성된 이미지의 크기는 크기 값에 여백의 2배를 더한 값입니다. 인쇄 크기가 작거나 일부가 가려져도 스캔되는 코드를 만들려면 errorCorrection=Q을 추가합니다.
계속 실행 상태 유지
단축 URL 서비스는 조용히 실패합니다. 링크의 리디렉션이 중단되어도 이를 알리는 사람이 없습니다. 링크를 클릭한 사용자는 링크가 이미 작동하지 않는다고 생각하기 때문입니다. 홈페이지가 아니라 실제 단축 URL을 대상으로 가동 시간 검사를 설정하고, 리디렉션이 아닌 모든 응답을 경고 대상으로 설정합니다. 자체 호스팅한 Uptime Kuma 인스턴스를 사용하면 이 작업을 효과적으로 수행할 수 있으며, 특정 상태 코드도 감시할 수 있습니다.
컨테이너가 아니라 데이터베이스를 백업합니다. 다음 명령 하나로 데이터베이스를 덤프할 수 있습니다.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gz이 파일과 compose 파일이 있으면 새 서버에서 전체 서비스를 다시 구축할 수 있습니다. 업그레이드는 sudo docker compose pull 후 sudo docker compose up -d 순서로 진행하며, Shlink은 시작할 때 새 마이그레이션을 실행합니다. pull을 실행하기 전에 덤프를 생성합니다. 마이그레이션은 롤백할 수 없기 때문입니다.
FAQ
reverse proxy를 추가한 후 단축 링크에서 404가 반환되는 이유는 무엇입니까?
Shlink은 Host 헤더의 도메인과 단축 코드를 대조합니다. 프록시가 자체 이름이나 내부 주소를 보내면 Shlink은 링크가 없는 도메인에서 해당 코드를 찾으므로 404를 반환합니다. nginx location 블록에서 proxy_set_header Host $host;을 설정한 후 프록시를 reload합니다. 컨테이너를 재시작하지 않아도 링크가 즉시 작동합니다.
Postgres가 필요합니까? SQLite로 충분합니까?
Shlink을 시험하는 용도라면 SQLite로 충분하며 두 번째 컨테이너도 필요하지 않습니다. 중요한 링크를 공개하기 전에는 Postgres로 전환합니다. 클릭할 때마다 방문 레코드가 증가하고 SQLite는 쓰기 작업을 직렬화하기 때문입니다. 나중에 전환하려면 링크를 export한 후 다시 import해야 하므로, 처음부터 Postgres를 선택하면 이 마이그레이션을 피할 수 있습니다.
복사하지 않은 API key를 복구할 수 있습니까?
아니요. Shlink은 key의 hash를 저장하므로 api-key:list은 이름과 상태만 표시하고 값은 표시하지 않습니다. shlink api-key:generate로 새 key를 생성하고 이를 web client에 붙여 넣습니다. 그런 다음 shlink api-key:disable으로 이전 key를 disable하여 더 이상 작동하지 않도록 합니다.
방문 통계에서 국가 열이 비어 있는 이유는 무엇입니까?
Geolocation에는 GeoLite2 database가 필요합니다. Shlink은 GEOLITE_LICENSE_KEY을 제공한 경우에만 이 database를 다운로드합니다. 이 key는 MaxMind에서 무료로 받을 수 있습니다. 이를 environment section에 추가하고 컨테이너를 recreate하면 새 방문의 위치가 확인됩니다. 그 전에 기록된 방문은 shlink visit:locate을 실행할 때까지 빈 상태로 유지됩니다.
Shlink을 다른 서버로 이전하려면 어떻게 해야 합니까?
도메인은 유지하고 데이터를 이전합니다. pg_dump으로 database를 dump하고, dump 파일과 compose 파일을 새 서버로 복사한 후 stack을 시작합니다. 실제 트래픽이 유입되기 전에 빈 database에 dump를 restore합니다. DNS record는 마지막에 변경합니다. 모든 데이터가 database에 저장되므로 단축 코드와 방문 기록이 유지됩니다.