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 <cursoragent@cursor.com>
This commit is contained in:
parent
d5a805853a
commit
ccead8733a
|
|
@ -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]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -3,38 +3,10 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: server
|
target: server
|
||||||
container_name: tunnel-server
|
image: git.nixc.us/colin/better-argo-tunnels:production
|
||||||
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
|
|
||||||
|
|
||||||
networks:
|
tunnel-client:
|
||||||
traefik:
|
build:
|
||||||
external: true
|
context: .
|
||||||
|
target: client
|
||||||
|
image: git.nixc.us/colin/better-argo-tunnels:client-production
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue