[実装編] Huly で Github Repository の管理始めました
Claude Code のお陰で、今まで開発・実装に至らない構想がたくさん実現できるようになってきて、とても捗った生活をしています。楽しい。
ただ、扱っているアプリケーションが増えてくると進捗状況の管理が手に負えなくなります。外部に公開している Github であれば issue や PR が来ることも少なくありません。これらが登録されていても気がつかない、といったことはままあります。
では、何か良い管理ツールはないか。イロイロ試して辿り着いたのが Huly です。
Notionのドキュメント管理、Jiraのガチガチなタスク管理、そしてLinearの爆速なUI。これらを一皿に盛りつけたような、実になんとも欲張りなツールです。そして最善は、セルフホストで実装可能なことです。
ご丁寧に self-hosting 用の Github リポジトリも公開されています。Github からの Webhook を受け取ってリアルタイムに管理できるところも個人的に嬉しい。
さぁと思って実装してみたは良いものの、単純に `docker compose up -d` でことが済まなかったので、Huly をセルフホストで、Github と連携させる場合の構成パターンをまとめました。現時点で踏んでしまったバグっぽい事象への対応もまとめています。ご参考にしてどうぞ。
手こずるべきは「コマンド」ではなく「3つの国境線」
Hulyのセルフホストで本当に手こずるのは、当然ながら Dockerのコマンド操作そのものではありません。設定が必要な「3つの国境線」を、それぞれ正しく把握して越えられるかどうかが勝負どころなのです。国境線とはわたしが名付けたのではなく、Claude Code が粋だなと思って付けた名前なのでご容赦ください。決して直すのが面倒だったわけではありません。文章ほとんど改定しているのに、直していないのは良い言葉が思いつかないからです。ごめんなさい。
さて、そんな国境線とは。
具体的には、以下の3つの境界のことです。
Docker構成ファイル(.env)の環境変数 — 十数個のマイクロサービスを束ねる司令塔
リバースプロキシとTLSの境界 — 「外から見たHuly」と「中で動くHuly」の分離
GitHub側の事前作成物 — OAuth AppやGitHub Appという、コードやコンテナの外にある設定
この3点さえ最初にきちんと設計してしまえば、構築の途中で迷子になることはありません。逆に言えば、ここを曖昧にしたまま「とりあえず起動」のボタンを押してしまうと、原因不明のエラーログの森で何時間もさまよう羽目になります。
Hulyは「一個のアプリ」ではなく「小さな国家の連合」だから
なぜ、これほどまでに境界の設定が問題になるのでしょうか。
その理由は明確で、Hulyが単一の巨大なアプリケーションではなく、役割の異なる複数のマイクロサービスが複雑に絡み合う「連合体」だからです。ちょっと docker で動かしてみようと思ったのに、dify よりも大量のプロダクトで構成されていて後悔しました。
何があるかと言うと。
画面を描く front、認証を司る account、リアルタイム編集を支える transactor と collaborator、全文検索の fulltext と rekoni、統計の stats、キーバリューストアの kvs……。
さらにその足元を、データストアの CockroachDB、メッセージ基盤の Redpanda、オブジェクトストレージの MinIO、検索エンジンの Elasticsearch という強力なミドルウェア陣が支えています。構成した後になって MinIO かよ…といまになって RustFS にすれば、または既に実装済みの RustFS 使えばよかったと言うのは一つの反省点です。
これだけの“サービス”たちが、同じ .env という一つの環境変数ファイルのもとで実装されているわけです。だからこそ、環境変数ひとつの綴り間違いや認識のズレが全体の機能停止につながります。
ここで一度、Hulyを構成する主なコンテナとその役割を整理しておきましょう。
具体例①:公式・単一ホスト構成で「まず動かす」
応用的な構成に進む前に、まずは公式の huly-selfhost をベースにした最短ルートで一度動かしてみましょう。何事も、まずは土台の感触をつかむのが先決です。
1. 下ごしらえ(OSレベルの2つの落とし穴)
コンテナを立ち上げる前に、ホストOS側で必ずやっておくべき設定が2つあります。
# Elasticsearchがメモリマップを大量に使うため、これを拡張しないと起動に失敗します
sudo sysctl -w vm.max_map_count=262144
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-huly.conf
# コンテナ群のデータが膨らむため、空きディスクは最低でも20GBは見ておくと安心です
df -BG /var/lib/docker
特に vm.max_map_count の上限解放を忘れると、Elasticsearchが何のエラーも吐かずに無言で即死します。地味ですが、もっとも頻出する初手ミスですので確実に実行してくださいね。
2. Docker構成ファイル(.env)の編集 — 第一の境界
公式リポジトリをクローンしたら、環境変数ファイル(.env または huly.conf)を編集します。要点を抜き出すと、以下のような構成になります。
HULY_VERSION=v0.7.423
DESKTOP_CHANNEL=0.7.423
DOCKER_NAME=huly
# 【重要】外からアクセスする際のホスト名。URLの「https://」やポートは付けない!
HOST_ADDRESS=huly.example.com
SECURE=true # trueにすると、frontが自動でhttps/wssの通信を要求します
HTTP_PORT=8087
HTTP_BIND=10.0.0.20 # LAN内のIPに絞ってバインド(安全策)
# CockroachDB(Postgres互換)。パスワードに特殊文字がある場合はURLエンコードが必須
CR_DATABASE=defaultdb
CR_USERNAME=selfhost
CR_USER_PASSWORD=********
CR_DB_URL=postgres://selfhost:********@cockroach:26257/defaultdb
# Redpanda 管理者設定
REDPANDA_ADMIN_USER=superadmin
REDPANDA_ADMIN_PWD=********
# サービス間共有シークレット(絶対に外部に漏らさないでください)
SECRET=********
ここで押さえるべきポイントは3つです。
HOST_ADDRESS には https:// などのスキームやポート番号を絶対に付けず、純粋なドメイン名(FQDN)だけを書くこと。
SECURE=true に設定することで、front コンテナが自動的に https:// と wss://(セキュアなWebSocket)のURIを発行してくれること。
SECRET は全サービスが互いを認証するための「共通の合言葉」なので、ここが1文字でもズレるとログインや同期が連鎖的に崩壊すること。
3. 起動とヘルスチェック
設定が終わったら、いよいよ起動です。
docker compose up -d無事に立ち上がったかどうかを確かめるには、ブラウザを開くよりも先に、アカウントAPIを叩いてみるのが一番手軽で確実です。
curl -s -o /dev/null -w "%{http_code}\n" http://10.0.0.20:8087/_accounts
# → 結果として「405」が返ってくれば、正常に起動しています!一見「405 Method Not Allowed」と出るとエラーのように思えますが、実はこれで大正解。/_accounts はPOSTメソッド専用のJSON-RPCエンドポイントなので、GETアクセスに対して正しく405を返している=背後の account サービスが元気に生きている証拠なのです。ここで「200 OK」を期待して待ち続けると、永遠に来ない朝を待つことになります。
具体例②:応用・Ansible + Traefik で「プロキシとコンテナを別ホストに分離する」
ひとまず動くことが確認できたら、運用のクオリティを上げていきましょう。
わたしの本番環境では、Hulyを動かすアプリケーションホスト(app-host = 10.0.0.20)と、TLS(SSL証明書)を終端するリバースプロキシホスト(proxy-host)を物理的、あるいは論理的にあえて切り離しています。他の社内ツールや監視基盤と入口を一本化したかったからです。
なぜ「Dockerラベル自動検出」ではなく「File Provider」なのか
ここが、クロスホスト構成における考慮点です。
モダンなリバースプロキシである「Traefik」は、通常コンテナの labels に設定を書き込むことで自動的にルーティングを生成してくれます。しかし、TraefikとHulyが別々のホストにいる場合、Traefikはネットワーク越しに別サーバーのDockerソケット(.sock)を覗き見ることができません。
そこで採用するのが、ファイルベースで静的にルーティングを定義する File Provider 方式 です。なんとなく古からのプロキシと言えばこれですね。
# proxy-host側(Traefik)の動的設定ファイル(/etc/traefik/dynamic/huly.yml)
http:
routers:
huly:
rule: "Host(`huly.example.com`)"
entryPoints: ["websecure"]
middlewares: ["lan-only@file"] # 社内ネットワーク(CIDR)からのアクセスのみに制限
tls:
certResolver: letsencrypt # Let's Encryptで証明書を自動更新
services:
huly:
loadBalancer:
servers:
- url: "http://10.0.0.20:8087" # 実際のHulyが動いているサーバーへ転送この構成にすることで、インターネット、および家庭内LANからの暗号化通信(HTTPS)はすべて proxy-host のTraefikが引き受け、そこから先の app-host へは平文でリレーされます。
「コンテナ側は平文で受けるのに、.env の SECURE=true はそのままでいいの?」と思われるかもしれませんが、これで問題ありません。ブラウザから見れば通信は常に https:// および wss:// であり、WebSocketのプロトコルアップグレードもTraefikが綺麗に透過させてくれるからです。
さらに、Huly側のNginxを HTTP_BIND=10.0.0.20 としてLAN内IPにのみ縛り、Traefik側でも社内IP制限のミドルウェア(lan-only)を噛ませることで、「うっかり全世界にフルオープンになっていた」という事故を防ぐ二重の安全弁を仕込んでいます。
うっかり Traefik を全世界にオープンした時の影響が甚大ですけれどもね。
具体例③:GitHub連携 — コードの外にある「2つの異なる顔」
ここが「第三の国境」であり、セルフホスト勢が最も気をつけるべき罠の領域です。
Hulyの機能にある「GitHub連携」には、まったく役割が異なる2つの仕組みが存在します。これらを混同してごちゃまぜに設定すると、高確率で迷宮入りします。まずは下の表で、その違いをきれいに切り分けておきましょう。
A. GitHubログイン連携(OAuth App)の構築
「Sign in with GitHub」のボタンをログイン画面に生やすだけなら、設定は非常にシンプルです。GitHubの Settings -> Developer settings -> OAuth Apps から作成します。
Homepage URL: https://huly.example.com/
Authorization callback URL: https://huly.example.com/_accounts/auth/github/callback
ここで発行された Client ID と Client Secret を暗号化して環境変数にセットし、account コンテナに渡します。OAuthのリダイレクト処理はユーザーのブラウザ上で完結しますし、コンテナ側はGitHubに対して外向きのAPIリクエストを送るだけです。そのため、サーバー自体がLAN内に閉じこもっていても何の問題もなく動作します。
B. Issue/PR双方向同期(GitHub App)の構築
こちらは、GitHub上のIssueやPull Requestの動きをリアルタイムにHulyのボードへ反映させる、Hulyの真骨頂とも言える機能です。こちらには OAuth App ではなく GitHub App の作成が必要です。
Callback / Setup URL: https://huly.example.com/github
Webhook URL: https://huly.example.com/_github/api/webhook (※任意のWebhook secretも発行)
必要な権限(Read & Write): Commit statuses / Contents / Discussions / Issues / Pages / Projects / Pull requests / Webhooks(※MetadataのみRead-onlyでOK)
購読するイベント: Issues / Pull request / PR review / PR review comment / PR review thread
ここで直面する最初の難関が、GitHubからダウンロードする秘密鍵(.pem ファイル)の扱い方です。Hulyの github コンテナにこの鍵を環境変数として渡す際、改行コードを含んだままでは読み込めません。改行を \n という文字列にエスケープして1行に整形してあげる必要があります。
手動でやると発狂しそうになりますが、以下のワンライナーを使えば一発で変換できます。
awk 'BEGIN{ORS="\\n"}{print}' your-app.private-key.pem
また、GitHubからのリアルタイムな通知をWebhookで受け取るため、このエンドポイントだけは外からアクセスできるようにプロキシ(Traefik)を開放する必要があります。
ただし、セキュリティのためにすべてを全開にする必要はありません。/_github/api/webhook という特定のパスだけをターゲットにした高優先度のルーティングを作り、それ以外のパスはLAN内限定のままブロックします。さらに、GitHub公式が公開している送信元IPアドレスの範囲だけに絞ったIP制限をかけることで、一応、多層防御を形成することができます。
運用ハマりどころ:わたしが実際に踏んだ「リアルな地雷原」
ここからは、公式ドキュメントのどこを探しても載っていない、実戦で血を流しながら見つけた“リアルな傷跡”と、その回避策を共有します。
ここから先は
この記事が参加している募集
あなたの支えが、私の心の糧になります。 note の収益はガジェットのレビューや、自費出版に使わせていただきます。
