SSD Nodes Learn メモリ 8GB — 年額 $66
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-01

ShlinkとDocker Composeで自前のURL短縮サービスを構築

VPS上でShlink 5.1とshlink-web-client 4.8をDocker Composeで運用します。短縮ドメインのDNS、Postgres、API key、HTTPS、QRコード、クリック統計まで設定します。

構築するもの

セルフホスト型のURL短縮サービスは、長いリンクを自分で所有する短いリンクに変換し、そのクリック数をすべて記録する小規模なサーバーです。選択すべきものはShlinkです。オープンソースで、Dockerイメージとしてリリースされており、データベースと1つのコンテナですべての処理を実行できます。このガイドでは、実際の短縮ドメインを使用し、HTTPS、API key、QRコード、クリック統計を備えた環境をVPS上に構築します。

商用の短縮サービスのように使える要素は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 にドメインを保存します。後から変更すると、すでに配布したすべてのリンクが機能しなくなります。

短いドメインに対して、VPS のパブリック IPv4 アドレスを指定する DNS A レコードを1つ作成します。サーバーが 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:

公開する2つのポートはどちらも127.0.0.1にバインドされます。そのため、次のセクションでリバースプロキシを設定するまで、インターネットからは到達できません。Dockerはホストのファイアウォールより先に独自の転送ルールを適用します。したがって、単純な8080:8080行では、ファイアウォールが閉じているように見えるホストでもアプリが公開されます。ループバックアドレスにバインドすると、これを防げます。同じ方法は、この形で実行するすべてのアプリに適用できます。詳しくは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

初回起動ではデータベースのマイグレーションが実行されるため、2回目以降より時間がかかります。起動が完了したら、サービスがローカルで応答することを確認します。

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/health

200は、APIが稼働し、データベース接続も機能していることを示します。ここで500になる場合、原因はほぼ必ずデータベースです。.env内のDB_PASSWORDが、Postgresの作成時に指定した値と一致していません。Postgresイメージは、空のデータディレクトリを初期化するときにのみPOSTGRES_PASSWORDを読み込むためです。後からパスワードを編集しても、ボリュームを削除して再起動するまでは反映されません。

Shlinkはport 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;
    }
}

次に証明書を発行します。renewal timerを含む手順全体は、Ubuntu 24.04向けCertbotのnginxガイドにあります。

sudo certbot --nginx -d s.example.com

composeファイルのIS_HTTPS_ENABLED: "true"により、Shlinkは返す短縮URLにhttps://を出力します。これは単独ではTLSを有効にしません。HTTPS proxyの背後でfalseのままにすると、APIが返すすべてのリンクがhttp://リンクになります。その後リダイレクトが発生するため、ラウンドトリップが1回増え、Webクライアント上でも不自然に見えます。

API keyを作成する

keyなしではAPIにアクセスできません。コンテナ内のCLIからkeyを生成します。

sudo docker compose exec shlink shlink api-key:generate --name "web client"

このコマンドはkeyを1回だけ表示します。keyはハッシュ化して保存され、再表示できないため、ここでコピーしてください。shlink api-key:listは各keyの名前と有効状態を表示しますが、key自体は表示しません。shlink api-key:disableと名前を指定すると、keyを失効させられます。

すべてのRESTリクエストでは、X-Api-Keyヘッダーにkeyを指定します。

curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urls

shortUrlsキーを含むJSONオブジェクトが返れば、keyは機能しています。INVALID_API_KEYを含む401が返る場合、keyが正しくないか、無効化されているか、有効期限を過ぎています。

コマンドラインから短縮リンクを作成する

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は繰り返し指定できます。タグを使うと、後でまとめて統計を確認するリンクをグループ化できます。

存在するリンクを一覧表示してから、1つのリンクのトラフィックを確認します。

sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docs

short-url:visitsは、クリックごとに日付、リファラー、ユーザーエージェントを1行で出力します。GEOLITE_LICENSE_KEY環境変数を設定しない限り、国と都市の列は空のままです。これは、ShlinkがGeoLite2データベースのダウンロードに使用する無料のMaxMindキーです。このキーがなくてもアクセスは記録されますが、位置情報は特定されません。

WebクライアントとQRコード

Webクライアントは現在127.0.0.1:8081で稼働しており、専用のプロキシエントリが必要です。公開したくない場合は、SSHトンネルを使用してください。初回の読み込み時に、サーバーURLとAPIキーを要求されます。https://s.example.comと、生成したキーを入力します。クライアントはこの2つをブラウザーのストレージに保存し、APIを直接呼び出します。そのため、データが第三者を経由することはありません。

QRコードには設定は一切必要ありません。任意の短縮URLに/qr-codeを追加すると、APIが画像を返します。

https://s.example.com/docs/qr-code?size=500&format=svg&margin=20

sizeはピクセル単位の幅で、50から1000まで指定できます。デフォルトは300です。formatにはpngまたはsvgを指定します。marginはコードの周囲に設ける余白のピクセル数です。生成される画像のサイズは、指定したサイズに余白の2倍を加えたものになります。印刷時に小さくなったり、一部が覆われたりしてもスキャンできるコードには、errorCorrection=Qを追加します。

稼働を維持する

短縮サービスは静かに停止します。リンクのリダイレクトが止まっても、クリックした人がリンク切れだと思うため、誰も知らせてくれません。ホームページではなく実際の短縮URLに稼働監視を設定し、リダイレクト以外の応答があればアラートを送信します。自己ホスト型の Uptime Kuma インスタンスはこの用途に適しており、特定のステータスコードを監視できます。

コンテナではなくデータベースをバックアップします。1つのコマンドでダンプを作成できます。

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

リバースプロキシを追加した後、短縮リンクが404を返すのはなぜですか?

Shlinkは、Host ヘッダーのドメインに対して短縮コードを照合します。プロキシが独自の名前や内部アドレスを送信すると、Shlinkはリンクが存在しないドメインでそのコードを検索するため、404を返します。nginxのlocationブロックで proxy_set_header Host $host; を設定し、プロキシをreloadしてください。コンテナを再起動しなくても、リンクはすぐに機能します。

Postgresは必要ですか。それともSQLiteで十分ですか?

Shlinkを試すだけならSQLiteで問題なく、2つ目のコンテナも必要ありません。重要なリンクを公開する前にPostgresへ移行してください。クリックごとにvisitの行が増え、SQLiteでは書き込みが直列化されるためです。後から切り替える場合はリンクのexportと再importが必要になるため、最初にPostgresを選ぶと、その移行作業を避けられます。

コピーし忘れたAPIキーを復元できますか?

できません。Shlinkはキーのhashを保存するため、api-key:list で表示できるのは名前と状態だけで、キーの値は表示されません。shlink api-key:generate で新しいキーを生成し、webクライアントに貼り付けてください。その後、shlink api-key:disable で古いキーを無効にすると、使用できなくなります。

visit統計の国別列が空なのはなぜですか?

GeolocationにはGeoLite2データベースが必要です。Shlinkは GEOLITE_LICENSE_KEY を指定した場合にのみ、このデータベースをダウンロードします。キーはMaxMindから無料で取得できます。environmentセクションに追加してコンテナを再作成すると、新しいvisitが位置情報付きで記録されます。それ以前に記録されたvisitは、shlink visit:locate を実行するまで空のままです。

ドメインは維持し、データを移動します。pg_dump でデータベースをdumpし、dumpファイルとcomposeファイルを新しいサーバーへコピーします。stackを起動し、実際のトラフィックが到着する前に、空のデータベースへdumpをrestoreしてください。DNSレコードは最後に変更します。短縮コードとvisit履歴はデータベースに保存されているため、そのまま維持されます。

#shlink#url-shortener#self-hosting#docker#postgres