
ローカル LLM(Ollama + Open WebUI)を Argo CDでデプロイしてみた
orbstack + Argo CD でローカル LLM 環境(Ollama + Open WebUI)を GitOps デプロイしてみた
はじめに
クラウド事業本部、あきやまです。
みなさんはローカル環境でLLM 動かしたい!!!という衝動に駆られたことはありますか?私はあります。
やるなら他の技術も色々試したいと思い手元の Mac 上に Kubernetes クラスタを立ち上げ Ollama と Open WebUI を Argo CD 経由でデプロイし、Private GitHub リポジトリと繋げるところまでを一通りやってみました。
環境
| 項目 | 値 |
|---|---|
| OS | macOS |
| チップ | Apple M4 |
| メモリ | 16GB |
| Kubernetes ランタイム | orbstack |
| kubectl | v1.29 以上 |
| Argo CD | stable(公式 install.yaml) |
| Ollama | ollama/ollama:latest |
| Open WebUI | ghcr.io/open-webui/open-webui:main |
| GitHub リポジトリ | Private(PAT 認証) |
結論
- orbstack の Kubernetes を使えば、Docker Desktop や minikube なしでもローカルクラスタを即座に立ち上げられます
- Argo CD の App of Apps パターン により、
apps/配下にマニフェストを追加するだけで新しいアプリが自動登録される構成が作れます - Private リポジトリ接続は
argocd.argoproj.io/secret-type: repositoryラベル付き Secret を apply するだけで完結します(PAT は Secret に格納し、git には載せない) - CPU のみでも
gemma2:2bなど小型モデルであれば十分動作し、Open WebUI からチャット可能でした
以降で具体的な手順を示します。
構成図
全体像は以下の通りです。
ディレクトリ構成
最終的な全ファイルは以下の通りです。このまま同じ構成を作れば再現できます。
localllm/
├── bootstrap/
│ └── root-app.yaml # App of Apps(apps/ を watch)
├── apps/
│ ├── ollama.yaml # ArgoCD Application: Ollama
│ └── openwebui.yaml # ArgoCD Application: Open WebUI
├── manifests/
│ ├── ollama/
│ │ ├── kustomization.yaml # resources を束ねる
│ │ ├── namespace.yaml # Namespace: ollama
│ │ ├── deployment.yaml # Ollama 本体
│ │ ├── service.yaml # ClusterIP :11434
│ │ └── pvc.yaml # モデル保存用 PVC (20Gi)
│ └── openwebui/
│ ├── kustomization.yaml
│ ├── namespace.yaml # Namespace: openwebui
│ ├── deployment.yaml # Open WebUI 本体
│ ├── service.yaml # ClusterIP :8080
│ └── pvc.yaml # 設定・履歴用 PVC (2Gi)
└── scripts/
├── 01-start-minikube.sh # orbstack の context 切り替え
├── 02-install-argocd.sh # Argo CD 本体インストール
├── 03-register-repo.sh # Private repo 認証情報を登録
├── 04-bootstrap.sh # root Application を apply
└── pull-model.sh # Ollama にモデルを pull
ファイル数は 17 個です。それぞれの中身を以降のステップで順番に示します。
やってみた
Step 1: orbstack の Kubernetes を有効化
orbstack の設定画面から Kubernetes を有効にし、kubectl のコンテキストを切り替えます。
#!/usr/bin/env bash
set -euo pipefail
kubectl config use-context orbstack
kubectl cluster-info
$ ./scripts/01-start-minikube.sh
Switched to context "orbstack".
Kubernetes control plane is running at https://127.0.0.1:xxxxx
※ orbstack は Docker Desktop の代替として使えるランタイムで、軽量な Kubernetes が内蔵されています。minikube 等と違って別途 VM を起動する必要がありません。
Step 2: Argo CD をインストール
Argo CD 本体は imperative(命令的)にインストールします。Argo CD で Argo CD 自身を管理する鶏と卵の問題を避けるためです。
#!/usr/bin/env bash
set -euo pipefail
kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml \
--server-side=true
kubectl -n argocd wait --for=condition=available deploy/argocd-server --timeout=300s
# 初期パスワードを表示
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d; echo
--server-side=true を付けているのは、install.yaml に含まれる大きな CRD のサイズがクライアントサイド apply の last-applied annotation 上限を超える可能性があるためです。
Step 3: Private リポジトリ認証情報を登録
Argo CD が Private GitHub リポジトリを fetch できるように、PAT を Secret に入れて apply します。
PAT は事前に GitHub の Settings から発行しておきます。
- Classic PAT:
repoスコープを有効化 - Fine-grained PAT: 対象リポジトリに限定し、
Contents: Read-onlyを付与
PAT は git にコミットしない運用にします。
#!/usr/bin/env bash
set -euo pipefail
: "${GITHUB_USERNAME:?Set GITHUB_USERNAME}"
: "${GITHUB_PAT:?Set GITHUB_PAT}"
REPO_URL="https://github.com/<your-org>/<your-repo>.git"
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: repo-localllm
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: ${REPO_URL}
username: ${GITHUB_USERNAME}
password: ${GITHUB_PAT}
EOF
実行時は環境変数で PAT を渡します。
export GITHUB_USERNAME=<your-github-username>
export GITHUB_PAT=ghp_xxxxxxxxxxxx
./scripts/03-register-repo.sh
Argo CD は argocd.argoproj.io/secret-type: repository ラベル付き Secret を自動検知し、その URL に一致する Application から利用します。これで CLI(argocd repo add)を使わずに宣言的に登録できます。
Step 4: App of Apps(root Application)を apply
bootstrap/root-app.yaml は apps/ ディレクトリを watch し、配下の Application CR を自動登録する役割を持ちます。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/<your-org>/<your-repo>.git
targetRevision: HEAD
path: apps
directory:
recurse: true
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ServerSideApply=true
この root Application を apply するためのラッパースクリプトを用意しておきます。
#!/usr/bin/env bash
set -euo pipefail
# App of Apps(root Application)を apply
# 以降のアプリ追加・変更は git push のみで反映される
kubectl apply -f bootstrap/root-app.yaml
echo ""
echo "Bootstrap completed. Monitoring sync status..."
kubectl -n argocd get applications
実行します。
./scripts/04-bootstrap.sh
以降は apps/*.yaml に Application を追加するだけで自動デプロイされます。
Step 5: 各アプリの Application 定義
apps/ollama.yaml と apps/openwebui.yaml の 2 ファイルを作成します。それぞれ manifests/<app> 配下の Kustomize を指します。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ollama
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/<your-org>/<your-repo>.git
targetRevision: HEAD
path: manifests/ollama
destination:
server: https://kubernetes.default.svc
namespace: ollama
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: openwebui
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/<your-org>/<your-repo>.git
targetRevision: HEAD
path: manifests/openwebui
destination:
server: https://kubernetes.default.svc
namespace: openwebui
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
違いは path(参照する manifests ディレクトリ)と destination.namespace の 2 箇所だけです。
Step 6: Ollama のマニフェスト
manifests/ollama/ 配下に 5 ファイルを作ります。
kustomization.yaml
Kustomize がこのディレクトリの入り口になります。resources: で束ねる全ファイルを列挙します。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ollama
resources:
- namespace.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
labels:
- includeSelectors: false
pairs:
app.kubernetes.io/part-of: localllm
namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: ollama
pvc.yaml
Ollama はモデルを /root/.ollama に保存します。Pod 再起動で消えないよう PVC を用意します。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ollama-models
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
deployment.yaml
CPU 推論用にリソース要求を多めに設定しています。PVC が ReadWriteOnce なので、strategy.type: Recreate にして新旧 Pod が同時にマウントしないようにしています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: ollama
labels:
app.kubernetes.io/name: ollama
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ollama
template:
metadata:
labels:
app.kubernetes.io/name: ollama
spec:
containers:
- name: ollama
image: ollama/ollama:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 11434
env:
- name: OLLAMA_HOST
value: "0.0.0.0:11434"
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
cpu: "4"
memory: 8Gi
volumeMounts:
- name: models
mountPath: /root/.ollama
readinessProbe:
tcpSocket:
port: 11434
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 11434
initialDelaySeconds: 30
periodSeconds: 30
volumes:
- name: models
persistentVolumeClaim:
claimName: ollama-models
service.yaml
apiVersion: v1
kind: Service
metadata:
name: ollama
labels:
app.kubernetes.io/name: ollama
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: ollama
ports:
- name: http
port: 11434
targetPort: http
protocol: TCP
Step 7: Open WebUI のマニフェスト
manifests/openwebui/ 配下に同じく 5 ファイルを作ります。Ollama と同じ構造ですが、env で Ollama の Service DNS を指定するのがポイントです。
kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: openwebui
resources:
- namespace.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
labels:
- includeSelectors: false
pairs:
app.kubernetes.io/part-of: localllm
namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: openwebui
pvc.yaml
Open WebUI は設定・履歴を /app/backend/data に保存します。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: openwebui-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
deployment.yaml
OLLAMA_BASE_URL に Ollama Service の FQDN を指定します。<service>.<namespace>.svc.cluster.local の形式です。
apiVersion: apps/v1
kind: Deployment
metadata:
name: openwebui
labels:
app.kubernetes.io/name: openwebui
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: openwebui
template:
metadata:
labels:
app.kubernetes.io/name: openwebui
spec:
containers:
- name: openwebui
image: ghcr.io/open-webui/open-webui:main
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
- name: OLLAMA_BASE_URL
value: "http://ollama.ollama.svc.cluster.local:11434"
- name: WEBUI_AUTH
value: "False"
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
volumeMounts:
- name: data
mountPath: /app/backend/data
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
volumes:
- name: data
persistentVolumeClaim:
claimName: openwebui-data
※ WEBUI_AUTH=False は検証用途のみ。本番では必ず認証を有効にしてください。
service.yaml
apiVersion: v1
kind: Service
metadata:
name: openwebui
labels:
app.kubernetes.io/name: openwebui
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: openwebui
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
Step 8: モデルを pull
デプロイが Synced / Healthy になったら、Ollama Pod に exec してモデルを pull します。CPU 推論でも現実的な速度で動く軽量モデル(gemma2:2b など)がおすすめです。
#!/usr/bin/env bash
set -euo pipefail
MODEL="${1:-llama3.2}"
kubectl exec -n ollama deploy/ollama -- ollama pull "${MODEL}"
kubectl exec -n ollama deploy/ollama -- ollama list
$ ./scripts/pull-model.sh llama3.2
Pulling model: llama3.2
pulling manifest
pulling dde5aa3fc5ff: 100% ▕██████████████████▏ 2.0 GB
pulling 966de95ca8a6: 100% ▕██████████████████▏ 1.4 KB
pulling fcc5a6bec9da: 100% ▕██████████████████▏ 7.7 KB
pulling a70ff7e570d9: 100% ▕██████████████████▏ 6.0 KB
pulling 56bb8bd477a5: 100% ▕██████████████████▏ 96 B
pulling 34bb5ab01051: 100% ▕██████████████████▏ 561 B
verifying sha256 digest
writing manifest
success
Installed models:
NAME ID SIZE MODIFIED
llama3.2:latest a80c4f17acd5 2.0 GB Less than a second ago
Step 9: Open WebUI から動作確認
port-forward でブラウザから Open WebUI にアクセスします。
kubectl port-forward -n openwebui svc/openwebui 3000:8080
http://localhost:3000 を開くと、先ほど pull したモデルが選択肢に表示され、チャットができました。
ローカルに閉じられた環境で無事デプロイできました!今後ファインチューニングしていきたいですね。

注意点・制約
PAT の扱い
- PAT は絶対に git にコミットしない
- 発行時は Fine-grained PAT で対象リポジトリと権限を最小化する
- Secret を
kubectl applyする際のシェル履歴にも注意(HISTCONTROL=ignorespaceを使う、.envrc で管理する 等)
ストレージ容量
Ollama のモデルは 1 モデルで数 GB あります。PVC を 20 Gi で確保していますが、複数モデルを入れる場合は拡張が必要です。
CPU 推論の速度
CPU のみで動かす場合、7B 以上のモデルは体感的にかなり遅くなります。検証目的では以下のような小型モデルが実用的でした。
| モデル | サイズ | 体感(初回トークンまでの応答時間) |
|---|---|---|
gemma2:2b |
1.6 GB | 応答がスムーズ |
qwen2.5:3b |
2.0 GB | ほぼ問題なし |
llama3.2:latest(3B) |
2.0 GB | 5〜10 秒程度。実用可能 |
llama3.1:8b |
4.7 GB | 明らかに遅い |
※ 応答時間は手元の orbstack(CPU のみ)で短めのプロンプトを投げた際の体感値です。プロンプト長や生成トークン数で大きく変わるのであくまで目安としてください。
PVC の ReadWriteOnce
Deployment の strategy を Recreate にしています。RollingUpdate だと新旧 Pod が同時に PVC をマウントしようとして失敗するためです。
Open WebUI の認証
WEBUI_AUTH=False は検証限定。ローカル以外に公開する場合は必ず True にして認証経路を設定します。
まとめ
今回の検証で実現できたことは以下の通りです。
- orbstack の Kubernetes 上で Ollama と Open WebUI を動作させた
- Argo CD の App of Apps パターンで複数アプリを宣言的に管理した
- Private GitHub リポジトリへの接続を PAT + Declarative Secret で実現した
- 以降の変更は
git pushのみで反映される GitOps フローを確立した
orbstack が想像以上に軽快で、minikube や kind を使わずに Kubernetes 検証ができるのは大きな発見でした。また Argo CD の App of Apps は、apps/ 配下にファイルを置くだけで新規アプリが追加されるので、学習用・検証用環境の管理として非常に使いやすいと感じました。
ローカル LLM 環境を GitOps で管理する構成を検討している方の参考になれば幸いです。








