diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index badcf7bd7..2adec7410 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -151,6 +151,7 @@ jobs:
               container-image: "hubble"
             - dir: "./hubble-relay"
               container-image: "hubble-relay"
+              make-post-targets: "cilium-checkout cilium-test-e2e-setup cilium-test-e2e-upgrade-inotify test-e2e"
               request-scan: "false"
             - dir: "./hubble-ui"
               container-image: "hubble-ui-frontend"
diff --git a/cilium/e2e/Makefile b/cilium/e2e/Makefile
index 6c3018e03..6e09f9f41 100644
--- a/cilium/e2e/Makefile
+++ b/cilium/e2e/Makefile
@@ -1,8 +1,10 @@
 SUDO ?= sudo
-IMAGE_TAG ?= ghcr.io/cybozu/cilium:$(shell cat ../TAG)
 CILIUM := bin/cilium
 CILIUM_DIR := ../src/cilium
 
+CILIUM_AGENT_IMAGE_TAG ?= ghcr.io/cybozu/cilium:$(shell cat ../TAG)
+HUBBLE_RELAY_IMAGE_TAG ?= ghcr.io/cybozu/hubble-relay:$(shell cat ../../hubble-relay/TAG)
+
 .PHONY: setup
 setup:
 	mkdir -p bin
@@ -24,12 +26,28 @@ upgrade-inotify:
 .PHONY: start
 start:
 	cd $(CILIUM_DIR); ./contrib/scripts/kind.sh --xdp "" 3 "" "" "none" "ipv4"
-	kind load docker-image $(IMAGE_TAG)
+	kind load docker-image $(CILIUM_AGENT_IMAGE_TAG)
+	$(CILIUM) install --wait \
+		--chart-directory=$(CILIUM_DIR)/install/kubernetes/cilium \
+		--values values.yaml \
+		--set image.repository=$(shell echo $(CILIUM_AGENT_IMAGE_TAG) | cut -d':' -f1) \
+		--set image.tag=$(shell echo $(CILIUM_AGENT_IMAGE_TAG) | cut -d':' -f2)
+
+.PHONY: start-hubble-relay
+start-hubble-relay:
+	cd $(CILIUM_DIR); ./contrib/scripts/kind.sh --xdp "" 3 "" "" "none" "ipv4"
+	CILIUM_AGENT_IMAGE_TAG=quay.io/cilium/cilium:v$(shell echo $(HUBBLE_RELAY_IMAGE_TAG) | cut -d':' -f2 | cut -d'.' -f1-3); \
+	docker pull $${CILIUM_AGENT_IMAGE_TAG}; \
+	kind load docker-image $${CILIUM_AGENT_IMAGE_TAG}
+	kind load docker-image $(HUBBLE_RELAY_IMAGE_TAG)
 	$(CILIUM) install --wait \
 		--chart-directory=$(CILIUM_DIR)/install/kubernetes/cilium \
 		--values values.yaml \
-		--set image.repository=$(shell echo $(IMAGE_TAG) | cut -d':' -f1) \
-		--set image.tag=$(shell echo $(IMAGE_TAG) | cut -d':' -f2) \
+		--set hubble.relay.image.repository=$(shell echo $(HUBBLE_RELAY_IMAGE_TAG) | cut -d':' -f1) \
+		--set hubble.relay.image.tag=$(shell echo $(HUBBLE_RELAY_IMAGE_TAG) | cut -d':' -f2) \
+		--set hubble.relay.image.pullPolicy=Never \
+		--set hubble.relay.securityContext.runAsUser=10000 \
+		--set hubble.relay.securityContext.runAsGroup=10000
 
 # check-log-errors is disabled in CI to accomodate with the following issue:
 # https://github.com/cilium/image-tools/pull/267
diff --git a/cilium/e2e/values.yaml b/cilium/e2e/values.yaml
index 34547d846..f2e2af2be 100644
--- a/cilium/e2e/values.yaml
+++ b/cilium/e2e/values.yaml
@@ -1,6 +1,12 @@
 autoDirectNodeRoutes: true
 devices: eth+
 enableIPv6Masquerade: false
+hubble:
+  enabled: true
+  relay:
+    enabled: true
+    image:
+      useDigest: false
 image:
   pullPolicy: Never
   useDigest: false
diff --git a/hubble-relay/BRANCH b/hubble-relay/BRANCH
index 63738cc28..d40acaaea 100644
--- a/hubble-relay/BRANCH
+++ b/hubble-relay/BRANCH
@@ -1 +1 @@
-1.14
+1.15
diff --git a/hubble-relay/Dockerfile b/hubble-relay/Dockerfile
index e33fc354b..e0ce8e62b 100644
--- a/hubble-relay/Dockerfile
+++ b/hubble-relay/Dockerfile
@@ -1,8 +1,8 @@
-ARG BASE_IMAGE=ghcr.io/cybozu/ubuntu:22.04
+ARG UBUNTU_IMAGE=ghcr.io/cybozu/ubuntu:22.04
 ARG GOLANG_IMAGE=ghcr.io/cybozu/golang:1.22-jammy
 
 # Stage1: build
-FROM ${GOLANG_IMAGE} as build
+FROM ${GOLANG_IMAGE} AS build
 
 COPY TAG /
 
@@ -22,8 +22,13 @@ RUN VERSION=$(cut -d \. -f 1,2,3 < /TAG ) \
 WORKDIR /go/src/github.com/cilium/cilium/hubble-relay
 RUN make
 
+# grpc_health_probe
+WORKDIR /go/src/github.com/cilium/cilium
+RUN cp images/hubble-relay/download-grpc-health-probe.sh /tmp/download-grpc-health-probe.sh
+RUN /tmp/download-grpc-health-probe.sh
+
 # Stage2: runtime
-FROM ${BASE_IMAGE}
+FROM ${UBUNTU_IMAGE}
 LABEL org.opencontainers.image.source="https://github.com/cybozu/neco-containers"
 
 # Add nonroot user for the neco environment
@@ -31,10 +36,12 @@ RUN useradd nonroot -u 10000 -U -m
 
 COPY --from=build /out/linux/amd64/bin/gops /bin/gops
 COPY --from=build /go/src/github.com/cilium/cilium/LICENSE.all /LICENSE
+COPY --from=build /out/linux/amd64/bin/grpc_health_probe /bin/grpc_health_probe
 COPY --from=build /go/src/github.com/cilium/cilium/hubble-relay/hubble-relay /usr/bin/hubble-relay
 
 # use uid:gid for the nonroot user for compatibility with runAsNonRoot
 USER 10000:10000
 
+ENV HOME=/home/nonroot
 ENTRYPOINT ["/usr/bin/hubble-relay"]
 CMD ["serve"]
diff --git a/hubble-relay/Makefile b/hubble-relay/Makefile
new file mode 100644
index 000000000..6f3ed8c53
--- /dev/null
+++ b/hubble-relay/Makefile
@@ -0,0 +1,24 @@
+IMAGE_TAG ?= ghcr.io/cybozu/hubble-relay:$(shell cat TAG)
+
+.PHONY: build
+build:
+	docker build . --tag=$(IMAGE_TAG)
+
+.PHONY: cilium-checkout
+cilium-checkout:
+	@$(MAKE) -C ../cilium checkout
+
+.PHONY: cilium-test-e2e-setup
+cilium-test-e2e-setup:
+	@$(MAKE) -C ../cilium test-e2e-setup
+
+.PHONY: cilium-test-e2e-upgrade-inotify
+cilium-test-e2e-upgrade-inotify:
+	@$(MAKE) -C ../cilium test-e2e-upgrade-inotify
+
+.PHONY: test-e2e
+test-e2e:
+	@$(MAKE) -C ../cilium/e2e start-hubble-relay
+	kubectl wait deployments -A --all --for condition=Available --timeout=300s
+	kubectl wait pods -A --all --for condition=Ready --timeout=300s
+	@$(MAKE) -C ../cilium/e2e stop
diff --git a/hubble-relay/TAG b/hubble-relay/TAG
index 26a572773..505c082d2 100644
--- a/hubble-relay/TAG
+++ b/hubble-relay/TAG
@@ -1 +1 @@
-1.14.14.1
+1.15.12.1