SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-15

LinkBreezeをDocker Composeでセルフホストする方法

Linktreeの代替LinkBreezeをVPSで公開する手順です。Docker ComposeとCaddyで構成し、固定イメージタグ、Cookieなしのクリック計測、サイト全体を保持する1つのボリュームを確認します。

LinkBreeze とは

LinkBreeze は、Linktree の代替となるセルフホスト型のサービスです。1 つの Docker コンテナで、公開用のリンク集ページと管理ダッシュボードを提供します。すべての状態は、1 つの SQLite ファイルに保存されます。MIT ライセンスで提供され、Next.js 上の TypeScript で記述され、ghcr.io/manak-hash/linkbreeze として公開されています。実行には、VPS、VPS を指す A レコードが設定されたドメイン、80 番ポートと 443 番ポートへのアクセス許可、Docker Compose plugin を備えた Docker Engine が必要です。

このガイドでは、リポジトリが実際にサポートしている構成を扱います。つまり、証明書を自動取得する reverse proxy の背後で Docker Compose を実行する構成です。また、問題が発生する箇所も扱います。プロフィールのリンクは、他の人がクリックする公開 URL だからです。リンクが壊れていると、そのクリックを失います。

その前に、このプロジェクトがまだ非常に新しいことを理解しておいてください。

LinkBreeze は公開プロフィールリンクに使えるほど成熟していますか?

2026 年 8 月時点で、リポジトリには 178 stars、17 forks、単独のメンテナーがいます。最初のタグ付きリリースである v1.0.0 の日付は 2026 年 7 月 1 日です。数年前からあるプロジェクトではなく、開始から数週間のプロジェクトです。

ChartLinkBreeze tagged releases per week, v1.0.0 to v1.2.7
The data behind this chart
[
  {
    "week": "2026-06-29",
    "releases": 3,
    "cumulative": 3
  },
  {
    "week": "2026-07-06",
    "releases": 3,
    "cumulative": 6
  },
  {
    "week": "2026-07-13",
    "releases": 1,
    "cumulative": 7
  },
  {
    "week": "2026-07-20",
    "releases": 2,
    "cumulative": 9
  },
  {
    "week": "2026-07-27",
    "releases": 3,
    "cumulative": 12
  },
  {
    "week": "2026-08-03",
    "releases": 2,
    "cumulative": 14
  },
  {
    "week": "2026-08-10",
    "releases": 3,
    "cumulative": 17
  }
]

v1.0.0 以降、このプロジェクトでは 17 件のタグ付きリリースが、7 暦週にわたって公開されています。このガイドの執筆時点では、グラフの最後の週はまだ進行中でしたが、その週だけで 3 件が公開されていました。

この点は、2 つの別々の事実として捉えてください。メンテナーは活発に対応しており、バグは数日以内に修正されます。一方で、スキーマとデフォルト値もまだ変更されています。そのため、デプロイしたまま放置するインスタンスは、開発中のコードから大きく乖離します。

ライセンスにより、最悪の事態からは保護されます。MIT ライセンス、コンテナーイメージ、自分のディスク上にある SQLite ファイルがあれば、開発が停止しても、構築済みの環境は動き続けます。ただし、公開 Web アプリがセキュリティ修正を受け取れなくなり、時間の経過とともに負債になるリスクまでは解消できません。更新を継続する前提でデプロイし、初日から以下のバックアップ手順を機能させてください。

イメージタグを固定し、latest は使用しない

リリースワークフローは、各バージョンにつき正確に2つのタグを push します。latestと、先頭の v を除いたバージョン番号です。したがって、リリース v1.2.7 の固定タグは ghcr.io/manak-hash/linkbreeze:1.2.7 です。:v1.2.7 と記述しても何も pull されず、Docker は manifest unknown と報告します。そのタグは push されていないためです。

latest は変わるため、タグを固定してください。上記のグラフの頻度で考えると、latest に対する docker compose pull は、利用者が使っているページを未レビューのまま更新することになります。タグを固定すれば、ファイルを編集した時点で更新が行われます。

イメージについて、もう1点あります。リリースワークフローは platforms: の設定なしでビルドするため、公開イメージは linux/amd64 のみです。arm64 ホストでは pull が no matching manifest for linux/arm64/v8 in the manifest list entries で失敗します。x86 ではなく ARM VPS を使用する場合は、ホスト上でイメージをビルドしてください。

git clone --branch v1.2.7 --depth 1 https://github.com/Manak-hash/LinkBreeze.git
cd LinkBreeze
docker build -t linkbreeze:1.2.7 .

その後、下記の compose ファイルでは linkbreeze:1.2.7 をイメージ名として使用します。

Caddy の背後に LinkBreeze をデプロイし、自動 TLS を有効にする

Caddy は Let's Encrypt から証明書を自動的に取得して更新するため、TLS(トランスポート層セキュリティ)用の証明書を別途設定する必要はありません。デプロイに必要なファイルは、1 つのディレクトリに置く 3 つだけです。

まず Secret を生成します。

mkdir -p ~/linkbreeze && cd ~/linkbreeze
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env

SECRET_KEY は管理者セッション Cookie の署名と、分析用の訪問者ハッシュのソルトに使用されます。リポジトリで公開されている compose ファイルでは、デフォルト値が ${SECRET_KEY:-changeme-in-production} になっています。この手順を省略すると、GitHub 上で公開されているセッション署名キーを使ってインスタンスが起動します。初回起動前に設定してください。後から変更するとログアウトされ、分析用のソルトもリセットされます。

docker-compose.yml を作成します。

services:
  linkbreeze:
    image: ghcr.io/manak-hash/linkbreeze:1.2.7
    restart: unless-stopped
    volumes:
      - linkbreeze-data:/app/data
    environment:
      - DATABASE_PATH=/app/data/linkbreeze.db
      - SECRET_KEY=${SECRET_KEY}
      - BASE_URL=https://links.example.com
    networks:
      - linkbreeze-net

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - linkbreeze-net

networks:
  linkbreeze-net:

volumes:
  linkbreeze-data:
  caddy-data:
  caddy-config:

BASE_URL は任意ですが、設定することを推奨します。アプリケーションに実際の公開アドレスを伝えるため、偽装された Host ヘッダーを含むリクエストによって、別のドメインへのリンクが生成されることを防げます。

次に、独自のドメインを指定して Caddyfile を同じディレクトリに作成します。

links.example.com {
    encode zstd gzip
    reverse_proxy linkbreeze:3000
}

Caddy は、プロキシ経由のリクエストに対してデフォルトで X-Forwarded-ForX-Forwarded-Proto を設定します。分析機能はこれらを使用します。起動します。

docker compose up -d
docker compose ps
docker compose logs -f caddy

docker compose ps で、LinkBreeze コンテナが healthy と表示されることを確認します。イメージには wget --spider -q http://127.0.0.1:3000/api/health という独自の healthcheck が含まれているため、追加する必要はありません。リポジトリの Caddy 用サンプルにある healthcheck をコピーしないでください。この healthcheck は curl を呼び出しますが、イメージは node:22-alpine をベースに構築されており、busybox wget が含まれ、curl はありません。そのため、ページを正常に配信していても、コンテナは unhealthy を報告します。

ブラウザーで https://links.example.com を開きます。初回アクセスでは /setup のセットアップウィザードが表示され、単一の管理者アカウントが作成されます。その後、ダッシュボードは /dashboard、ログインフォームは /login で開けます。このアカウントはこのインスタンス専用です。アプリケーションにはシングルサインオン用のフックがないため、ホストしている他のサービスと同じ認証でダッシュボードにアクセスしたい場合は、自己ホスト型 Authentik などの forward auth プロキシを前段に置く必要があります。

compose ファイルが行っていないことにも注意してください。port 3000 は公開していません。公開インターフェイスで待ち受けるのは Caddy だけです。Compose ファイルの構文に慣れていない場合は、VPS 向け Docker Compose の基本で、このファイルが前提とする部分を確認できます。すでに別のプロキシを前段で運用している場合は、Nginx、Caddy、Traefik の比較で変更点を確認できます。リポジトリには、Certbot を使う Nginx、Traefik、Cloudflare tunnel の動作するサンプルも含まれています。

Where your data lives, and what a backup has to contain

DATABASE_PATH points at /app/data/linkbreeze.db. Uploaded avatars and link thumbnails are written beside it in /app/data/uploads. Both live in the named volume linkbreeze-data, so the unit of backup is the volume, not the database file on its own. Restore the file without the uploads directory and every image on the page returns a 404.

Everything else really is in that one database: pages, links, settings, theme, email subscribers and analytics rows.

Take the copy with the container stopped:

docker compose stop linkbreeze
docker compose cp linkbreeze:/app/data ./backup-$(date +%F)
docker compose start linkbreeze

Stop first because copying a SQLite database while a process is writing to it can capture a half-finished transaction, and the copy then opens as a corrupt file. The page is offline while the copy runs. Restoring is the same move in reverse:

docker compose stop linkbreeze
docker compose cp ./backup-2026-08-14/. linkbreeze:/app/data
docker compose start linkbreeze
docker compose logs -f linkbreeze

The dashboard also offers a JSON export, served from /api/backup as linkbreeze-backup-YYYY-MM-DD.json. It carries the profile, the links, the settings and the saved themes. It does not carry analytics history, email subscribers or uploaded images, and restoring it deletes the current rows in those four tables before inserting the file's. Treat it as a config snapshot for moving hosts or undoing an editing mistake. The volume copy is the backup.

Two storage rules apply here the same as anywhere else you are running SQLite in production on a VPS. Keep the database on local disk, because SQLite's locking is unreliable on a network filesystem and a corrupt page is how you find out. And if you swap the named volume for a host bind mount, chown the host directory first: the container runs as the non-root node user, uid 1000 in node:22-alpine, and a directory created by root is not writable by it, so the app cannot open the database and the container exits at startup. Bind mounts against named volumes in Compose covers that trade in full.

不要な分析機能と同意バナー

他の場所で無料提供されているページを、自己ホストする理由になる機能です。

分析機能は Cookie を使用しません。訪問者の端末に Cookie を設定せず、公開ページでサードパーティーのスクリプトも読み込みません。訪問者は、IP アドレス、User-Agent 文字列、salt から生成した SHA-256 ハッシュを16進数16文字に切り詰めた値で識別します。salt 自体は現在の UTC 日付と SECRET_KEY から生成したハッシュです。そのため UTC の深夜に変わり、前日のハッシュを当日のハッシュと照合できません。元の IP アドレスがデータベースに書き込まれることはありません。

クリックはサーバー側でカウントします。公開ページ上のすべての http リンクは、自分のドメイン上の /go/<id> を指します。/go/<id> はクリックを記録した後、実際の遷移先への 302 リダイレクトを返します。そのため、JavaScript を無効にしている読者や、バックグラウンドリクエストをブロックするアプリ内ブラウザーでもカウントできます。ページビューは /api/track を通じて記録します。

知っておくべき除外が2つあります。有効な admin セッションを含むリクエストは除外するため、自分のページを編集しても数値は増えません。既知のクローラーの User-Agent も除外します。

同意については、読者の端末に何も保存しません。読者の端末に保存される Cookie こそ、Cookie バナーが許可を求める対象です。読者が居住する場所によって義務は異なるため、必要な要件は確認してください。ただし、ここには開示すべきトラッキング Cookie はなく、データを受け取るサードパーティーもありません。

注意点が1つあります。SECRET_KEY をローテーションすると daily salt も変わるため、その時点から、再訪問したすべての訪問者が新規訪問者としてカウントされます。

分析の国列が空なのはなぜですか?

スタック内のどの構成要素も国情報のヘッダーを設定していないためです。LinkBreeze は cf-ipcountryx-vercel-ip-country などのプロキシヘッダーから国情報を解決します。独自の Caddy または Nginx の背後にある VPS では、これらのヘッダーが存在しないため、国情報は null として記録され、内訳は空のままになります。コンテナ内に GeoIP データベースはありません。

入力する方法は 2 つあります。ドメインの前段に Cloudflare を配置すると、プロキシするすべてのリクエストに cf-ipcountry が追加されます。もう 1 つは、独自のリバースプロキシでローカルの GeoIP 参照を行い、その結果をいずれかのヘッダーに設定する方法です。

関連する別の問題はさらに深刻なので、確認してください。クリックと表示のハンドラーは、まず X-Forwarded-For からクライアントアドレスを読み取り、次に X-Real-IP を読み取ります。どちらのヘッダーもない場合は 0.0.0.0 にフォールバックします。プロキシを前段に置かずに port 3000 をインターネットへ直接公開すると、すべての訪問者が同じ値にハッシュされます。その結果、unique visitors は常に 1 になり、IP ごとの 1 分あたり 60 events のレート制限が利用者全体に一度に適用されます。上記の reverse_proxy ディレクティブの背後では、Caddy がヘッダーを設定するため、両方の問題が解消されます。

Linktree からのインポートと、引き継がれない内容

ダッシュボードの移行ウィザードでは、公開プロフィール URL またはエクスポートファイルを指定できます。linktr.ee、bento.me、lnk.bio、tap.link、hopp.bio、beacons.ai、solo.to、linkfly、mssg.me、LittleLink のページに対応しており、汎用的な HTML および JSON のエクスポートも認識します。Linktree または Bento の URL では、ページに埋め込まれた __NEXT_DATA__ JSON を読み取ります。静的ページでは、アンカータグを読み取ります。

引き継がれるのは、各リンクのタイトル、URL、説明、画像、リンクがソーシャルプロフィールかどうか、および表示名、自己紹介、アバターです。見つかったリンクのうち保持するものを選択してから、データベースへの書き込みが行われます。

引き継がれないのは、分析履歴、テーマとレイアウト、メール購読者、公開予定日時、旧プラットフォームが独自のログインの内側で管理している情報です。外観は手作業で再構築する必要があります。過去のクリック履歴は旧サービスに残ることも理解しておいてください。

インポーターはブラウザーではなくサーバーから URL を取得するため、公開されていないアドレスは拒否します。Private/local URLs are not allowed は、自分のネットワーク内にあるアドレスを指定したことを示します。この拒否は意図的なものです。これがなければ、ダッシュボードにアクセスできるユーザーが、サーバーからしか到達できないマシンを調査するためにサーバーを悪用できてしまいます。ほかに表示される可能性があるメッセージは Only http and https URLs are allowedRequest timed outResponse too large です。

スクレイピングは、他者が管理するマークアップに依存します。リンクが明らかに存在するページでウィザードが何も検出しない場合、そのプラットフォームがパーサーの作成後に HTML を変更した可能性があります。修正を待つのではなく、リンクを手動で追加してください。必要なのがプロフィールページではなく、効果を測定できる短縮リンクであれば、Shlink のようなセルフホスト型 URL 短縮サービスが適しています。同じサーバー上で問題なく動作します。

固定したデプロイメントの更新

# edit the image tag in docker-compose.yml, then
docker compose pull
docker compose up -d
docker compose logs -f linkbreeze

スキーマのマイグレーションは、コンテナの起動時に自動的に実行されます。マイグレーションを逆方向に実行する方法は文書化されていないため、先にボリュームのコピーを作成してください。元に戻せないアップグレードは、変更前の状態を復元できる場合にのみ安全です。

新しいリリースが存在すると、ダッシュボードにバナーが表示されます。これは 24 時間に 1 回、プロジェクトの GitHub リポジトリから小さなバージョンファイルを取得して確認します。インスタンスに関する情報は送信しません。タグを変更する前にリリースノートを確認してください。プロジェクトの現段階では、マイナーバージョンの変更によって、依存しているデフォルト値が変わる可能性があります。

障害パターンと表示される文字列

manifest unknown の取得時に発生するエラー。 タグは :v1.2.7 として記述されています。レジストリタグには v がないため、:1.2.7 を使用してください。

no matching manifest for linux/arm64/v8 in the manifest list entries 公開イメージは amd64 専用です。タグ付けされたソースから ARM ホスト上でビルドしてください。

ページは正常に読み込めるのに、コンテナが unhealthy を報告する。 compose ファイルの healthcheck が curl を呼び出していますが、そのコマンドはイメージに含まれていません。healthcheck を削除し、イメージ独自の wget healthcheck を実行させてください。

Caddy が証明書エラーを返す、または何も返さない。 docker compose logs caddy を確認してください。よくある原因は、A レコードがまだこの VPS を指していないこと、またはファイアウォールでポート 80 が閉じていることです。これにより、Caddy がドメインの管理権限を証明するために使用する ACME(自動証明書管理環境)の HTTP challenge がブロックされます。

Unique visitors が 1 のままになる。 プロキシが X-Forwarded-For を設定していないため、すべての訪問者が同じ値としてハッシュされています。

昨日まで動作していたコンテナが、起動直後に終了する。 名前付きボリュームからホストの bind mount に切り替えた場合、データディレクトリの所有者が root になり、アプリは uid 1000 で実行されるため、データベースファイルを開けません。ホストディレクトリに対して sudo chown -R 1000:1000 を実行してください。

トラッキングリクエストが HTTP 429 で応答される。 /api/track/go/<id> の IP 単位のスロットルに達しています。訪問者は引き続き目的の URL へリダイレクトされますが、クリックはカウントされません。

FAQ

まだ新しいプロジェクトです。2026 年 8 月時点で、リポジトリの star 数は 178、fork 数は 17、メンテナーは 1 人で、最初のリリース日は 2026 年 7 月 1 日です。平均すると週に 2 回を超える頻度でリリースされているため、バグはすぐに修正されますが、動作も頻繁に変わります。MIT ライセンスとローカルの SQLite ファイルにより、開発が停止しても動作するページは維持できます。ただし、セキュリティ修正のない公開 Web アプリケーションはリスクになります。1 度インストールして終わりにするのではなく、継続的に更新するソフトウェアとして扱ってください。

LinkBreeze ではどの image tag を実行すべきですか?

バージョン tag を実行してください。たとえば ghcr.io/manak-hash/linkbreeze:1.2.7 です。tag は意図的に変更してください。リリースワークフローが push するのは latest とバージョン番号だけです。そのため、v を付けた :v1.2.7 は存在せず、Docker は manifest unknown を返します。イメージは linux/amd64 専用にビルドされます。arm64 の VPS では tag を clone して、ローカルでビルドする必要があります。

LinkBreeze の analytics で国別の内訳が空のままになるのはなぜですか?

LinkBreeze は cf-ipcountryx-vercel-ip-country などの proxy header から訪問者の国を読み取り、独自の GeoIP データベースは持ちません。自分の Caddy または Nginx の背後にある VPS では、これらの header が設定されないため、国は null として保存されます。ドメインの前段に Cloudflare を置くか、reverse proxy でローカルの GeoIP lookup からこれらの header のいずれかを設定してください。

何をバックアップし、どのように復元すればよいですか?

データベースファイルだけでなく、linkbreeze-data volume 全体をバックアップしてください。/app/data/linkbreeze.db にはすべての link、page、setting、subscriber、analytics の row が保存され、/app/data/uploads には page が参照する avatar と thumbnail の画像が保存されます。コンテナを停止し、docker compose cp linkbreeze:/app/data ./backup-$(date +%F) を実行してから、再度起動してください。復元時は、停止したコンテナにディレクトリをコピーして戻し、コンテナを起動します。dashboard の JSON export は profile、link、setting、theme の設定スナップショットです。analytics と画像は含まれません。

Linktree からの import で analytics と theme も移行されますか?

いいえ。migration wizard が読み取るのは、以前の公開 profile にある link の title、URL、description、image と、display name、bio、avatar です。analytics の履歴、theme、email subscriber、予約済みの公開日時は移行されません。import 後に theme editor で外観を作り直してください。また、click の履歴は以前のプラットフォームに残る点に注意してください。

#linkbreeze#linktree-alternative#docker-compose#sqlite#self-hosting#analytics