Coolifyハンズオン:Hono・Go・Railsを実際にデプロイしてみる
概要
- Coolifyには3つのビルドパック(Nixpacks・Dockerfile・Docker Compose)がある
- それぞれの特徴を、実際にアプリを作ってデプロイしながら体験する
- 簡単な順に進む:Hono(Nixpacks)→ Go(Dockerfile)→ Rails + Ollama(Docker Compose)
第1回でCoolifyの選定理由、第2回でインストールとMCPを紹介しました。今回は「実際にゼロからアプリを載せる」ハンズオン。読みながら手を動かせるようにしています。
前提
- Coolifyがインストール済み(第2回参照)
- GitHubアカウントがある
- ワイルドカードDNS(
*.yourdomain.com)が設定済み
3つのビルドパック
Coolifyには、アプリをどうやってコンテナにするかを決める「ビルドパック」が3種類あります。
| ビルドパック | 何をするか | 向いているケース |
|---|---|---|
| Nixpacks | ソースコードから自動でDockerイメージを生成 | プロトタイプ、小〜中規模アプリ |
| Dockerfile | リポジトリのDockerfileでビルド | 本番運用、細かい制御が必要な場合 |
| Docker Compose | 複数コンテナをまとめてデプロイ | DB・キャッシュ込みのフルスタック構成 |
順番にやっていきます。
1. Hono × Nixpacks:Dockerfileを書かずにデプロイ
Nixpacksとは
NixpacksはRailwayが開発したビルドツール。ソースコードを読んで、言語やフレームワークを自動検出し、Dockerイメージを勝手に作ってくれる。Dockerfileを書く必要がない。
HerokuのBuildpacksと似た思想だけど、Nixpacksは最終的にDockerイメージを生成するので、どこでも動く。
アプリを作る
HonoはCloudflare Workers向けに作られた軽量Webフレームワーク。Node.jsでも動くので、ここではNode.jsランタイムで使います。
mkdir coolify-demo-hono && cd coolify-demo-hononpm init -ynpm install hono @hono/node-servernpm install -D typescript @types/node tsxsrc/index.ts を作成。
import { Hono } from 'hono'import { serve } from '@hono/node-server'
const app = new Hono()
app.get('/', (c) => { const now = new Date().toISOString() return c.html(` <h1>Hello from Hono on Coolify!</h1> <p>Current time: ${now}</p> <p>Node.js ${process.version}</p> `)})
app.get('/health', (c) => { return c.json({ status: 'ok' })})
const port = Number(process.env.PORT) || 3000console.log(`Server running on port ${port}`)serve({ fetch: app.fetch, port })package.json の scripts にビルドとスタートを追加。
{ "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js" }}これだけ。 Dockerfile不要。package.jsonにbuildとstartがあれば、Nixpacksが勝手にやってくれる。
GitHubにpushしてCoolifyでデプロイ
git init && git add -A && git commit -m "initial commit"gh repo create coolify-demo-hono --public --source=. --push管理画面(GUI)の場合:
New Resource → Application → GitHubリポジトリを選択 → ビルドパックでNixpacksを選択 → ドメインを入力 → Deploy。
Claude Code MCP(プロンプト)の場合:
coolify-demo-honoリポジトリをNixpacksでデプロイして。ドメインはdemo-hono.teraren.comで。MCPが create application → deploy まで自動で実行してくれる。GUIで3〜4画面分の操作が、プロンプト1つで完了する。
動作確認
$ curl https://demo-hono.teraren.com/<h1>Hello from Hono on Coolify!</h1><p>Current time: 2026-03-14T21:41:05.634Z</p><p>Node.js v22.11.0</p>
$ curl https://demo-hono.teraren.com/health{"status":"ok"}Nixpacksが自動検出したビルドプラン:
╔══════════════ Nixpacks v1.41.0 ═══════════════╗║ setup │ nodejs_22, npm-9_x, curl, wget ║║────────────────────────────────────────────────║║ install │ npm ci ║║────────────────────────────────────────────────║║ build │ npm run build ║║────────────────────────────────────────────────║║ start │ npm run start ║╚════════════════════════════════════════════════╝package.jsonを見てNode.jsアプリだと判定、npm ci → npm run build → npm run startのフローを自動構成してくれた。設定ファイルゼロでデプロイ完了。
Nixpacksの限界
便利だけど限界もあります。
- マルチステージビルドができない(イメージサイズが大きくなりがち)
- キャッシュ制御が粗い(ビルド時間の最適化が難しい)
- Alpine以外のベースイメージを選べない場面がある
- ネイティブ拡張の依存関係で詰まることがある
プロトタイプや小規模アプリには最高。でも本番運用で細かい制御が必要になったら、Dockerfileに移行するタイミング。
2. Go × Dockerfile:バイナリ1つのミニマルデプロイ
なぜDockerfileを書くのか
Nixpacksで自動生成されるDockerfileは汎用的。でもGoのようにシングルバイナリにコンパイルできる言語では、マルチステージビルドで本番イメージを極小にできる。これはNixpacksではできない。
アプリを作る
標準ライブラリだけで書きます。外部依存ゼロ。
package main
import ( "encoding/json" "fmt" "net/http" "os" "runtime" "time")
func main() { port := os.Getenv("PORT") if port == "" { port = "8080" }
hostname, _ := os.Hostname()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintf(w, "Hello from Go on Coolify!\n\nGo version: %s\nHostname: %s\nTime: %s\n", runtime.Version(), hostname, time.Now().Format(time.RFC3339)) })
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) })
fmt.Printf("Listening on :%s\n", port) http.ListenAndServe(":"+port, nil)}Dockerfile:マルチステージビルド
ここがDockerfileの真価。
# Build stageFROM golang:1.24-alpine AS builderWORKDIR /appCOPY go.mod .COPY main.go .RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
# Production stage — tiny image with just the binaryFROM scratchCOPY --from=builder /app/server /serverEXPOSE 8080ENTRYPOINT ["/server"]ポイント:
- ビルドステージ(
golang:1.24-alpine、約300MB)でコンパイル - 本番ステージは
scratch(0バイト)にバイナリだけコピー - 最終イメージはバイナリのサイズだけ(数MB)
CGO_ENABLED=0で静的リンク、-ldflags="-s -w"でデバッグ情報を除去
デプロイと動作確認
gh repo create coolify-demo-go --public --source=. --push管理画面(GUI)の場合:
New Resource → Application → GitHubリポジトリを選択 → ビルドパックでDockerfileを選択 → ドメインを入力 → Deploy。
Claude Code MCP(プロンプト)の場合:
coolify-demo-goリポジトリをDockerfileでデプロイして。ドメインはdemo-go.teraren.comで。ビルド時間は約40秒。
$ curl https://demo-go.teraren.com/Hello from Go on Coolify!
Go version: go1.24.13Hostname: ed06252fb474Time: 2026-03-14T21:41:04Z
$ curl https://demo-go.teraren.com/health{"status":"ok"}Goのマルチステージビルドのイメージサイズ比較。
| ステージ | ベースイメージ | サイズ |
|---|---|---|
| ビルド | golang:1.24-alpine | ~300MB |
| 本番 | scratch | ~6MB |
これがDockerfileを自分で書く理由。Nixpacksだとこの最適化はできない。
3. Rails × Docker Compose:ローカルLLMチャットアプリ
Docker Composeとは
複数のコンテナをまとめて定義・管理する仕組み。Webアプリ + DB + キャッシュ + α をdocker-compose.yaml一つで定義できる。
Coolifyは「アプリとDBを個別に作って環境変数で繋ぐ」思想だけど、Docker Compose方式も対応している。コンテナ間の依存関係が複雑な場合はこっちの方が楽。
何を作るか
Ollamaを使ったローカルLLMチャットアプリ。構成:
| サービス | イメージ | 役割 |
|---|---|---|
| app | Ruby on Rails | Webアプリ + チャットUI |
| db | PostgreSQL 17 | チャット履歴の保存 |
| redis | Redis 7 | セッション・キャッシュ |
| ollama | Ollama | ローカルLLM推論 |
4つのコンテナが連携して動く、フルスタック構成。
docker-compose.yaml
services: app: build: . ports: - "3000:3000" depends_on: db: condition: service_healthy redis: condition: service_healthy environment: - RAILS_ENV=production - DATABASE_HOST=db - DATABASE_USER=postgres - DATABASE_PASSWORD=password - REDIS_URL=redis://redis:6379/0 - OLLAMA_URL=http://ollama:11434 - SECRET_KEY_BASE=dummy-secret-key-for-demo-only - RAILS_LOG_TO_STDOUT=true - RAILS_SERVE_STATIC_FILES=true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/up"] interval: 10s timeout: 5s retries: 3 start_period: 30s
db: image: postgres:17-alpine volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=password - POSTGRES_DB=app_production healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 5
redis: image: redis:7-alpine volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5
ollama: image: ollama/ollama:latest volumes: - ollama_data:/root/.ollama healthcheck: test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"] interval: 10s timeout: 5s retries: 5 start_period: 30s
volumes: postgres_data: redis_data: ollama_data:ポイント:
depends_on+condition: service_healthyでDBとRedisの起動を待ってからアプリを起動- 各サービスに
healthcheckを定義。Coolifyがコンテナの状態を監視できる volumesでデータを永続化。デプロイしてもDB・Redis・Ollamaのモデルデータは保持される
Railsアプリのコード
Ollamaとの通信はNet::HTTPで直接。gem追加不要。
class HomeController < ApplicationController def index @db_adapter = ActiveRecord::Base.connection.adapter_name @redis_status = check_redis @ollama_status, @ollama_models = check_ollama @messages = Message.order(created_at: :desc).limit(20).reverse end
def chat prompt = params[:prompt].to_s.strip return redirect_to root_path if prompt.empty?
Message.create!(role: "user", content: prompt) response = chat_with_ollama(prompt) Message.create!(role: "assistant", content: response) redirect_to root_path end
private
def chat_with_ollama(prompt) uri = URI("#{ENV['OLLAMA_URL']}/api/generate") body = { model: "tinyllama", prompt: prompt, stream: false }.to_json response = Net::HTTP.post(uri, body, "Content-Type" => "application/json") JSON.parse(response.body)["response"] rescue => e "Error: #{e.message}" endendOllamaの/api/generateエンドポイントにPOSTするだけ。stream: falseで一括レスポンスを受け取っている。
Dockerfile
FROM ruby:3.4-alpine AS baseWORKDIR /appENV RAILS_ENV=production BUNDLE_WITHOUT="development:test" BUNDLE_DEPLOYMENT=1
FROM base AS buildRUN apk add --no-cache build-base postgresql-dev curl yaml-devCOPY Gemfile ./RUN bundle lock && bundle install --jobs 4
COPY . .RUN SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompile 2>/dev/null || true
FROM base AS runtimeRUN apk add --no-cache libpq curl tzdata yaml && adduser -D -h /app railsCOPY --from=build /usr/local/bundle /usr/local/bundleCOPY --from=build /app /appRUN mkdir -p db storage log tmp/pids && chown -R rails:rails db storage log tmpUSER railsEXPOSE 3000
CMD ["sh", "-c", "bin/rails db:prepare && bin/rails server -b 0.0.0.0 -p 3000"]Coolifyでのデプロイ
Docker Composeアプリの場合、Coolifyの設定が少し違います。
管理画面(GUI)の場合:
- ビルドパックでDocker Composeを選択
- ファイル名は
docker-compose.yaml(.ymlだとCoolifyが見つけられない。。。ここでハマった) - ドメインは「Domains for app」欄に入力(サービス名ごとに設定欄がある)
Claude Code MCP(プロンプト)の場合:
coolify-demo-railsリポジトリをDocker Composeでデプロイして。appサービスのドメインはdemo-rails.teraren.comで。Docker Composeの場合、MCPの恩恵が特に大きい。GUIだとdocker_compose_domainsの設定がわかりにくいが、MCPなら「appサービスにこのドメイン」と言えば裏でAPIを叩いて設定してくれる。

動作確認

4つのコンテナが連携して動いています。

Docker Composeの注意点
Coolifyは本来「アプリとDBを個別管理」の思想。Docker Composeを使う場合の注意:
- DB・RedisのバックアップはCoolifyの管理外になる(Compose内のボリュームは自分で管理)
- 個別サービスのスケーリングができない
- 環境変数の管理がCompose内に閉じる(CoolifyのUI経由で変更しにくい)
逆に向いているケース:
- Ollama + App のようにサービス間の結合度が高い場合
depends_onで起動順序の制御が必要な場合- ローカルとCoolifyで同じcomposeを使い回したい場合
ビルドパック比較まとめ
| 観点 | Nixpacks | Dockerfile | Docker Compose |
|---|---|---|---|
| 設定ファイル | 不要 | Dockerfile | docker-compose.yaml + Dockerfile |
| 難易度 | 簡単 | 中 | 複雑 |
| イメージ最適化 | 自動(大きめ) | 自由自在 | 自由自在 |
| マルチコンテナ | 不可 | 不可 | 可能 |
| ビルド時間 | 長め | 制御可能 | 長め |
| 本番向き | △ | ◎ | ○ |
迷ったら:
- とりあえず動かしたい → Nixpacks
- 本番で使う → Dockerfile
- DB込みで丸ごと載せたい → Docker Compose
MCPでやる場合の比較
今回の3つのアプリ、GUIだと各アプリで3〜5画面の操作が必要だった。MCPなら:
| 操作 | GUI | MCP(プロンプト) |
|---|---|---|
| Hono(Nixpacks) | 画面操作4ステップ | プロンプト1つ |
| Go(Dockerfile) | 画面操作4ステップ | プロンプト1つ |
| Rails(Docker Compose) | 画面操作6ステップ + API | プロンプト1つ |
| 3つまとめてデプロイ | 約15分 | 約4分 |
特にDocker Composeのドメイン設定は、GUIだとdocker_compose_domainsのJSON構造を理解する必要がある。MCPなら「appサービスにこのドメイン」と言うだけ。詳しくは第2回を参照。
次回の第4回ではCloudflare Tunnelとの連携を詳しく書きます。自宅サーバを安全に外部公開する方法と、PRプレビュー環境の構築など。
シリーズ記事
- Vercel月額$42→自宅サーバ月額$0。Coolifyで個人サービス基盤を作った話
- Coolifyインストールから「プロンプトでデプロイ」まで:Claude Code MCP実践
- Coolifyハンズオン:Hono・Go・Railsを実際にデプロイしてみる(この記事)
- Cloudflare Tunnel×Coolify:自宅サーバを安全に外部公開する
- API-firstなインフラが生き残る:LLM時代のセルフホスト戦略
- ZabbixでDockerコンテナをリソース監視する:Coolify環境の可視化
- Coolify環境のバックアップ戦略:6つのDBを自動ダンプ+復旧手順
- Coolify環境のログ管理と障害対応:Docker + Zabbix + Discordで運用を回す