Skip to content

Commit 11c7e5e

Browse files
h4x3rotabclaude
andcommitted
feat(consul-postgres-ha): wire up service-VIP plane end-to-end
Rebuild the entrypoint, compose, terraform, and per-workload glue on top of the new mesh-conn shape: - mesh-sidecar/entrypoint.sh: provisions peer VIPs (127.50.0.0/24) for every PEERS_JSON entry and service VIPs (127.10.0.0/24) for every UPSTREAMS_JSON entry. Consul advertises 127.50.0.<self-vip>; retry-joins build off COORDINATOR_VIPS. Workers register a postgres Connect sidecar at port 21001 and supervise both Envoys (webdemo at 21000, postgres at 21001). Coord-0 writes the cluster-wide config entries (proxy-defaults, postgres service-resolver master/replica subsets, default-allow intentions) once quorum is visible. - compose/{coordinator,worker}.yaml: drop PROTOCOL_BASES / COORDINATOR_*_PORTS, add PEERS_JSON+vip / UPSTREAMS_JSON / COORDINATOR_VIPS, add NET_ADMIN to the sidecar (needed for `ip addr add` on lo). - cluster-example/cluster.tf: drop the protocol_bases map + the per-protocol port arithmetic; PEERS_JSON shrinks to [{id, vip}] with vip=ordinal+1; service_vips local declares the three upstream services (webdemo, postgres-master, postgres-replica) the workers consume. - patroni/entrypoint.sh: postgres on canonical 127.0.0.1:5432, REST on 127.0.0.1:8008, DCS at 127.0.0.1:8500; connect_address set to the constant string `postgres-master:5432` so consumers resolve via /etc/hosts → local Envoy → mTLS to current leader. - webdemo/main.go: register sidecar at 127.50.0.<self-vip>:21000, declare webdemo upstream at 127.10.0.<webdemo-vip>:8080 (vip parsed from UPSTREAMS_JSON env), fan out via http://webdemo:8080. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b3eab5f commit 11c7e5e

6 files changed

Lines changed: 446 additions & 251 deletions

File tree

consul-postgres-ha/cluster-example/cluster.tf

Lines changed: 61 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
# Stage 4example cluster.
1+
# Example clusterpeer-VIP / service-VIP layout.
22
#
3-
# This whole HCL file IS the cluster definition. To bring up a 4-instance
4-
# Consul + Connect mesh on dstack:
3+
# This whole HCL file IS the cluster definition. To bring up an
4+
# HA Postgres + webdemo cluster on dstack:
55
#
6-
# PHALA_CLOUD_API_KEY=$(your token) terraform apply
6+
# PHALA_CLOUD_API_KEY=$(your token) terraform apply -parallelism=1
77
#
88
# Adding a worker is a `replicas` bump on phala_app.worker; terraform
99
# apply propagates the new PEERS_JSON to every CVM via in-place env
10-
# update (no destroy/recreate; disks survive — verified in
11-
# stage4-experiments/disk-persistence/).
10+
# update (no destroy/recreate; disks survive).
1211

1312
terraform {
1413
required_version = ">= 1.5"
@@ -47,6 +46,10 @@ provider "phala" {}
4746
# them in Terraform and hand the same bytes to every phala_app via
4847
# env. Trade-off accepted: anyone with read access to terraform.tfstate
4948
# (or the apply host's memory) sees plaintext keys. Stage 2 closes this.
49+
#
50+
# Connect CA root is NOT in this list — Consul's built-in CA provider
51+
# generates the root in Raft on first quorum, no external derivation
52+
# required and no per-CVM problem.
5053
resource "random_bytes" "gossip_key" {
5154
length = 32
5255
}
@@ -80,92 +83,70 @@ variable "gateway_domain" {
8083
description = "Phala dstack gateway domain (e.g. dstack-pha-prod5.phala.network)"
8184
}
8285

83-
# Image references. Gap 2 collapsed bootstrap-secrets, mesh-conn, the
84-
# legacy keepalive placeholder, and the old envoy-only sidecar into
85-
# one `mesh_sidecar_image` (consul-postgres-ha-mesh-sidecar) — workers
86-
# and coordinators both reference it and the entrypoint dispatches on
87-
# ROLE. The `signaling` image is still published by CI (used by the
88-
# external Vultr coordinator), but no dstack CVM in this cluster
89-
# references it, so it isn't a Terraform input here.
86+
# Image references. The mesh_sidecar image bundles
87+
# bootstrap-secrets, mesh-conn, consul agent, and envoy; workers and
88+
# coordinators both reference it and the entrypoint dispatches on ROLE.
9089
variable "mesh_sidecar_image" { type = string }
9190
variable "webdemo_image" { type = string }
9291
variable "patroni_image" { type = string }
9392

9493
# External coordinator (Vultr coturn + signaling box). Used until
9594
# Phala admin enables UDP ingress on dstack apps; once that lands we
9695
# can host coturn + signaling inside the dstack mesh and drop these
97-
# external_* vars. The dstack-coordinator compose no longer carries
98-
# unused local copies of those services.
96+
# external_* vars.
9997
variable "external_coordinator_host" { type = string }
10098
variable "external_signaling_url" { type = string }
10199
variable "external_turn_secret" {
102100
type = string
103101
sensitive = true
104102
}
105103

106-
# ---------- Protocol port plan ----------
104+
# ---------- Cluster topology + VIP allocation ----------
107105

108106
locals {
109-
# Index i is the same protocol on every peer; the per-peer port for
110-
# protocol `name` at ordinal `n` is base + n. mesh-conn reads
111-
# /run/instance/info.json for this peer's actual ports (computed by
112-
# bootstrap-secrets from PROTOCOL_BASES + the ordinal it claimed).
113-
protocol_bases = {
114-
serf_lan = 18000
115-
server_rpc = 18100
116-
http_api = 18200
117-
grpc = 18300
118-
webdemo = 18500
119-
sidecar_public = 18600
120-
postgres = 18700 # Patroni-managed PostgreSQL listen
121-
patroni_rest = 18800 # Patroni REST API (peer health, leader query)
122-
}
123-
124-
# The full peer list, identical on every CVM. Coordinators occupy
125-
# ordinals 0..C-1 (where C = coordinator_replicas), workers fill
126-
# ordinals C..C+W-1. PEERS_JSON is what mesh-conn consumes; the
127-
# role-ordinal pair is what each peer self-identifies as in its
128-
# bootstrap-secrets-derived /run/instance/info.json (mesh-conn then
129-
# reads "<role>-<ordinal>" as its self ID).
107+
# Peer VIPs: 127.50.0.<vip>/32. Allocated as ordinal+1 (so vip=1
108+
# for the first coordinator, never 0 — the validate_test in
109+
# mesh-conn rejects vip=0). PEERS_JSON shape: [{id, vip}], same
110+
# on every CVM. mesh-conn binds the static infra-port allowlist
111+
# ({21000, 21001, 8300, 8301}) on every other peer's VIP.
130112
peers = concat(
131113
[
132114
for i in range(var.coordinator_replicas) : {
133115
id = "coordinator-${i}"
134116
ordinal = i
135117
role = "coordinator"
118+
vip = i + 1
136119
}
137120
],
138121
[
139122
for i in range(var.worker_replicas) : {
140-
# ID must match mesh-conn's self_id, which is `role-ordinal`,
141-
# NOT slot. Workers occupy ordinals C..C+W-1.
142123
id = "worker-${i + var.coordinator_replicas}"
143124
ordinal = i + var.coordinator_replicas
144125
role = "worker"
126+
vip = i + var.coordinator_replicas + 1
145127
}
146128
],
147129
)
148130

149131
peers_json = jsonencode([
150-
for p in local.peers : {
151-
id = p.id
152-
ports = [for proto, base in local.protocol_bases : base + p.ordinal]
153-
}
132+
for p in local.peers : { id = p.id, vip = p.vip }
154133
])
155134

156-
protocol_bases_json = jsonencode(local.protocol_bases)
157-
158-
# Comma-separated lists of coordinator-ordinal-shifted ports. Workers
159-
# use COORDINATOR_SERF_PORTS to retry-join EVERY coordinator, and
160-
# COORDINATOR_HTTP_PORTS to pick ANY coordinator's HTTP API for
161-
# KV-CAS bootstrapping. Coordinators use COORDINATOR_SERF_PORTS to
162-
# gossip-join their server peers (consul -bootstrap-expect=N).
163-
coordinator_serf_ports = join(",", [for i in range(var.coordinator_replicas) : tostring(local.protocol_bases.serf_lan + i)])
164-
coordinator_http_ports = join(",", [for i in range(var.coordinator_replicas) : tostring(local.protocol_bases.http_api + i)])
165-
166-
# First coordinator's HTTP port — used as a single endpoint for the
167-
# consul-ui output and for legacy single-coord callers.
168-
coordinator_http_port_first = local.protocol_bases.http_api + 0
135+
# COORDINATOR_VIPS — comma-separated for serf retry-join.
136+
coordinator_vips = join(",", [for p in local.peers : tostring(p.vip) if p.role == "coordinator"])
137+
138+
# Service VIPs: 127.10.0.<vip>/32 — one per Connect upstream a
139+
# worker consumes. Three services in this template (extend by
140+
# adding entries here + Connect upstreams in the sidecar config):
141+
# webdemo for the cross-peer fan-out demo
142+
# postgres-master the Patroni leader, leader-aware via subset filter
143+
# postgres-replica any Patroni replica
144+
service_vips = [
145+
{ name = "webdemo", vip = 10, port = 8080 },
146+
{ name = "postgres-master", vip = 20, port = 5432 },
147+
{ name = "postgres-replica", vip = 21, port = 5432 },
148+
]
149+
upstreams_json = jsonencode(local.service_vips)
169150
}
170151

171152
# ---------- Coordinator ----------
@@ -186,16 +167,15 @@ resource "phala_app" "coordinator" {
186167
docker_compose = file("${path.module}/../compose/coordinator.yaml")
187168

188169
env = {
189-
CLUSTER_NAME = var.cluster_name
190-
PROTOCOL_BASES = local.protocol_bases_json
191-
PEERS_JSON = local.peers_json
192-
COORDINATOR_ORDINAL = tostring(each.value)
193-
BOOTSTRAP_EXPECT = tostring(var.coordinator_replicas)
194-
COORDINATOR_SERF_PORTS = local.coordinator_serf_ports
195-
SIGNALING_URL = var.external_signaling_url
196-
TURN_HOST = var.external_coordinator_host
197-
TURN_SHARED_SECRET = var.external_turn_secret
198-
MESH_SIDECAR_IMAGE = var.mesh_sidecar_image
170+
CLUSTER_NAME = var.cluster_name
171+
PEERS_JSON = local.peers_json
172+
COORDINATOR_ORDINAL = tostring(each.value)
173+
BOOTSTRAP_EXPECT = tostring(var.coordinator_replicas)
174+
COORDINATOR_VIPS = local.coordinator_vips
175+
SIGNALING_URL = var.external_signaling_url
176+
TURN_HOST = var.external_coordinator_host
177+
TURN_SHARED_SECRET = var.external_turn_secret
178+
MESH_SIDECAR_IMAGE = var.mesh_sidecar_image
199179
# Stage-1 WORKAROUND — see `random_bytes` block at top of file.
200180
GOSSIP_KEY = random_bytes.gossip_key.base64
201181
}
@@ -215,17 +195,6 @@ resource "phala_app" "worker" {
215195
# app with replicas:N. Reason: each worker needs its OWN ordinal
216196
# passed in via env so bootstrap-secrets can write the correct
217197
# /run/instance/info.json without a Consul KV CAS round-trip.
218-
# The CAS path has a chicken-and-egg: workers need Consul to
219-
# claim an ordinal, but Consul (on the coordinator) is reached
220-
# via mesh-conn, which depends on bootstrap-secrets having
221-
# finished. Per-worker resources sidestep this entirely.
222-
#
223-
# Once phala-cloud#243 lands phala_app_instance + per-instance
224-
# env, this reverts to one resource with replicas:N + per-instance
225-
# env block.
226-
# Key is the worker's 1-based slot (used in the CVM name); value is
227-
# the cluster-wide ordinal (= slot + coordinator_replicas, since
228-
# coordinators occupy ordinals 0..C-1).
229198
for_each = { for i in range(var.worker_replicas) : tostring(i + 1) => i + var.coordinator_replicas }
230199

231200
name = "${var.cluster_name}-worker-${each.key}"
@@ -237,19 +206,18 @@ resource "phala_app" "worker" {
237206
docker_compose = file("${path.module}/../compose/worker.yaml")
238207

239208
env = {
240-
CLUSTER_NAME = var.cluster_name
241-
PROTOCOL_BASES = local.protocol_bases_json
242-
PEERS_JSON = local.peers_json
243-
WORKER_ORDINAL = tostring(each.value)
244-
EXPECTED_REPLICAS = var.worker_replicas + var.coordinator_replicas
245-
COORDINATOR_SERF_PORTS = local.coordinator_serf_ports
246-
COORDINATOR_HTTP_PORTS = local.coordinator_http_ports
247-
SIGNALING_URL = var.external_signaling_url
248-
TURN_HOST = var.external_coordinator_host
249-
TURN_SHARED_SECRET = var.external_turn_secret
250-
MESH_SIDECAR_IMAGE = var.mesh_sidecar_image
251-
WEBDEMO_IMAGE = var.webdemo_image
252-
PATRONI_IMAGE = var.patroni_image
209+
CLUSTER_NAME = var.cluster_name
210+
PEERS_JSON = local.peers_json
211+
UPSTREAMS_JSON = local.upstreams_json
212+
WORKER_ORDINAL = tostring(each.value)
213+
EXPECTED_REPLICAS = var.worker_replicas + var.coordinator_replicas
214+
COORDINATOR_VIPS = local.coordinator_vips
215+
SIGNALING_URL = var.external_signaling_url
216+
TURN_HOST = var.external_coordinator_host
217+
TURN_SHARED_SECRET = var.external_turn_secret
218+
MESH_SIDECAR_IMAGE = var.mesh_sidecar_image
219+
WEBDEMO_IMAGE = var.webdemo_image
220+
PATRONI_IMAGE = var.patroni_image
253221
# Stage-1 WORKAROUND — see `random_bytes` block at top of file.
254222
GOSSIP_KEY = random_bytes.gossip_key.base64
255223
PATRONI_SUPERUSER_PW = random_bytes.patroni_superuser_pw.hex
@@ -269,11 +237,7 @@ resource "phala_app" "worker" {
269237
output "coordinator_app_ids" { value = { for k, c in phala_app.coordinator : k => c.app_id } }
270238
output "worker_app_ids" { value = { for k, w in phala_app.worker : k => w.app_id } }
271239
output "consul_ui" {
272-
# Any coordinator's HTTP port serves the UI. Pick coord-0 by convention.
273-
# `<port>` (no trailing 's') = gateway terminates TLS using the wildcard
274-
# cert and forwards plain HTTP to the backend. Consul HTTP binds plain
275-
# HTTP on 127.0.0.1, so this is the right convention. The `<port>s`
276-
# form is for TLS pass-through where the backend speaks TLS itself
277-
# (e.g. Envoy public mTLS on :21000).
278-
value = "https://${phala_app.coordinator["0"].app_id}-${local.coordinator_http_port_first}.${var.gateway_domain}/ui"
240+
# Coordinator-0's Consul HTTP API on the canonical 8500. The dstack
241+
# gateway maps `<app_id>-<port>s.<gateway>` to that port on the CVM.
242+
value = "https://${phala_app.coordinator["0"].app_id}-8500s.${var.gateway_domain}/ui"
279243
}

consul-postgres-ha/compose/coordinator.yaml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
# Coordinator compose template — Gap 2 single-sidecar layout.
1+
# Coordinator compose template — peer-VIP / service-VIP layout.
22
#
33
# A coordinator CVM runs exactly one container: the consolidated
44
# `sidecar` image with ROLE=coordinator. Inside it:
55
#
66
# bootstrap-secrets one-shot init — derives per-CVM secrets from
7-
# the dstack KMS, claims COORDINATOR_ORDINAL,
8-
# writes /run/instance/info.json.
9-
# mesh-conn QUIC-on-pion/ICE overlay (same as on workers).
7+
# the dstack KMS, writes /run/instance/info.json
8+
# (identity only — no per-protocol ports).
9+
# mesh-conn QUIC-on-pion/ICE overlay (binds the static
10+
# infra-port allowlist on every other peer's VIP).
1011
# consul Server agent (`-server -bootstrap-expect=N -ui`),
1112
# joins peer coordinators via mesh-conn-forwarded
12-
# loopback ports. envoy is NOT started here
13-
# (coordinators don't host a Connect-mTLS workload).
13+
# VIPs (127.50.0.<coord-vip>:8301). Envoy is NOT
14+
# started here — coordinators don't host any
15+
# Connect-mTLS workload.
1416
#
1517
# coturn + signaling that earlier coordinator templates carried have
1618
# been removed: the cluster uses an external (Vultr) coordinator box
@@ -31,17 +33,22 @@ services:
3133
image: ${MESH_SIDECAR_IMAGE}
3234
network_mode: host
3335
restart: on-failure
36+
# NET_ADMIN: required for `ip addr add 127.50.0.<n>/32 dev lo` at
37+
# entrypoint. Loopback-only, no external interface touched.
38+
cap_add:
39+
- NET_ADMIN
3440
environment:
3541
- ROLE=coordinator
3642
- CLUSTER_NAME=${CLUSTER_NAME}
37-
- PROTOCOL_BASES=${PROTOCOL_BASES}
3843
- PEERS_JSON=${PEERS_JSON}
3944
# COORDINATOR_ORDINAL is per-CVM (0..N-1); makes bootstrap-secrets
4045
# write the right /run/instance/info.json without needing Consul
4146
# KV (which itself runs on the coordinators — chicken-and-egg).
4247
- COORDINATOR_ORDINAL=${COORDINATOR_ORDINAL}
4348
- BOOTSTRAP_EXPECT=${BOOTSTRAP_EXPECT}
44-
- COORDINATOR_SERF_PORTS=${COORDINATOR_SERF_PORTS}
49+
# Comma-separated VIPs of all coordinators — entrypoint uses these
50+
# for retry-join (skipping self).
51+
- COORDINATOR_VIPS=${COORDINATOR_VIPS}
4552
# External coordinator path — coordinator's mesh-conn uses the
4653
# same Vultr coturn + signaling that workers do, so peer-pair
4754
# ICE rendezvous happens in a single shared place.

consul-postgres-ha/compose/worker.yaml

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,45 @@
1-
# Worker compose template — Gap 2 single-sidecar layout.
1+
# Worker compose template — peer-VIP / service-VIP layout.
22
#
33
# A "worker" CVM ends up running exactly three containers:
44
#
5-
# sidecar bundled platform plumbing — bootstrap-secrets,
6-
# mesh-conn, consul (client), and envoy (Connect data
7-
# plane). See consul-postgres-ha-sidecar's README.
5+
# sidecar bundled platform plumbing — bootstrap-secrets, mesh-conn,
6+
# consul (client), and TWO envoys (one per Connect sidecar:
7+
# webdemo on 21000, postgres on 21001). Provisions per-peer
8+
# VIP loopback aliases on 127.50.0.0/24 and per-service VIP
9+
# aliases on 127.10.0.0/24 at startup, so needs NET_ADMIN.
810
# patroni the workload — Postgres + Patroni leader/replica.
911
# webdemo tiny example app sitting on the mesh; swap for your
1012
# own service when adapting this template.
11-
#
12-
# Coordinator CVMs run their own compose (compose/coordinator.yaml)
13-
# without patroni/webdemo, since they only host the Consul server
14-
# quorum.
1513

1614
services:
1715
sidecar:
1816
image: ${MESH_SIDECAR_IMAGE}
1917
network_mode: host
2018
restart: on-failure
19+
# NET_ADMIN: required for `ip addr add 127.{50,10}.0.<n>/32 dev lo`
20+
# at entrypoint. Loopback-only modifications, no external interface
21+
# touched — the sidecar is the only platform-level component that
22+
# touches the kernel network at all.
23+
cap_add:
24+
- NET_ADMIN
2125
environment:
2226
- ROLE=worker
2327
- CLUSTER_NAME=${CLUSTER_NAME}
24-
- PROTOCOL_BASES=${PROTOCOL_BASES}
2528
- WORKER_ORDINAL=${WORKER_ORDINAL}
2629
- EXPECTED_REPLICAS=${EXPECTED_REPLICAS}
30+
# PEERS_JSON shape: [{id, vip}] — vip is the last octet in
31+
# 127.50.0.0/24. mesh-conn binds the static infra-port allowlist
32+
# on every other peer's VIP; entrypoint provisions the aliases.
2733
- PEERS_JSON=${PEERS_JSON}
34+
# UPSTREAMS_JSON shape: [{name, vip, port}] — service VIPs the
35+
# local Envoy will bind for its declared Connect upstreams.
36+
- UPSTREAMS_JSON=${UPSTREAMS_JSON}
37+
# COORDINATOR_VIPS: comma-separated list of coordinators' peer
38+
# VIPs. Workers retry-join 127.50.0.<vip>:8301 (serf) for each.
39+
- COORDINATOR_VIPS=${COORDINATOR_VIPS}
2840
- SIGNALING_URL=${SIGNALING_URL}
2941
- TURN_HOST=${TURN_HOST}
3042
- TURN_SHARED_SECRET=${TURN_SHARED_SECRET}
31-
- COORDINATOR_SERF_PORTS=${COORDINATOR_SERF_PORTS}
32-
# COORDINATOR_HTTP_PORTS is comma-separated; bootstrap-secrets
33-
# picks the first reachable one to talk to Consul KV — only used
34-
# if WORKER_ORDINAL is unset (legacy CAS-claim fallback).
35-
- COORDINATOR_HTTP_PORTS=${COORDINATOR_HTTP_PORTS}
3643
# Stage-1 WORKAROUND: gossip key generated in Terraform and
3744
# broadcast identically to every CVM. See cluster.tf.
3845
- GOSSIP_KEY=${GOSSIP_KEY}
@@ -61,15 +68,20 @@ services:
6168
network_mode: host
6269
restart: on-failure
6370
# CLUSTER_NAME drives Patroni's `scope` — every peer's patroni
64-
# must use the same value to land in the same cluster. The rest
65-
# is read from /run/instance/info.json by entrypoint.sh.
71+
# must use the same value to land in the same cluster. Postgres
72+
# binds the canonical 127.0.0.1:5432; Patroni REST on 127.0.0.1:8008.
6673
# Stage-1 WORKAROUND: superuser + replication passwords generated
6774
# in Terraform and broadcast identically to every worker. See
6875
# cluster.tf.
6976
environment:
7077
- CLUSTER_NAME=${CLUSTER_NAME}
7178
- PATRONI_SUPERUSER_PW=${PATRONI_SUPERUSER_PW}
7279
- PATRONI_REPLICATION_PW=${PATRONI_REPLICATION_PW}
80+
# Service VIPs for /etc/hosts — patroni needs to resolve
81+
# postgres-master to the local service VIP so its replication
82+
# source string ("primary_conninfo") routes through the local
83+
# Envoy → mesh → leader's Postgres.
84+
- UPSTREAMS_JSON=${UPSTREAMS_JSON}
7385
volumes:
7486
- /tmp/dstack-runtime/instance:/run/instance:ro
7587
- /tmp/dstack-runtime/secrets:/run/secrets:ro
@@ -82,14 +94,19 @@ services:
8294
image: ${WEBDEMO_IMAGE}
8395
network_mode: host
8496
restart: unless-stopped
97+
environment:
98+
- PEERS_JSON=${PEERS_JSON}
99+
- UPSTREAMS_JSON=${UPSTREAMS_JSON}
85100
entrypoint: ["/bin/sh", "-c"]
86101
command:
87102
- |
88103
set -e
89104
export PEER_ID=$$(jq -r '.role + "-" + (.ordinal|tostring)' /run/instance/info.json)
90-
export WEBDEMO_PORT=$$(jq -r '.ports.webdemo' /run/instance/info.json)
91-
export SIDECAR_PORT=$$(jq -r '.ports.sidecar_public' /run/instance/info.json)
92-
export CONSUL_HTTP_ADDR=127.0.0.1:$$(jq -r '.ports.http_api' /run/instance/info.json)
105+
export SELF_VIP=$$(echo "$$PEERS_JSON" | jq -r --arg id "$$PEER_ID" '.[] | select(.id == $$id) | .vip')
106+
# Append service-VIP /etc/hosts entries — each container has
107+
# its own /etc/hosts (network_mode: host doesn't share it).
108+
echo "$$UPSTREAMS_JSON" | jq -r '.[] | "127.10.0.\(.vip) \(.name)"' >> /etc/hosts
109+
export CONSUL_HTTP_ADDR=127.0.0.1:8500
93110
exec webdemo
94111
volumes:
95112
- /tmp/dstack-runtime/instance:/run/instance:ro

0 commit comments

Comments
 (0)