vibed-infra 0.3.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.
@@ -0,0 +1,88 @@
1
+ # packageconfig.yaml schema
2
+
3
+ Product-owned file. Infra reads **only** the fields below.
4
+
5
+ ## Top level
6
+
7
+ ```yaml
8
+ name: my-product # slug for cron markers / logs
9
+ version: 1
10
+
11
+ rawBase: https://raw.githubusercontent.com/org/repo/main/deploy/templates
12
+ packagerRaw: https://raw.githubusercontent.com/naiemk/vibed-infra/main
13
+
14
+ network:
15
+ edge: my-product-edge # shared Docker network for gateway + APIs
16
+
17
+ images:
18
+ backend: ghcr.io/org/my-api:main
19
+ ui: ghcr.io/org/my-ui:main
20
+ worker: ghcr.io/org/my-worker:main
21
+ nginx: ghcr.io/org/my-nginx:main # optional; default nginx:alpine + mounted conf
22
+
23
+ autoUpdate:
24
+ lockFile: /var/lock/infra-auto-update.lock # optional
25
+ ```
26
+
27
+ ## profiles
28
+
29
+ Each install directory maps to one profile key (`api`, `nodes`, `gateway`, …).
30
+
31
+ ```yaml
32
+ profiles:
33
+ api:
34
+ role: backend # backend | workers | gateway
35
+ templates:
36
+ config: app.yaml # fetched from rawBase; never overwritten if exists
37
+ envExample: .env.api.example
38
+ startScript: start-api.sh # copied from rawBase or generated
39
+ updateScript: update-api.sh
40
+ autoUpdate:
41
+ flag: API_AUTO_UPDATE
42
+ intervalEnv: API_AUTO_UPDATE_INTERVAL_MIN
43
+ offset: 0 # cron minute offset (api :00, nodes :10, gateway :20)
44
+ stopTimeoutEnv: API_STOP_TIMEOUT
45
+ extras: [] # optional scripts from rawBase (e.g. register-node.sh)
46
+
47
+ nodes:
48
+ role: workers
49
+ templates:
50
+ config: workers.yaml
51
+ envExample: .env.nodes.example
52
+ compose: docker-compose.workers.yml
53
+ workers:
54
+ services:
55
+ - name: worker-a
56
+ containerName: my-worker-a
57
+ roleEnv: WORKER_ROLE=a
58
+ activityLog: /data/logs/a.jsonl
59
+ - name: worker-b
60
+ profile: optional # compose profile name
61
+ autoUpdate: { flag: NODES_AUTO_UPDATE, offset: 10, ... }
62
+
63
+ gateway:
64
+ role: gateway
65
+ mode: standalone # standalone | bundled
66
+ templates:
67
+ envExample: .env.gateway.example
68
+ nginxInclude: gateway/extra.conf # optional product snippet
69
+ sites:
70
+ - host: app.example.com
71
+ aliases: [www.app.example.com]
72
+ backend: app-api
73
+ backendPort: 8080
74
+ ui: app-ui
75
+ uiPort: 80
76
+ healthPath: /api/health # optional; default /api/health
77
+ createPath: /api/items # optional rate-limited POST; omitted if unset
78
+ tlsCertDir: /etc/letsencrypt/live/app.example.com
79
+ autoUpdate:
80
+ flags: [UI_AUTO_UPDATE, GATEWAY_AUTO_UPDATE]
81
+ offset: 20
82
+ ```
83
+
84
+ ## Infra ignores
85
+
86
+ - Keys inside `config` YAML templates
87
+ - Secret names inside `.env` (opaque strings)
88
+ - Application health-check response bodies
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: infra-cicd
3
+ description: >-
4
+ Set up GitHub Actions CI/CD for infra-packaged products: GHCR image build,
5
+ compose smoke, and install e2e against published images. Use when adding deploy
6
+ pipelines for Backend/UI/worker images.
7
+ ---
8
+
9
+ # Infra packager CI/CD
10
+
11
+ ## Image build (GHCR)
12
+
13
+ Use reusable workflow [`github/workflows/docker-build-reusable.yml`](../../github/workflows/docker-build-reusable.yml):
14
+
15
+ ```yaml
16
+ jobs:
17
+ images:
18
+ uses: naiemk/vibed-infra/.github/workflows/docker-build-reusable.yml@main
19
+ with:
20
+ dockerfile: deploy/Dockerfile.api
21
+ image: ghcr.io/${{ github.repository_owner }}/my-api
22
+ secrets: inherit
23
+ ```
24
+
25
+ Or copy the workflow into the product repo. Tags: `:main`, `main-<sha>`, semver on tag push.
26
+
27
+ ## Install e2e (optional job)
28
+
29
+ 1. Serve repo over HTTP (Python `http.server` at repo root or multi-path).
30
+ 2. Set `PACKAGER_RAW` to the packager (npm path, git checkout, or HTTP) and `PACKAGECONFIG_URL=.../deploy/packageconfig.yaml`.
31
+ 3. Optionally set `PRODUCT_RAW` if it should differ from `packageconfig.rawBase`.
32
+ 4. Run the product `install-api.sh` into a temp dir.
33
+ 5. Fill `.env` with test secrets; `./start-*.sh`; curl health.
34
+
35
+ See [`examples/vps-hello/scripts/try-install.sh`](../../examples/vps-hello/scripts/try-install.sh) for a packager-only dry-run.
36
+
37
+ ## packageconfig in CI
38
+
39
+ - `IMAGE_TAG=main` in system tests matches GHCR `:main` from default branch push.
40
+ - Keep `rawBase` pointing at `main` branch raw URLs for operator wget; override with `PRODUCT_RAW` on a feature branch.
41
+
42
+ ## Checklist
43
+
44
+ - [ ] Dockerfiles under `deploy/`
45
+ - [ ] `deploy/packageconfig.yaml` images match GHCR names
46
+ - [ ] Install dry-run or workflow job passes
47
+ - [ ] Secrets not in templates — only `.env.example` placeholders
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: infra-packager
3
+ description: >-
4
+ Wire a new product to the infra VPS packager: packageconfig.yaml, Docker images,
5
+ deploy/templates, thin install wrappers, and CI. Use when adding Backend/UI/worker
6
+ deploy to a repo or migrating from ad-hoc deploy/install scripts.
7
+ ---
8
+
9
+ # Infra packager — onboard a product
10
+
11
+ ## When to use
12
+
13
+ - New repo needs wget VPS install (backend + UI + workers + HTTPS gateway)
14
+ - Splitting deploy scripts into reusable vibed-infra + product templates
15
+
16
+ ## Steps
17
+
18
+ 1. **Depend on** [`vibed-infra`](https://www.npmjs.com/package/vibed-infra) (`npm install vibed-infra`) or wget `install.sh` from this repo. Set `packagerRaw` in packageconfig.
19
+
20
+ 2. **Create** `deploy/packageconfig.yaml` — see [`schema/packageconfig.md`](../../schema/packageconfig.md) and the VPS example [`examples/vps-hello/packageconfig.yaml`](../../examples/vps-hello/packageconfig.yaml).
21
+
22
+ 3. **Add templates** under `deploy/templates/`:
23
+ - `.env.*.example` (secrets — opaque to infra)
24
+ - App config YAML (opaque)
25
+ - `start-*.sh` / `update-*.sh` (or use generic `start.sh`)
26
+ - Worker `docker-compose.*.yml` if multi-runner
27
+
28
+ 4. **Thin wrappers** (set profile + product URLs):
29
+
30
+ ```bash
31
+ # deploy/install/install-api.sh
32
+ export INFRA_PROFILE=api
33
+ export PACKAGECONFIG_URL=https://raw.githubusercontent.com/ORG/REPO/main/deploy/packageconfig.yaml
34
+ export PRODUCT_RAW=https://raw.githubusercontent.com/ORG/REPO/main/deploy/templates
35
+ wget -qO- https://raw.githubusercontent.com/naiemk/vibed-infra/main/install.sh | bash
36
+ ```
37
+
38
+ 5. **Build images** — Dockerfiles in `deploy/`; push to GHCR; reference in `packageconfig.images`.
39
+
40
+ 6. **CI** — use [`github/workflows/docker-build-reusable.yml`](../../github/workflows/docker-build-reusable.yml); add install e2e serving the packager + `deploy/templates/` over HTTP.
41
+
42
+ 7. **VPS** — per component directory:
43
+
44
+ ```bash
45
+ mkdir -p ~/app/api && cd ~/app/api
46
+ wget -qO- .../deploy/install/install-api.sh | bash
47
+ # edit .env, then ./start-api.sh (or ./start.sh)
48
+ ```
49
+
50
+ ## Rules
51
+
52
+ - Infra never parses app config keys — only image names, ports, volume paths, site hostnames.
53
+ - Never overwrite existing `.env` on re-install.
54
+ - Gateway container names must match `sites[].backend` / `sites[].ui` on shared `network.edge`.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: system-gateway
3
+ description: >-
4
+ Configure a system-wide HTTPS gateway on one VPS for multiple products/domains.
5
+ Use when several infra-packaged projects share one host, edge network, and Let's Encrypt.
6
+ ---
7
+
8
+ # System-wide gateway (multi-project)
9
+
10
+ ## Model
11
+
12
+ - One **gateway** install dir per host (or per edge): nginx on ports 80/443.
13
+ - Each product **backend** + **UI** containers join the same Docker network (`network.edge` from each product's packageconfig — use one shared name, e.g. `vps-edge`).
14
+ - Gateway `sites[]` in one product's packageconfig **or** a dedicated `gateway-only` packageconfig lists all vhosts.
15
+
16
+ ## Steps
17
+
18
+ 1. Pick shared network: `DOCKER_NETWORK=vps-edge` in every product `.env`.
19
+
20
+ 2. Install APIs/UIs with distinct container names:
21
+ - `product-a-api`, `product-a-ui`
22
+ - `product-b-api`, `product-b-ui`
23
+
24
+ 3. Install gateway once:
25
+
26
+ ```bash
27
+ mkdir -p ~/vps/gateway && cd ~/vps/gateway
28
+ wget -qO- .../install-gateway.sh | bash
29
+ ```
30
+
31
+ 4. Edit gateway `packageconfig` `sites[]` (or merge generated `gateway/conf.d/domains.conf`) so each `host` maps to the correct `backend` + `ui` container names.
32
+
33
+ 5. **TLS** — single SAN cert or per-host certs:
34
+
35
+ ```bash
36
+ sudo certbot certonly --standalone \
37
+ -d app-a.example.com -d app-b.example.com -d www.app-b.example.com
38
+ ```
39
+
40
+ Set `TLS_FULLCHAIN` / `TLS_PRIVKEY` in gateway `.env`. Defaults come from the first `sites[].tlsCertDir` (or `/etc/letsencrypt/live/<host>`).
41
+
42
+ 6. **Auto-update** — stagger cron: APIs :00, workers :10, gateway :20 (infra default).
43
+
44
+ ## Dual-domain on one host
45
+
46
+ Two APIs + two UIs on one edge network, one nginx `sites[]` list. See [`examples/vps-hello/packageconfig.yaml`](../../examples/vps-hello/packageconfig.yaml) for a single-site starting point.
47
+
48
+ ## Pitfalls
49
+
50
+ - API must listen on a container name resolvable by nginx (`app-api:8080`, not `localhost`).
51
+ - Do not bind host port 443 twice — only gateway publishes 443.
52
+ - Pull UI/nginx **before** stop on gateway update (infra update scripts do this).
package/start.sh ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # Generic start — delegates to profile startScript from .infra-profile.
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ cd "$SCRIPT_DIR"
6
+ if [[ -f .infra-profile ]]; then
7
+ # shellcheck source=/dev/null
8
+ source .infra-profile
9
+ fi
10
+ TARGET="${START_SCRIPT:-start.sh}"
11
+ if [[ -x "$SCRIPT_DIR/$TARGET" && "$TARGET" != "start.sh" ]]; then
12
+ exec "$SCRIPT_DIR/$TARGET" "$@"
13
+ fi
14
+ echo "No profile start script — run product-specific start script listed in .infra-profile" >&2
15
+ exit 1
@@ -0,0 +1,32 @@
1
+ # Generic backend service (optional compose path).
2
+ # Most products use profile start script (docker create + config cp) for remote Docker compatibility.
3
+ #
4
+ # Usage: docker compose --env-file .env -f docker-compose.backend.yml up -d
5
+
6
+ services:
7
+ backend:
8
+ image: ${BACKEND_IMAGE:-ghcr.io/example/backend:main}
9
+ container_name: ${DOCKER_NAME:-backend}
10
+ restart: ${RESTART:-unless-stopped}
11
+ mem_limit: ${BACKEND_MEMORY_LIMIT:-384m}
12
+ env_file: .env
13
+ environment:
14
+ CONFIG_PATH: /config/server.yaml
15
+ DB_PATH: /data/app.db
16
+ ports:
17
+ - "${HOST_PORT:-8080}:8080"
18
+ volumes:
19
+ - ${DATA_DIR:-./data}:/data
20
+ - ${CONFIG_FILE:-./app.yaml}:/config/server.yaml:ro
21
+ networks:
22
+ - edge
23
+ healthcheck:
24
+ test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/api/health || exit 1"]
25
+ interval: 10s
26
+ timeout: 5s
27
+ retries: 6
28
+
29
+ networks:
30
+ edge:
31
+ name: ${DOCKER_NETWORK:-app-edge}
32
+ external: false
@@ -0,0 +1,25 @@
1
+ # Standalone HTTPS gateway: nginx + optional UI containers on shared edge network.
2
+ # Prefer product-specific start script when nginx image bundles domains; this template uses mounted conf.
3
+
4
+ services:
5
+ gateway:
6
+ image: ${NGINX_IMAGE:-nginx:alpine}
7
+ container_name: ${GATEWAY_NAME:-app-gateway}
8
+ restart: unless-stopped
9
+ mem_limit: ${GATEWAY_MEMORY_LIMIT:-64m}
10
+ ports:
11
+ - "${HTTP_PORT:-80}:80"
12
+ - "${HTTPS_PORT:-443}:443"
13
+ volumes:
14
+ - ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
15
+ - ./gateway/conf.d:/etc/nginx/conf.d:ro
16
+ - ${TLS_FULLCHAIN:-/etc/letsencrypt/live/example.com/fullchain.pem}:/etc/nginx/certs/fullchain.pem:ro
17
+ - ${TLS_PRIVKEY:-/etc/letsencrypt/live/example.com/privkey.pem}:/etc/nginx/certs/privkey.pem:ro
18
+ - ${CERTBOT_WWW:-/var/www/certbot}:/var/www/certbot:ro
19
+ networks:
20
+ - edge
21
+
22
+ networks:
23
+ edge:
24
+ name: ${DOCKER_NETWORK:-app-edge}
25
+ external: true
@@ -0,0 +1,17 @@
1
+ # Generic worker runners — customize via packageconfig workers.services[].
2
+ # Product install copies a concrete compose (e.g. docker-compose.sweepers.yml) from templates/.
3
+
4
+ services:
5
+ worker:
6
+ image: ${WORKER_IMAGE:-ghcr.io/example/worker:main}
7
+ container_name: ${WORKER_CONTAINER_NAME:-app-worker}
8
+ restart: unless-stopped
9
+ mem_limit: ${WORKER_MEMORY_LIMIT:-192m}
10
+ env_file: .env
11
+ environment:
12
+ WORKER_CONFIG: /config/workers.yaml
13
+ volumes:
14
+ - ./logs:/data/logs
15
+ - ${CONFIG_FILE:-./workers.yaml}:/config/workers.yaml:ro
16
+ extra_hosts:
17
+ - "host.docker.internal:host-gateway"
@@ -0,0 +1,21 @@
1
+ user nginx;
2
+ worker_processes auto;
3
+ error_log /var/log/nginx/error.log warn;
4
+ pid /var/run/nginx.pid;
5
+
6
+ events {
7
+ worker_connections 1024;
8
+ }
9
+
10
+ http {
11
+ include /etc/nginx/mime.types;
12
+ default_type application/octet-stream;
13
+ sendfile on;
14
+ keepalive_timeout 65;
15
+
16
+ limit_req_zone $binary_remote_addr zone=api_create:10m rate=1r/s;
17
+ limit_req_zone $binary_remote_addr zone=api_public:10m rate=20r/s;
18
+ limit_conn_zone $binary_remote_addr zone=addr:10m;
19
+
20
+ include /etc/nginx/conf.d/*.conf;
21
+ }
package/update.sh ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # Generic update — delegates to profile updateScript from .infra-profile.
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ cd "$SCRIPT_DIR"
6
+ if [[ -f .infra-profile ]]; then
7
+ # shellcheck source=/dev/null
8
+ source .infra-profile
9
+ fi
10
+ TARGET="${UPDATE_SCRIPT:-update.sh}"
11
+ if [[ -x "$SCRIPT_DIR/$TARGET" && "$TARGET" != "update.sh" ]]; then
12
+ exec "$SCRIPT_DIR/$TARGET" "$@"
13
+ fi
14
+ echo "No profile update script — run product-specific update script listed in .infra-profile" >&2
15
+ exit 1