vibed-infra 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,7 +72,7 @@ See [`skills/infra-update-agent/SKILL.md`](skills/infra-update-agent/SKILL.md).
72
72
  | `VIBED_HOME` | Machine vibed root (default `~/services/vibed-infra`) |
73
73
  | `VIBED_UPDATE_AGENT` | Override update-agent dir (default `$VIBED_HOME/update-agent`) |
74
74
  | `GATEWAY_PUBLIC_IP` | VPS IPv4 (from `gateway.publicIp`) |
75
- | `TLS_EMAIL` / `TLS_MODE` | Let’s Encrypt email; `lab` or `letsencrypt` |
75
+ | `TLS_EMAIL` / `TLS_MODE` | Let’s Encrypt email; `lab` or `letsencrypt` (docker certbot → `./certs` PEMs; optional `CERTBOT_IMAGE` / `LETSENCRYPT_HOME`) |
76
76
  | `PERSIST_LOG_DIR` | Per-service event log dir |
77
77
  | `DOCKER_AUTO_PRUNE` | Prune dangling images after update (default on) |
78
78
 
@@ -52,9 +52,11 @@ vibed_bootstrap_host_gateway() {
52
52
  fi
53
53
  }
54
54
 
55
- # Always refresh setup-tls (idempotent re-install / multi-app)
55
+ # Always refresh gateway scripts (idempotent re-install / multi-app)
56
56
  _hg_fetch "templates/host-gateway/setup-tls.sh" "$home/setup-tls.sh"
57
- chmod +x "$home/setup-tls.sh"
57
+ _hg_fetch "templates/host-gateway/start-gateway.sh" "$home/start-gateway.sh"
58
+ _hg_fetch "templates/host-gateway/reload-gateway.sh" "$home/reload-gateway.sh"
59
+ chmod +x "$home/setup-tls.sh" "$home/start-gateway.sh" "$home/reload-gateway.sh"
58
60
 
59
61
  if [[ -f "${home}/.vibed-host-gateway" ]]; then
60
62
  echo "host gateway already present: $home"
@@ -66,11 +68,9 @@ vibed_bootstrap_host_gateway() {
66
68
  _hg_fetch "templates/host-gateway/gateway/nginx.conf" "$home/gateway/nginx.conf"
67
69
  _hg_fetch "templates/host-gateway/gateway/conf.d/00-default.conf" "$home/gateway/conf.d/00-default.conf"
68
70
  _hg_fetch "templates/host-gateway/.env.example" "$home/.env.example"
69
- _hg_fetch "templates/host-gateway/start-gateway.sh" "$home/start-gateway.sh"
70
- _hg_fetch "templates/host-gateway/reload-gateway.sh" "$home/reload-gateway.sh"
71
71
  _hg_fetch "templates/host-gateway/update-gateway.sh" "$home/update-gateway.sh"
72
72
  _hg_fetch "lib/env.sh" "$home/lib-env.sh"
73
- chmod +x "$home/start-gateway.sh" "$home/reload-gateway.sh" "$home/update-gateway.sh"
73
+ chmod +x "$home/update-gateway.sh"
74
74
 
75
75
  if [[ ! -f "$home/.env" ]]; then
76
76
  cp "$home/.env.example" "$home/.env"
package/lib/package.py CHANGED
@@ -100,8 +100,13 @@ DOCKER_PRUNE_UNTIL=72h
100
100
  def _env_gateway(meta: dict, conf: dict, sites: list) -> str:
101
101
  au = conf["profiles"]["gateway"]["autoUpdate"]
102
102
  gw = conf["profiles"]["gateway"]
103
- host = sites[0]["host"] if sites else "example.com"
104
- cert_dir = sites[0].get("tlsCertDir", f"/etc/letsencrypt/live/{host}") if sites else "/etc/letsencrypt/live/example.com"
103
+ # Host gateway stores PEMs under ./certs (setup-tls.sh). tlsCertDir is legacy/docs only
104
+ # unless it already points at a relative ./certs tree.
105
+ cert_dir = "./certs"
106
+ if sites:
107
+ raw = sites[0].get("tlsCertDir") or ""
108
+ if raw.startswith("./certs") or raw.rstrip("/") == "certs":
109
+ cert_dir = raw.rstrip("/")
105
110
  flags = au.get("flags") or ["GATEWAY_AUTO_UPDATE"]
106
111
  flag_lines = "\n".join(f"{f}=0" for f in flags)
107
112
  public_ip = gw.get("publicIp") or meta.get("publicIp") or ""
package/lib/tls.sh CHANGED
@@ -9,22 +9,16 @@ infra_tls_check() {
9
9
 
10
10
  infra_tls_suggest_certbot() {
11
11
  local domains=("$@")
12
- local joined=""
13
- local d
14
- for d in "${domains[@]}"; do
15
- joined+=" -d $d"
16
- done
17
12
  cat <<EOF
18
13
 
19
- TLS certificates not found. Issue with certbot (port 80 must be free):
14
+ TLS certificates not found. From the host gateway directory, issue with:
20
15
 
21
- sudo certbot certonly --standalone${joined}
16
+ cd ~/services/gateway && ./setup-tls.sh --force
22
17
 
23
- Or after gateway HTTP is up (webroot):
18
+ (setup-tls.sh uses docker certbot when available — no host certbot/sudo required —
19
+ or host certbot when installed. PEMs land in ./certs/.)
24
20
 
25
- sudo certbot certonly --webroot -w /var/www/certbot${joined}
26
-
27
- Then set TLS_FULLCHAIN and TLS_PRIVKEY in .env and run ./start.sh
21
+ Or set TLS_MODE=lab for self-signed lab certs, then ./start.sh
28
22
 
29
23
  EOF
30
24
  }
@@ -41,19 +35,16 @@ infra_tls_offer_interactive() {
41
35
  if [[ ! -t 0 ]]; then
42
36
  return 0
43
37
  fi
44
- read -r -p "Run certbot --standalone now? [y/N] " ans
38
+ local gw="${GATEWAY_HOME:-${HOME:-}/services/gateway}"
39
+ read -r -p "Run setup-tls.sh in ${gw}? [y/N] " ans
45
40
  case "$ans" in
46
41
  y|Y|yes|YES)
47
- if ! command -v certbot >/dev/null 2>&1; then
48
- echo "certbot not found — install certbot first" >&2
42
+ if [[ -x "${gw}/setup-tls.sh" ]]; then
43
+ (cd "$gw" && ./setup-tls.sh --force)
44
+ else
45
+ echo "setup-tls.sh not found at ${gw} — bootstrap the host gateway first" >&2
49
46
  return 1
50
47
  fi
51
- local args=()
52
- local d
53
- for d in "${domains[@]}"; do
54
- args+=(-d "$d")
55
- done
56
- sudo certbot certonly --standalone "${args[@]}"
57
48
  ;;
58
49
  esac
59
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibed-infra",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Product-agnostic VPS packager: wget install, Docker Compose, TLS/nginx, auto-update",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,7 +27,7 @@ gateway:
27
27
  aliases: [www.app.example.com]
28
28
  healthPath: /api/health
29
29
  createPath: /api/items
30
- tlsCertDir: /etc/letsencrypt/live/app.example.com
30
+ tlsCertDir: ./certs # host gateway PEMs; legacy /etc/letsencrypt/... paths ignored in .env.gateway.example
31
31
  ```
32
32
 
33
33
  Defaults: containers `{name}-api` / `{name}-ui` / `{name}-worker`; host gateway container `vps-gateway`.
@@ -55,7 +55,7 @@ cd ~/services/gateway
55
55
  ./setup-tls.sh --force # re-issue after host or IP change
56
56
  ```
57
57
 
58
- - **Production:** certbot (webroot if gateway is up, else standalone). On failure, fix DNS then re-run `setup-tls.sh`.
58
+ - **Production:** `setup-tls.sh` issues Let’s Encrypt via docker `certbot/certbot` by default (config under `$GATEWAY_HOME/letsencrypt`, PEMs copied to `$GATEWAY_HOME/certs/`). No sudo required when Docker is available. Host certbot + sudo still preferred when present (same `--config-dir` under gateway home). Webroot if gateway is up, else standalone. On failure, fix DNS then re-run `setup-tls.sh`.
59
59
  - **Lab/CI:** `TLS_MODE=lab` → multi-SAN self-signed under `./certs/`.
60
60
 
61
61
  ## Webhook path (immediate docker pull)
@@ -9,6 +9,9 @@ TLS_MODE=
9
9
  TLS_EMAIL=
10
10
  GATEWAY_PUBLIC_IP=
11
11
  CERTBOT_WWW=./certbot-www
12
+ # Optional Let's Encrypt (docker certbot when host certbot/sudo unavailable)
13
+ # CERTBOT_IMAGE=certbot/certbot
14
+ # LETSENCRYPT_HOME=./letsencrypt
12
15
  HTTP_PORT=80
13
16
  HTTPS_PORT=443
14
17
  GATEWAY_MEMORY_LIMIT=64m
@@ -12,6 +12,8 @@ for arg in "$@"; do
12
12
  -h|--help)
13
13
  echo "usage: $0 [--force]"
14
14
  echo " Issues Let's Encrypt (TLS_EMAIL / TLS_MODE=letsencrypt) or lab self-signed certs."
15
+ echo " LE prefers host certbot when available (root or passwordless sudo);"
16
+ echo " otherwise uses docker run certbot/certbot (no sudo). PEMs land in ./certs/."
15
17
  echo " Re-runs when certs missing, domains/IP changed vs .vibed-tls-state, or --force."
16
18
  exit 0
17
19
  ;;
@@ -137,27 +139,104 @@ issue_letsencrypt() {
137
139
  echo "TLS_EMAIL (or gateway.tlsEmail) required for Let's Encrypt" >&2
138
140
  return 1
139
141
  fi
140
- if ! command -v certbot >/dev/null 2>&1; then
141
- echo "certbot not found — install certbot, or set TLS_MODE=lab for self-signed" >&2
142
- return 1
142
+
143
+ local LE_HOME="${LETSENCRYPT_HOME:-${SCRIPT_DIR}/letsencrypt}"
144
+ local CERTBOT_IMAGE="${CERTBOT_IMAGE:-certbot/certbot}"
145
+ mkdir -p "$LE_HOME" "$LE_HOME/work" "$LE_HOME/logs" "$CERT_DIR" "$CERTBOT_WWW"
146
+
147
+ # Absolute webroot for docker bind mounts
148
+ local CERTBOT_WWW_ABS="$CERTBOT_WWW"
149
+ [[ "$CERTBOT_WWW_ABS" != /* ]] && CERTBOT_WWW_ABS="${SCRIPT_DIR}/${CERTBOT_WWW_ABS#./}"
150
+ mkdir -p "$CERTBOT_WWW_ABS"
151
+
152
+ local use_webroot=0
153
+ if gateway_running; then
154
+ use_webroot=1
143
155
  fi
144
- local args=(certonly --non-interactive --agree-tos --email "$email" --cert-name "$primary")
156
+
157
+ local domain_args=()
145
158
  local d
146
159
  for d in "${all[@]}"; do
147
- args+=(-d "$d")
160
+ domain_args+=(-d "$d")
148
161
  done
149
- if gateway_running; then
150
- echo "Gateway running — certbot webroot ($CERTBOT_WWW)"
151
- args+=(--webroot -w "$CERTBOT_WWW")
152
- else
153
- echo "Gateway not running — certbot standalone (port 80 must be free)"
154
- args+=(--standalone)
155
- fi
162
+
163
+ local base_args=(
164
+ certonly
165
+ --agree-tos
166
+ --non-interactive
167
+ --email "$email"
168
+ --cert-name "$primary"
169
+ "${domain_args[@]}"
170
+ )
156
171
  if [[ "${CERTBOT_DRY_RUN:-0}" == "1" ]]; then
157
- args+=(--dry-run)
172
+ base_args+=(--dry-run)
173
+ fi
174
+
175
+ local ran=0
176
+ local host_can_run=0
177
+ if command -v certbot >/dev/null 2>&1; then
178
+ if [[ "$(id -u)" -eq 0 ]]; then
179
+ host_can_run=1
180
+ elif sudo -n true >/dev/null 2>&1; then
181
+ host_can_run=1
182
+ fi
183
+ fi
184
+
185
+ if [[ "$host_can_run" == "1" ]]; then
186
+ local host_args=(
187
+ "${base_args[@]}"
188
+ --config-dir "$LE_HOME"
189
+ --work-dir "$LE_HOME/work"
190
+ --logs-dir "$LE_HOME/logs"
191
+ )
192
+ if [[ "$use_webroot" == "1" ]]; then
193
+ echo "Gateway running — host certbot webroot ($CERTBOT_WWW_ABS)"
194
+ host_args+=(--webroot -w "$CERTBOT_WWW_ABS")
195
+ else
196
+ echo "Gateway not running — host certbot standalone (port 80 must be free)"
197
+ host_args+=(--standalone)
198
+ fi
199
+ local run_certbot=(certbot)
200
+ if [[ "$(id -u)" -ne 0 ]]; then
201
+ run_certbot=(sudo certbot)
202
+ fi
203
+ if "${run_certbot[@]}" "${host_args[@]}"; then
204
+ ran=1
205
+ # Best-effort: reclaim root-owned LE tree after sudo certbot
206
+ if [[ "$(id -u)" -ne 0 ]]; then
207
+ sudo -n chown -R "$(id -u):$(id -g)" "$LE_HOME" 2>/dev/null || true
208
+ fi
209
+ fi
210
+ fi
211
+
212
+ if [[ "$ran" != "1" && "$host_can_run" != "1" ]] && command -v docker >/dev/null 2>&1; then
213
+ local docker_args=(
214
+ run --rm
215
+ -v "${LE_HOME}:/etc/letsencrypt"
216
+ )
217
+ local container_args=("${base_args[@]}")
218
+ if [[ "$use_webroot" == "1" ]]; then
219
+ echo "Gateway running — docker certbot webroot ($CERTBOT_WWW_ABS)"
220
+ docker_args+=(-v "${CERTBOT_WWW_ABS}:/var/www/certbot")
221
+ container_args+=(--webroot -w /var/www/certbot)
222
+ else
223
+ echo "Gateway not running — docker certbot standalone (-p 80:80)"
224
+ docker_args+=(-p 80:80)
225
+ container_args+=(--standalone)
226
+ fi
227
+ if docker "${docker_args[@]}" "$CERTBOT_IMAGE" "${container_args[@]}"; then
228
+ ran=1
229
+ fi
158
230
  fi
159
- if ! sudo certbot "${args[@]}"; then
160
- cat <<EOF >&2
231
+
232
+ if [[ "$ran" != "1" ]]; then
233
+ if [[ "$host_can_run" != "1" ]] && ! command -v docker >/dev/null 2>&1; then
234
+ cat <<EOF >&2
235
+ Neither usable host certbot nor docker found.
236
+ Install Docker (preferred for non-root), or certbot with sudo, or set TLS_MODE=lab.
237
+ EOF
238
+ else
239
+ cat <<EOF >&2
161
240
 
162
241
  Let's Encrypt failed (often DNS not pointing here yet).
163
242
  1. Paste dist/DNS-SKILL.md into your AU DNS agent (or create the A records yourself).
@@ -165,15 +244,27 @@ Let's Encrypt failed (often DNS not pointing here yet).
165
244
  3. Re-run: cd ${SCRIPT_DIR} && ./setup-tls.sh --force
166
245
 
167
246
  EOF
247
+ fi
168
248
  return 1
169
249
  fi
170
- local live="/etc/letsencrypt/live/${primary}"
250
+
251
+ # Dry-run does not write live PEMs
252
+ if [[ "${CERTBOT_DRY_RUN:-0}" == "1" ]]; then
253
+ echo "certbot dry-run succeeded (no PEMs written)"
254
+ return 0
255
+ fi
256
+
257
+ local live="${LE_HOME}/live/${primary}"
171
258
  if [[ ! -f "${live}/fullchain.pem" || ! -f "${live}/privkey.pem" ]]; then
172
259
  echo "certbot succeeded but ${live} PEMs missing" >&2
173
260
  return 1
174
261
  fi
175
- patch_env_key TLS_FULLCHAIN "${live}/fullchain.pem"
176
- patch_env_key TLS_PRIVKEY "${live}/privkey.pem"
262
+ cp -L "${live}/fullchain.pem" "${CERT_DIR}/fullchain.pem"
263
+ cp -L "${live}/privkey.pem" "${CERT_DIR}/privkey.pem"
264
+ chmod 644 "${CERT_DIR}/fullchain.pem"
265
+ chmod 600 "${CERT_DIR}/privkey.pem"
266
+ patch_env_key TLS_FULLCHAIN "${CERT_DIR}/fullchain.pem"
267
+ patch_env_key TLS_PRIVKEY "${CERT_DIR}/privkey.pem"
177
268
  patch_env_key TLS_MODE letsencrypt
178
269
  patch_env_key TLS_EMAIL "$email"
179
270
  }
@@ -33,7 +33,7 @@ if [[ ! -f "$TLS_FULLCHAIN" || ! -f "$TLS_PRIVKEY" ]]; then
33
33
  echo "TLS certs not found:" >&2
34
34
  echo " $TLS_FULLCHAIN" >&2
35
35
  echo " $TLS_PRIVKEY" >&2
36
- echo "Run gen-dev-certs.sh into ./certs for lab, or certbot for production." >&2
36
+ echo "Run ./setup-tls.sh (lab or letsencrypt) to create ./certs PEMs." >&2
37
37
  exit 1
38
38
  fi
39
39
 
@@ -44,6 +44,10 @@ fi
44
44
 
45
45
  docker network create "$NETWORK" >/dev/null 2>&1 || true
46
46
  mkdir -p "$CERTBOT_WWW" "$APPS_DIR"
47
+ # Absolute path for live ACME webroot bind mount
48
+ CERTBOT_ABS="$CERTBOT_WWW"
49
+ [[ "$CERTBOT_ABS" != /* ]] && CERTBOT_ABS="${SCRIPT_DIR}/${CERTBOT_ABS#./}"
50
+ mkdir -p "$CERTBOT_ABS"
47
51
 
48
52
  if docker inspect "$GATEWAY_NAME" >/dev/null 2>&1; then
49
53
  echo "Removing existing container $GATEWAY_NAME ..."
@@ -58,7 +62,7 @@ STAGE="$(mktemp -d)"
58
62
  cleanup_stage() { rm -rf "$STAGE"; }
59
63
  trap cleanup_stage EXIT
60
64
 
61
- mkdir -p "$STAGE/conf.d" "$STAGE/certs" "$STAGE/certbot" "$STAGE/apps"
65
+ mkdir -p "$STAGE/conf.d" "$STAGE/certs" "$STAGE/apps"
62
66
  cp "$NGINX_CONF" "$STAGE/nginx.conf"
63
67
  cp -r "$CONF_D/." "$STAGE/conf.d/"
64
68
  cp "$TLS_FULLCHAIN" "$STAGE/certs/fullchain.pem"
@@ -75,10 +79,6 @@ if [[ -d "$APPS_DIR" ]]; then
75
79
  fi
76
80
  done
77
81
  fi
78
- if [[ -d "$CERTBOT_WWW" ]]; then
79
- cp -r "$CERTBOT_WWW/." "$STAGE/certbot/" 2>/dev/null || true
80
- fi
81
-
82
82
  tar -C "$STAGE" -cf - . | docker run --rm -i -v "${CONF_VOL}:/cfg" alpine tar -xf - -C /cfg
83
83
 
84
84
  echo "Starting $GATEWAY_NAME (HTTP ${HTTP_PORT}, HTTPS ${HTTPS_PORT}) ..."
@@ -92,6 +92,7 @@ docker run -d \
92
92
  -p "${HTTP_PORT}:80" \
93
93
  -p "${HTTPS_PORT}:443" \
94
94
  -v "${CONF_VOL}:/gateway-cfg:ro" \
95
+ -v "${CERTBOT_ABS}:/var/www/certbot" \
95
96
  "$NGINX_IMAGE" \
96
97
  sh -c 'mkdir -p /etc/nginx/certs /etc/nginx/conf.d /etc/nginx/apps /var/www/certbot && \
97
98
  cp /gateway-cfg/nginx.conf /etc/nginx/nginx.conf && \
@@ -99,7 +100,6 @@ docker run -d \
99
100
  if [ -d /gateway-cfg/apps ]; then cp -r /gateway-cfg/apps/. /etc/nginx/apps/; fi && \
100
101
  cp /gateway-cfg/certs/fullchain.pem /etc/nginx/certs/fullchain.pem && \
101
102
  cp /gateway-cfg/certs/privkey.pem /etc/nginx/certs/privkey.pem && \
102
- cp -r /gateway-cfg/certbot/. /var/www/certbot/ 2>/dev/null || true && \
103
103
  exec nginx -g "daemon off;"' >/dev/null
104
104
 
105
105
  trap - EXIT