From d7562d8a3016a95dc4a1b639bd05d429828b8347 Mon Sep 17 00:00:00 2001 From: Christian van Dijk Date: Mon, 23 Feb 2026 23:30:20 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20enhance=20Makefile=20and=20Readm?= =?UTF-8?q?e=20with=20Docker=20Swarm=20and=20Kubernetes=20support,=20addin?= =?UTF-8?q?g=20new=20targets=20for=20deployment=20and=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 52 +++++++++- Readme.md | 38 +++++-- compose.swarm.yml | 137 +++++++++++++++++++++++++ k8s/handheld-devices/values-a.yaml | 17 +++ k8s/handheld-devices/values-local.yaml | 11 ++ 5 files changed, 247 insertions(+), 8 deletions(-) create mode 100644 compose.swarm.yml create mode 100644 k8s/handheld-devices/values-a.yaml create mode 100644 k8s/handheld-devices/values-local.yaml diff --git a/Makefile b/Makefile index e4c4d38..7fe735d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,15 @@ .PHONY: dev up down build test lint install-deps logs logs-api logs-worker logs-all \ - init-db shell-postgres shell-api shell-worker status clean help + init-db shell-postgres shell-api shell-worker status clean help \ + k8s-apply-local k8s-apply-a k8s-down-local k8s-down-a \ + swarm-build swarm-deploy swarm-down + +# Docker Swarm +SWARM_STACK ?= handheld + +# Kubernetes +K8S_CHART := k8s/handheld-devices +K8S_NAMESPACE ?= handheld-devices +K8S_CONTEXT_A ?= # set to your A env context, e.g. gke_project_region_cluster # Default target help: @@ -23,6 +33,15 @@ help: @echo "" @echo " status Show container status" @echo " clean Stop and remove volumes" + @echo "" + @echo " k8s-apply-local Helm upgrade/install to local cluster (minikube, kind)" + @echo " k8s-apply-a Helm upgrade/install to A env (set K8S_CONTEXT_A)" + @echo " k8s-down-local Helm uninstall from local cluster" + @echo " k8s-down-a Helm uninstall from A env (set K8S_CONTEXT_A)" + @echo "" + @echo " swarm-build Build and tag images for Swarm (no build in stack)" + @echo " swarm-deploy Deploy stack to Swarm (requires: docker swarm init)" + @echo " swarm-down Remove stack from Swarm" install-deps: luarocks install lua-cjson && luarocks install luasocket && luarocks install pgmoon && \ @@ -73,3 +92,34 @@ status: clean: down docker-compose down -v + +k8s-apply-local: + helm upgrade --install handheld-devices $(K8S_CHART) \ + -f $(K8S_CHART)/values.yaml \ + -f $(K8S_CHART)/values-local.yaml \ + -n $(K8S_NAMESPACE) --create-namespace + +k8s-apply-a: + helm upgrade --install handheld-devices $(K8S_CHART) \ + -f $(K8S_CHART)/values.yaml \ + -f $(K8S_CHART)/values-a.yaml \ + -n $(K8S_NAMESPACE) --create-namespace \ + $(if $(K8S_CONTEXT_A),--kube-context $(K8S_CONTEXT_A)) + +k8s-down-local: + helm uninstall handheld-devices -n $(K8S_NAMESPACE) --ignore-not-found + +k8s-down-a: + helm uninstall handheld-devices -n $(K8S_NAMESPACE) --ignore-not-found \ + $(if $(K8S_CONTEXT_A),--kube-context $(K8S_CONTEXT_A)) + +swarm-build: + docker build -t handheld-devices-api:latest ./devices-api + docker build -t handheld-devices-worker:latest ./devices-worker + docker build -t handheld-devices-frontend:latest ./frontend + +swarm-deploy: swarm-build + docker stack deploy -c compose.swarm.yml $(SWARM_STACK) + +swarm-down: + docker stack rm $(SWARM_STACK) diff --git a/Readme.md b/Readme.md index a77d1d2..4b4d60d 100644 --- a/Readme.md +++ b/Readme.md @@ -79,14 +79,14 @@ make lint # deno check From project root: -| Target | Description | -| --------------------- | ----------------------------------------- | +| Target | Description | +| --------------------- | ------------------------------------------ | | `make install-deps` | Install LuaRocks + Deno deps for local dev | -| `make dev` | Start all services | -| `make down` | Stop all services | -| `make build` | Build Docker images | -| `make test` | Run API unit tests | -| `make lint` | Luacheck + Deno check | +| `make dev` | Start all services | +| `make down` | Stop all services | +| `make build` | Build Docker images | +| `make test` | Run API unit tests | +| `make lint` | Luacheck + Deno check | | `make logs` | Follow API logs | | `make logs-worker` | Follow worker logs | | `make init-db` | Run DB migrations manually | @@ -110,6 +110,30 @@ From project root: - **Kubernetes**: `helm lint k8s/handheld-devices` - **CI**: See `.github/workflows/ci.yml` (lint, build, unit tests, helm lint) +## Docker Swarm + +Use `compose.swarm.yml` to deploy to Docker Swarm. Postgres is pinned to a single node (via label) so its volume stays on one node. + +**Label the node** that will run Postgres before deploying: + +```bash +docker node ls +docker node update --label-add db=true +``` + +Then deploy: + +```bash +docker swarm init # if not already a swarm manager +make swarm-deploy +``` + +| Target | Description | +| ------------------- | ------------------------------ | +| `make swarm-build` | Build and tag images for Swarm | +| `make swarm-deploy` | Build images and deploy stack | +| `make swarm-down` | Remove stack from Swarm | + ## Environment See `.env.example` for configuration. Key variables: diff --git a/compose.swarm.yml b/compose.swarm.yml new file mode 100644 index 0000000..dedb030 --- /dev/null +++ b/compose.swarm.yml @@ -0,0 +1,137 @@ +# Docker Swarm stack - run: make swarm-deploy (build images first: make swarm-build) +# Postgres is pinned to manager node so its volume stays on one node. + +services: + api: + image: handheld-devices-api:latest + networks: + - handheld-net + ports: + - "8080:8080" + env_file: + - .env + environment: + DB_HOST: postgres + DB_PORT: "5432" + DB_NAME: handheld_devices + DB_USER: devices_user + DB_PASSWORD: devices_password + REDIS_HOST: redis + REDIS_PORT: "6379" + healthcheck: + test: ["CMD-SHELL", "wget -q -O - http://localhost:8080/health/live || exit 1"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 30s + deploy: + replicas: 2 + restart_policy: + condition: on-failure + update_config: + parallelism: 1 + delay: 10s + + postgres: + image: postgres:15-alpine + networks: + - handheld-net + ports: + - "5432:5432" + environment: + POSTGRES_DB: handheld_devices + POSTGRES_USER: devices_user + POSTGRES_PASSWORD: devices_password + volumes: + - postgres_data:/var/lib/postgresql/data + configs: + - source: postgres_init + target: /docker-entrypoint-initdb.d/001_create_devices.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U devices_user -d handheld_devices"] + interval: 5s + timeout: 3s + retries: 10 + deploy: + replicas: 1 + restart_policy: + condition: on-failure + placement: + constraints: + - node.labels.db == true + + redis: + image: redis:7-alpine + networks: + - handheld-net + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + deploy: + replicas: 1 + restart_policy: + condition: on-failure + + frontend: + image: handheld-devices-frontend:latest + networks: + - handheld-net + ports: + - "8090:8090" + env_file: + - .env + environment: + API_URL: http://localhost:8080 + healthcheck: + test: ["CMD-SHELL", "wget -q -O - http://localhost:8090/health || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + deploy: + replicas: 1 + restart_policy: + condition: on-failure + update_config: + parallelism: 1 + delay: 10s + + worker: + image: handheld-devices-worker:latest + networks: + - handheld-net + env_file: + - .env + environment: + DB_HOST: postgres + DB_PORT: "5432" + DB_NAME: handheld_devices + DB_USER: devices_user + DB_PASSWORD: devices_password + REDIS_HOST: redis + REDIS_PORT: "6379" + healthcheck: + test: ["CMD-SHELL", "pgrep -f 'worker.lua' || exit 1"] + interval: 10s + timeout: 3s + retries: 3 + deploy: + replicas: 1 + restart_policy: + condition: on-failure + +configs: + postgres_init: + file: ./devices-api/migrations/001_create_devices.sql + +volumes: + postgres_data: + +networks: + handheld-net: + driver: overlay + attachable: true diff --git a/k8s/handheld-devices/values-a.yaml b/k8s/handheld-devices/values-a.yaml new file mode 100644 index 0000000..cd9e1f1 --- /dev/null +++ b/k8s/handheld-devices/values-a.yaml @@ -0,0 +1,17 @@ +# Overrides for A environment (staging/production) +# Customize: image tags, ingress, apiUrl, replicas, etc. +frontend: + apiUrl: "https://handheld-a.example.com" # override with your A env URL +ingress: + enabled: true + hosts: + - host: handheld-a.example.com + paths: + - path: /api + pathType: Prefix + service: api + port: 8080 + - path: / + pathType: Prefix + service: frontend + port: 8090 diff --git a/k8s/handheld-devices/values-local.yaml b/k8s/handheld-devices/values-local.yaml new file mode 100644 index 0000000..32d5ac7 --- /dev/null +++ b/k8s/handheld-devices/values-local.yaml @@ -0,0 +1,11 @@ +# Overrides for local dev (minikube, kind, etc.) +api: + image: + pullPolicy: Never # use locally built images +worker: + image: + pullPolicy: Never +frontend: + image: + pullPolicy: Never + apiUrl: "http://localhost:8080"