From ccead8733aeefc528a8a9d6f3a7c9bc9c6bd7e21 Mon Sep 17 00:00:00 2001 From: Leopere Date: Sun, 8 Feb 2026 18:18:54 -0500 Subject: [PATCH] Add Woodpecker CI, production stack, and compose files - .woodpecker.yml: test, build+push x86 images, smoke test, deploy to Swarm - docker-compose.production.yml: CI build targets for server + client images - stack.production.yml: Swarm stack with secrets, Traefik TCP labels, port range - docker-compose.yml: simplified to minimal build+image (matches smsbridge pattern) Co-authored-by: Cursor --- .woodpecker.yml | 154 ++++++++++++++++++++++++++++++++++ docker-compose.production.yml | 12 +++ docker-compose.yml | 40 ++------- stack.production.yml | 58 +++++++++++++ 4 files changed, 230 insertions(+), 34 deletions(-) create mode 100644 .woodpecker.yml create mode 100644 docker-compose.production.yml create mode 100644 stack.production.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..262a648 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,154 @@ +# Woodpecker CI Configuration for better-argo-tunnels +# +# SYNTAX NOTES: +# - Environment variables from secrets MUST use $${VAR} syntax (double dollar) +# - Single $ will be interpreted literally and won't expand variables + +labels: + location: manager + +clone: + git: + image: woodpeckerci/plugin-git + settings: + partial: false + depth: 1 + +steps: + # Build and test Go binaries + test: + name: test + image: golang:1.24-alpine + commands: + - go version | cat + - go vet ./... + - go build ./cmd/server/ + - go build ./cmd/client/ + - echo "Build and vet passed" + when: + branch: main + event: [push, pull_request] + + # Build and Push Docker images for production (x86) + build-push-production: + name: build-push-production + image: woodpeckerci/plugin-docker-buildx + depends_on: ["test"] + environment: + REGISTRY_USER: + from_secret: REGISTRY_USER + REGISTRY_PASSWORD: + from_secret: REGISTRY_PASSWORD + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - echo "nameserver 1.1.1.1" > /etc/resolv.conf + - echo "nameserver 1.0.0.1" >> /etc/resolv.conf + - HOSTNAME=$(docker info --format "{{.Name}}") + - echo "Building on $HOSTNAME" + - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us + - apk add --no-cache git || true + - export GIT_COMMIT=$${CI_COMMIT_SHA} + - export GIT_COMMIT_DATE=$(git log -1 --format=%ci HEAD 2>/dev/null || echo "unknown") + - echo "Building GIT_COMMIT=$GIT_COMMIT" + # Build server image + - docker build --target server -t git.nixc.us/colin/better-argo-tunnels:production . + - docker push git.nixc.us/colin/better-argo-tunnels:production + # Build client image + - docker build --target client -t git.nixc.us/colin/better-argo-tunnels:client-production . + - docker push git.nixc.us/colin/better-argo-tunnels:client-production + when: + branch: main + event: [push, cron] + + # Smoke test - verify server binary starts + smoke-production: + name: smoke-production + image: woodpeckerci/plugin-docker-buildx + depends_on: ["build-push-production"] + environment: + REGISTRY_USER: + from_secret: REGISTRY_USER + REGISTRY_PASSWORD: + from_secret: REGISTRY_PASSWORD + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - echo "nameserver 1.1.1.1" > /etc/resolv.conf + - echo "nameserver 1.0.0.1" >> /etc/resolv.conf + - echo "$${REGISTRY_PASSWORD}" | docker login git.nixc.us -u "$${REGISTRY_USER}" --password-stdin + - docker pull git.nixc.us/colin/better-argo-tunnels:production + - docker rm -f tunnel-smoke || true + # Smoke: just verify the binary runs and prints startup log + - mkdir -p /tmp/smoke-keys + - ssh-keygen -t ed25519 -f /tmp/smoke-keys/host_key -N "" -q + - ssh-keygen -t ed25519 -f /tmp/smoke-keys/client_key -N "" -q + - cat /tmp/smoke-keys/client_key.pub > /tmp/smoke-keys/authorized_keys + - | + docker run -d --name tunnel-smoke \ + -e SSH_PORT=2222 \ + -e SSH_HOST_KEY=/keys/host_key \ + -e AUTHORIZED_KEYS=/keys/authorized_keys \ + -e TRAEFIK_SSH_HOST=127.0.0.1 \ + -e TRAEFIK_SSH_KEY=/keys/host_key \ + -e TRAEFIK_CONFIG_DIR=/tmp/dynamic \ + -v /tmp/smoke-keys:/keys:ro \ + git.nixc.us/colin/better-argo-tunnels:production + - sleep 3 + - docker logs tunnel-smoke 2>&1 | head -20 + - docker rm -f tunnel-smoke || true + - rm -rf /tmp/smoke-keys + - echo "Smoke test passed" + when: + branch: main + event: [push, cron] + + # Deploy to Swarm + deploy-production: + name: deploy-production + image: woodpeckerci/plugin-docker-buildx + depends_on: ["test", "build-push-production", "smoke-production"] + environment: + REGISTRY_USER: + from_secret: REGISTRY_USER + REGISTRY_PASSWORD: + from_secret: REGISTRY_PASSWORD + TUNNEL_SSH_HOST_KEY: + from_secret: tunnel_ssh_host_key + TUNNEL_AUTHORIZED_KEYS: + from_secret: tunnel_authorized_keys + TUNNEL_TRAEFIK_DEPLOY_KEY: + from_secret: tunnel_traefik_deploy_key + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - echo "nameserver 1.1.1.1" > /etc/resolv.conf + - echo "nameserver 1.0.0.1" >> /etc/resolv.conf + - HOSTNAME=$(docker info --format "{{.Name}}") + - echo "Deploying on $HOSTNAME" + - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us + # Remove old stack + - echo "Removing old stack..." + - docker stack rm $${CI_REPO_NAME} || true + - sleep 10 + # Refresh secrets + - | + echo "Refreshing Docker secrets"; \ + if [ -z "$${TUNNEL_SSH_HOST_KEY}" ] || [ -z "$${TUNNEL_AUTHORIZED_KEYS}" ] || [ -z "$${TUNNEL_TRAEFIK_DEPLOY_KEY}" ]; then \ + echo "ERROR: Required secrets are empty. Aborting."; exit 1; \ + fi; \ + docker secret rm tunnel_ssh_host_key 2>/dev/null || true; \ + docker secret rm tunnel_authorized_keys 2>/dev/null || true; \ + docker secret rm tunnel_traefik_deploy_key 2>/dev/null || true; \ + sleep 3; \ + echo "$${TUNNEL_SSH_HOST_KEY}" | docker secret create tunnel_ssh_host_key -; \ + echo "$${TUNNEL_AUTHORIZED_KEYS}" | docker secret create tunnel_authorized_keys -; \ + echo "$${TUNNEL_TRAEFIK_DEPLOY_KEY}" | docker secret create tunnel_traefik_deploy_key -; \ + echo "Secrets created:"; \ + docker secret ls | grep tunnel_ + # Deploy stack + - echo "Deploying stack..." + - docker stack deploy --with-registry-auth -c ./stack.production.yml $${CI_REPO_NAME} + when: + branch: main + event: [push, cron] diff --git a/docker-compose.production.yml b/docker-compose.production.yml new file mode 100644 index 0000000..97dab45 --- /dev/null +++ b/docker-compose.production.yml @@ -0,0 +1,12 @@ +services: + tunnel-server: + build: + context: . + target: server + image: git.nixc.us/colin/better-argo-tunnels:production + + tunnel-client: + build: + context: . + target: client + image: git.nixc.us/colin/better-argo-tunnels:client-production diff --git a/docker-compose.yml b/docker-compose.yml index cf8bfa8..97dab45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,38 +3,10 @@ services: build: context: . target: server - container_name: tunnel-server - restart: unless-stopped - environment: - # SSH server config (for accepting tunnel clients) - SSH_PORT: "2222" - PORT_RANGE_START: "10000" - PORT_RANGE_END: "10100" - SSH_HOST_KEY: "/keys/host_key" - AUTHORIZED_KEYS: "/keys/authorized_keys" - # Remote Traefik host config (SSH into ingress to manage routes) - TRAEFIK_SSH_HOST: "ingress.nixc.us" - TRAEFIK_SSH_USER: "root" - TRAEFIK_SSH_KEY: "/keys/traefik_deploy_key" - TRAEFIK_CONFIG_DIR: "/root/traefik/dynamic" - TRAEFIK_ENTRYPOINT: "websecure" - TRAEFIK_CERT_RESOLVER: "letsencryptresolver" - volumes: - - ./keys:/keys:ro - ports: - - "2222:2222" - - "10000-10100:10000-10100" - labels: - # Traefik labels for the SSH endpoint itself. - # This lets Traefik TCP-route SSH traffic to the tunnel server. - traefik.enable: "true" - traefik.tcp.routers.tunnel-ssh-router.rule: "HostSNI(`*`)" - traefik.tcp.routers.tunnel-ssh-router.entrypoints: "ssh" - traefik.tcp.services.tunnel-ssh-service.loadbalancer.server.port: "2222" - traefik.docker.network: "traefik" - networks: - - traefik + image: git.nixc.us/colin/better-argo-tunnels:production -networks: - traefik: - external: true + tunnel-client: + build: + context: . + target: client + image: git.nixc.us/colin/better-argo-tunnels:client-production diff --git a/stack.production.yml b/stack.production.yml new file mode 100644 index 0000000..7ff32b1 --- /dev/null +++ b/stack.production.yml @@ -0,0 +1,58 @@ +networks: + traefik: + external: true + +secrets: + tunnel_ssh_host_key: + external: true + tunnel_authorized_keys: + external: true + tunnel_traefik_deploy_key: + external: true + +services: + tunnel-server: + image: git.nixc.us/colin/better-argo-tunnels:production + networks: + - traefik + secrets: + - source: tunnel_ssh_host_key + target: host_key + mode: 0400 + - source: tunnel_authorized_keys + target: authorized_keys + mode: 0440 + - source: tunnel_traefik_deploy_key + target: traefik_deploy_key + mode: 0400 + environment: + SSH_PORT: "2222" + PORT_RANGE_START: "10000" + PORT_RANGE_END: "10100" + SSH_HOST_KEY: "/run/secrets/host_key" + AUTHORIZED_KEYS: "/run/secrets/authorized_keys" + TRAEFIK_SSH_HOST: "ingress.nixc.us" + TRAEFIK_SSH_USER: "root" + TRAEFIK_SSH_KEY: "/run/secrets/traefik_deploy_key" + TRAEFIK_CONFIG_DIR: "/root/traefik/dynamic" + TRAEFIK_ENTRYPOINT: "websecure" + TRAEFIK_CERT_RESOLVER: "letsencryptresolver" + HOSTNAME: "{{.Node.Hostname}}" + NODE_ID: "{{.Node.ID}}" + SERVICE_NAME: "{{.Service.Name}}" + TASK_ID: "{{.Task.ID}}" + ENVIRONMENT: "production" + ports: + - "2222:2222" + - "10000-10100:10000-10100" + deploy: + replicas: 1 + placement: + constraints: + - node.hostname == macmini1 + labels: + traefik.enable: "true" + traefik.tcp.routers.tunnel-ssh-router.rule: "HostSNI(`*`)" + traefik.tcp.routers.tunnel-ssh-router.entrypoints: "ssh" + traefik.tcp.services.tunnel-ssh-service.loadbalancer.server.port: "2222" + traefik.docker.network: "traefik"