Tilt の Live Update: image を作り直さずにコンテナを更新する
- 公開日
- カテゴリ:Tilt
- タグ:Kubernetes,Tilt,学習メモ

Tilt を使ってローカルの kind のクラスタでアプリを動かすと、ソースを保存するたびに image が作り直され、Pod が入れ替わる。1 行の変更でも、image の作成、kind の Node への読み込み、Pod の入れ替えが毎回行われる。
この記事では、Tilt の Live Update を使い、image も Pod も作り直さずに、動いているコンテナの中の実行ファイルだけを置き換えて反映する。Go で書いた小さな API で、保存してから返事が変わるまでに何が起きるかを確認する。
contents
前提
次の状態から始める。
- Docker Desktop が起動している
- kind のクラスタがあり、
kubectl config current-contextがkind-k8s-studyのように kind のクラスタを指している tiltコマンドが入っている(この記事は v0.37.8)- ローカルの開発環境に Go が入っている(この記事は 1.25.0。macOS、Apple Silicon)
Live Update
Live Update は、image を作り直さずに、動いているコンテナの中のファイルを書き換えて反映する Tilt の機能。Tiltfile の docker_build に live_update として、次の手順を書く。
sync: ローカルの開発環境のファイルが変わったら、コンテナの中にコピーするrun: コピーのあとに、コンテナの中でコマンドを実行する
Kubernetes の Pod は Node の上で動き、volume としてつなげるのも Node のディレクトリなので、Docker Compose のようにローカルの開発環境のソースのディレクトリをそのままコンテナにつなぐことはできない。Live Update は、変わったファイルをその都度コンテナにコピーすることで、これに近い動きをする。
Go のアプリで必要になること
PHP や Node.js のように、ソースのファイルをそのまま読んで動く言語なら、ファイルをコピーするだけで反映される。Go の場合、コンテナで動いているのはソースではなく go build で作った実行ファイルなので、保存のたびに次の 3 つが必要になる。
go buildで実行ファイルを作り直す- 実行ファイルをコンテナにコピーする
- コンテナの中のプログラムを起動し直す(動いているプログラムは、実行ファイルを置き換えても古いまま動き続けるため)
この記事では、Tilt の公式ドキュメントの Go の例(Example: Go)と同じく、1 をローカルの開発環境で行い、2 を sync、3 を Tilt の拡張 restart_process で行う。
動かすアプリ
ディレクトリの構成は次のとおり。
app/
├── main.go
├── go.mod
├── Dockerfile # 本番用
├── Dockerfile.tilt # Tilt で開発するときだけ使う
├── .dockerignore
├── .gitignore
├── k8s/
│ ├── deployment.yaml
│ └── service.yaml
└── Tiltfile
API のソース
GET / に 1 行の文字列を返すだけの API。8000 番で待ち受け、起動時とリクエストを 1 件受けるごとにログを 1 行出す。
// main.go
package main
import (
"fmt"
"log"
"net/http"
)
const message = "Hello from Tilt"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
fmt.Fprintln(w, message)
})
log.Println("listening on :8000")
log.Fatal(http.ListenAndServe(":8000", nil))
}
あとで変えるのは const message の行だけ。
Linux 用の実行ファイル
go build はローカルの開発環境で行う。kind の Node とその上のコンテナは Linux で動いているので、macOS で何も指定せずに作った実行ファイルは動かない。GOOS=linux を付けて Linux 用の実行ファイルを作る。
$ CGO_ENABLED=0 GOOS=linux go build -o build/hello-api-server .
$ file build/hello-api-server
# => build/hello-api-server: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, ...
ELF: Linux 用の実行ファイルの形式ARM aarch64: CPU の種類。GOARCHを指定していないので、ローカルの開発環境(Apple Silicon)と同じものになる。kind の Node もローカルの開発環境の Docker の上で動いているので、CPU は同じstatically linked:CGO_ENABLED=0を付けたので、C 言語のライブラリに頼らない 1 つの実行ファイルになっている
このコマンドは、あとで Tiltfile に書いて Tilt に実行させる。できる build/ は .gitignore に書いて Git に入れない。
Dockerfile.tilt
Tilt が image を作るときに使う Dockerfile。ローカルの開発環境で作った実行ファイルを alpine にコピーするだけで、go build は行わない。
# Dockerfile.tilt
FROM alpine:3.24
WORKDIR /app
COPY build/hello-api-server /app/build/hello-api-server
EXPOSE 8000
ENTRYPOINT ["/app/build/hello-api-server"]
本番用の Dockerfile は別に持つ。こちらは Dockerfile の中で go build し、できた実行ファイルだけを gcr.io/distroless/static-debian12:nonroot に入れる 2 段階のもの。
Dockerfile(本番用) | Dockerfile.tilt(開発用) | |
|---|---|---|
go build | Dockerfile の中で行う | 行わない。ローカルの開発環境で作った実行ファイルをコピーする |
| 土台の image | distroless(シェルも tar も入っていない最小の image) | alpine(シェルと tar が入った小さな Linux) |
開発用の土台を alpine にするのは、Live Update がコンテナの中のコマンドを使うため。
- Tilt はファイルをコピーするとき、コンテナの中で
tarを実行してファイルを受け取らせる restart_process拡張は、プログラムを起動し直す合図をコンテナの中のシェル(sh)で送る
distroless にはどちらも入っていない。拡張の README にも、scratch や distroless のようなシェルのない image には対応しないとある(restart_process: Unsupported Cases)。
そのため、Tilt が作る開発用の image と、本番用の image は中身が異なる。本番用の image は CI などが Dockerfile から作り、開発の都合(シェル、ローカルの開発環境で作った実行ファイル)を持ち込まない。
Deployment と Service
YAML は Live Update のために変えるところはない。image: はタグを付けずに hello-api と書き、Tilt が作った image のタグ付きの名前に置き換えて apply する。
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-api
spec:
replicas: 1
selector:
matchLabels:
app: hello-api
template:
metadata:
labels:
app: hello-api
spec:
containers:
- name: api
image: hello-api
ports:
- containerPort: 8000
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: hello-api
spec:
selector:
app: hello-api
ports:
- port: 8000
targetPort: 8000
Tiltfile
# Tiltfile
load('ext://restart_process', 'docker_build_with_restart')
local_resource(
'hello-api-compile',
'CGO_ENABLED=0 GOOS=linux go build -o build/hello-api-server .',
deps=['main.go', 'go.mod'],
)
docker_build_with_restart(
'hello-api',
'.',
dockerfile='Dockerfile.tilt',
entrypoint=['/app/build/hello-api-server'],
only=['./build'],
live_update=[
sync('./build', '/app/build'),
],
)
k8s_yaml(['k8s/deployment.yaml', 'k8s/service.yaml'])
k8s_resource('hello-api', port_forwards=8000, resource_deps=['hello-api-compile'])
restart_process 拡張
1 行目の load は、Tilt の拡張 restart_process から関数 docker_build_with_restart を読み込む。拡張は Tiltfile に関数を追加する部品で、公式のものは GitHub の tilt-dev/tilt-extensions で配布されている。tilt up のときに Tilt が GitHub からコードを取得する。
docker_build_with_restart は、docker_build に次の 2 つを加えたもの。
- image に小さなプログラム
tilt-restart-wrapperを追加し、コンテナの起動コマンドを「tilt-restart-wrapperがentrypointのコマンドを起動する」形に変える。tilt-restart-wrapperは、ファイル/tmp/.restart-procが書き換わると、起動したプログラムを止めて起動し直す live_updateの最後にrun('date > /tmp/.restart-proc')を追加する。コピーが終わるたびにこのファイルが書き換わり、プログラムが起動し直す
コンテナの中は次の形になる。コンテナが最初に起動したプログラムは tilt-restart-wrapper で、これは動き続ける。起動し直すのは、その下の hello-api-server だけ。
コンテナ
└── tilt-restart-wrapper # コンテナが最初に起動したプログラム。動き続ける
└── hello-api-server # tilt-restart-wrapper が起動したアプリ。Live Update のたびに起動し直す
local_resource
local_resource('名前', 'コマンド', deps=[...]) は、ローカルの開発環境でコマンドを実行する resource(Tilt の管理画面の一覧の 1 行)を作る。deps に書いたファイルが変わるたびに、コマンドを実行し直す。
ここでは、main.go か go.mod が変わるたびに、Linux 用の実行ファイル build/hello-api-server を作り直す。
docker_build_with_restart の引数
| 引数 | 意味 |
|---|---|
'hello-api'、'.' | image の名前と、image を作るときに Docker に渡すディレクトリ(docker_build と同じ) |
dockerfile='Dockerfile.tilt' | 開発用の Dockerfile を使う |
entrypoint=[...] | 起動するコマンド。tilt-restart-wrapper が起動し直すのはこのコマンド |
only=['./build'] | Docker に渡すのは build/ だけにする |
live_update=[sync('./build', '/app/build')] | build/ の中身が変わったら、コンテナの /app/build/ にコピーする |
ファイルが変わったとき、Tilt は次の規則で Live Update と image の作り直しを振り分ける(Live Update Reference)。
syncに書いたディレクトリの中のファイル: コンテナにコピーする(Live Update)syncの外で、docker_buildが見張っているディレクトリの中のファイル: image を作り直す
only がなければ、docker_build は '.' の中を全部見張る(.dockerignore に書いた k8s/ と Tiltfile は除く)。main.go は sync の ./build の外なので、保存した時点で image の作り直しになる。only=['./build'] で見張るのを build/ だけにすると、main.go の変更は hello-api-compile だけが受け取り、その結果の build/hello-api-server の変更が Live Update になる。
resource_deps
最初の tilt up では、build/hello-api-server ができるまで image を作れない(Dockerfile.tilt がコピーするファイルがない)。k8s_resource の resource_deps=['hello-api-compile'] で、hello-api-compile が終わってから hello-api の image を作るように順番を決める。
tilt up
Tiltfile のあるディレクトリで tilt up を実行する。Tilt の管理画面(http://localhost:10350/)の一覧には 3 行が並ぶ。
| 行 | 何か |
|---|---|
(Tiltfile) | Tiltfile の読み込み |
hello-api-compile | local_resource。ローカルの開発環境で go build を実行する |
hello-api | image の作成、YAML の apply、port-forward |
hello-api-compile が終わると、hello-api が image を作る。最初の 1 回は Live Update ではなく、image を 2 つ作る。
Dockerfile.tiltから、元になる imagehello-api-tilt_docker_build_with_restart_baseを作る- 1 の image に
tilt-restart-wrapperを加えた imagehello-apiを作る。kind の Node に入るのはこちら
hello-api のログは次のとおり(Docker の build の詳細は一部省略)。
STEP 1/5 — Building Dockerfile: [hello-api-tilt_docker_build_with_restart_base]
Building Dockerfile for platform linux/arm64:
FROM alpine:3.24
WORKDIR /app
COPY build/hello-api-server /app/build/hello-api-server
EXPOSE 8000
ENTRYPOINT ["/app/build/hello-api-server"]
...
STEP 2/5 — Pushing hello-api-tilt_docker_build_with_restart_base:tilt-b42ccfdcdda9e6d0
Skipping push: base image does not need deploy
STEP 3/5 — Building Dockerfile: [hello-api]
Building Dockerfile for platform linux/arm64:
FROM tiltdev/restart-helper:2026-10-21 as restart-helper
FROM hello-api-tilt_docker_build_with_restart_base:tilt-b42ccfdcdda9e6d0
RUN ["touch", "/tmp/.restart-proc"]
RUN ["chmod", "666", "/tmp/.restart-proc"]
COPY --from=restart-helper /tilt-restart-wrapper /
COPY --from=restart-helper /entr /
ENTRYPOINT ["/tilt-restart-wrapper", "--watch_file=/tmp/.restart-proc", "/app/build/hello-api-server"]
...
STEP 4/5 — Pushing hello-api:tilt-0f51adab9627cc1c
Loading image to KIND
...
STEP 5/5 — Deploying
Applying YAML to cluster
Objects applied to cluster:
→ hello-api:service
→ hello-api:deployment
Step 1 - 3.25s (Building Dockerfile: [hello-api-tilt_docker_build_with_restart_base])
Step 2 - 0.00s (Pushing hello-api-tilt_docker_build_with_restart_base:tilt-b42ccfdcdda9e6d0)
Step 3 - 2.85s (Building Dockerfile: [hello-api])
Step 4 - 1.98s (Pushing hello-api:tilt-0f51adab9627cc1c)
Step 5 - 0.06s (Deploying)
DONE IN: 8.13s
STEP 3/5 の Dockerfile は、拡張が作ったもの。最後の ENTRYPOINT で、コンテナの起動コマンドが tilt-restart-wrapper に変わっている。
DONE IN が出たら、Pod、返事、image の名前を確認する。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-api-66bc6695bd-pmtb7 1/1 Running 0 65s
$ curl http://localhost:8000/
# => Hello from Tilt
$ kubectl get deployment hello-api -o jsonpath='{.spec.template.spec.containers[0].image}'
# => hello-api:tilt-0f51adab9627cc1c
ソースの保存から反映まで
main.go の const message の行を変えて保存する。
const message = "Hello from Tilt, live updated"
保存すると、Tilt の管理画面で hello-api-compile と hello-api が順に動く。ログは次のとおり(ファイルのパスは一部省略)。
hello-api-co… │ 1 File Changed: [main.go]
hello-api-co… │ Running cmd: CGO_ENABLED=0 GOOS=linux go build -o build/hello-api-server .
hello-api │ 1 File Changed: [build/hello-api-server]
hello-api │ Will copy 1 file(s) to container: [hello-api-66bc6695bd-pmtb7/api]
hello-api │ - '.../app/build/hello-api-server' --> '/app/build/hello-api-server'
hello-api │ [CMD 1/1] sh -c date > /tmp/.restart-proc
hello-api │ → Container hello-api-66bc6695bd-pmtb7/api updated!
hello-api │ 2026/10/25 03:16:11 listening on :8000
hello-api-compileがmain.goの変更を受け取り、ローカルの開発環境でgo buildを実行したhello-apiがbuild/hello-api-serverの変更を受け取り、コンテナの/app/build/hello-api-serverにコピーした(sync)[CMD 1/1] sh -c date > /tmp/.restart-procは拡張が追加したrun。これを受けてtilt-restart-wrapperがプログラムを起動し直し、listening on :8000がもう一度出ている
tilt up のときのログにあった Building Dockerfile、Loading image to KIND、Applying YAML to cluster は出ていない。
返事、Pod、image の名前を、tilt up のあとと同じコマンドで確認する。
$ curl http://localhost:8000/
# => Hello from Tilt, live updated
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-api-66bc6695bd-pmtb7 1/1 Running 0 5m16s
$ kubectl get deployment hello-api -o jsonpath='{.spec.template.spec.containers[0].image}'
# => hello-api:tilt-0f51adab9627cc1c
tilt up のあと | 保存のあと | |
|---|---|---|
curl の返事 | Hello from Tilt | Hello from Tilt, live updated |
| Pod の名前 | hello-api-66bc6695bd-pmtb7 | 同じ |
RESTARTS | 0 | 0 |
| image のタグ | hello-api:tilt-0f51adab9627cc1c | 同じ |
- Pod の名前が同じなので、Pod は入れ替わっていない
RESTARTSが0のままなので、コンテナも起動し直していない。起動し直したのは、コンテナの中のhello-api-serverだけ- image のタグが同じなので、image は作られていない。コンテナの中の実行ファイルだけが新しく、image の中身とは異なる状態になっている
反映にかかった時間
Live Update のログには、tilt up のときの DONE IN に当たる行が出ない。resource ごとの更新の開始と終了の時刻は、tilt get で確認できる(新しい順に並ぶ)。
$ tilt get uiresource hello-api-compile -o jsonpath='{range .status.buildHistory[*]}{.startTime} {.finishTime}{"\n"}{end}'
2026-10-25T03:16:09.592461Z 2026-10-25T03:16:10.960506Z
2026-10-25T03:11:25.668892Z 2026-10-25T03:11:26.566798Z
$ tilt get uiresource hello-api -o jsonpath='{range .status.buildHistory[*]}{.startTime} {.finishTime}{"\n"}{end}'
2026-10-25T03:16:10.932556Z 2026-10-25T03:16:11.148685Z
2026-10-25T03:11:26.568220Z 2026-10-25T03:11:34.703245Z
1 行目が保存のときの更新。hello-api-compile(go build)が約 1.4 秒、hello-api(コピーと起動し直し)が約 0.2 秒で、保存から反映まで約 1.6 秒だった。2 行目の tilt up のときの hello-api は、image の作成を含めて約 8.1 秒。
Pod が作り直されたとき
Live Update で置き換わったのは、動いているコンテナの中のファイルだけ。Pod が作り直されると、新しいコンテナは image から起動するので、tilt up のときに image に入れた実行ファイルで動く。
tilt up をやり直したときは、最初の 1 回は必ず image を作るところから行う(Live Update Reference)。そのときは hello-api-compile が今の main.go から実行ファイルを作り直すので、新しい image には最新の実行ファイルが入る。
tilt down
tilt up のターミナルで Ctrl+C を押してから、同じディレクトリで tilt down を実行すると、Tiltfile の k8s_yaml で入れた Deployment と Service が消える。
$ tilt down
ローカルの開発環境の build/hello-api-server と、Docker の image(hello-api:tilt-...、hello-api-tilt_docker_build_with_restart_base:tilt-...、tiltdev/restart-helper)は残る。
まとめ
- Live Update は、image を作り直さずに、動いているコンテナの中のファイルを書き換えて反映する。
docker_buildのlive_updateにsync(コピー)とrun(コンテナの中のコマンド)を書く - Go のアプリでは、
local_resourceでローカルの開発環境で Linux 用の実行ファイルを作り、syncでコンテナにコピーし、restart_process拡張のdocker_build_with_restartでプログラムを起動し直す restart_process拡張と Live Update のコピーはコンテナの中のシェルとtarを使うので、開発用の image はdistrolessではなくalpineなどにする。本番用の Dockerfile とは分けるsyncの外でdocker_buildが見張っているファイルが変わると、image の作り直しになる。onlyで見張るのをbuild/に絞り、main.goの変更をlocal_resourceだけに受け取らせる- 保存のあとも、Pod の名前・
RESTARTS・image のタグは変わらない。起動し直すのはコンテナの中のプログラムだけ
参考 URL
- [official] Live Update Reference | Tilt
- [official] Example: Go | Tilt
- [official] Tiltfile API Reference: sync | Tilt
- [official] Tiltfile API Reference: run | Tilt
- [official] Tiltfile API Reference: local_resource | Tilt
- [official] Setting up Local Commands, Servers, and Tests | Tilt
- [official] Extensions | Tilt
- [official] tilt-extensions/restart_process at master · tilt-dev/tilt-extensions · GitHub
- [official] GitHub - tilt-dev/tilt-example-go: An example project that demonstrates live Go development in Kubernetes

