-
-
Notifications
You must be signed in to change notification settings - Fork 0
110 lines (94 loc) · 5.46 KB
/
Copy pathdocker.yml
File metadata and controls
110 lines (94 loc) · 5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
name: Docker Prod
# PUSH TRIGGER REMOVED 2026-07-23: prod moved to k3s. A push here previously ran don's
# self-hosted runner and `docker stack deploy`, respawning the retired revenue stack
# (zombie incident). Manual-only now; k3s deploys via GHCR image + ArgoCD.
on:
workflow_dispatch:
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: self-hosted
env:
# Per-commit tag: swarm only rolls tasks when the service spec changes,
# a re-deployed identical :latest spec is a no-op and ships nothing.
NEWAPI_IMAGE: unorouter-new-api:${{ github.sha }}
steps:
- uses: actions/checkout@v4
- name: Create env file
run: |
echo DEBUG_CRYPTO_SECRET=${{ secrets.DEBUG_CRYPTO_SECRET }} > .env
echo DEBUG_POSTGRES_DB=${{ secrets.DEBUG_POSTGRES_DB }} >> .env
echo DEBUG_POSTGRES_PASSWORD=${{ secrets.DEBUG_POSTGRES_PASSWORD }} >> .env
echo DEBUG_POSTGRES_USER=${{ secrets.DEBUG_POSTGRES_USER }} >> .env
echo DEBUG_SESSION_SECRET=${{ secrets.DEBUG_SESSION_SECRET }} >> .env
echo CRYPTO_SECRET=${{ secrets.CRYPTO_SECRET }} >> .env
echo POSTGRES_DB=${{ secrets.POSTGRES_DB }} >> .env
echo POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} >> .env
echo POSTGRES_USER=${{ secrets.POSTGRES_USER }} >> .env
echo SESSION_SECRET=${{ secrets.SESSION_SECRET }} >> .env
echo SMTP_BCC=${{ secrets.SMTP_BCC }} >> .env
echo POSTHOG_KEY=${{ secrets.POSTHOG_KEY }} >> .env
echo POSTHOG_HOST=${{ secrets.POSTHOG_HOST }} >> .env
echo VAPID_PUBLIC_KEY=${{ secrets.VAPID_PUBLIC_KEY }} >> .env
echo VAPID_PRIVATE_KEY=${{ secrets.VAPID_PRIVATE_KEY }} >> .env
echo ERROR_LOG_ENABLED=true >> .env
# Zero-downtime HA via single-node Swarm: the app runs as a swarm stack
# (master + slave) with start-first rolling behind Traefik's WRR.
# grep -qx (exact line): plain `grep active` matches the substring in
# "inactive" and would skip init on a fresh host.
- name: Ensure swarm active
run: docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx active || docker swarm init
# --attachable so the standalone Traefik + plain-compose datastores can join
# this swarm overlay too. Ignore "already exists" on redeploys.
- name: Ensure overlay network
run: docker network create --driver overlay --attachable newapi-overlay 2>/dev/null || true
# Cold-start safe: --wait blocks until postgres/redis are healthy so the
# master never stack-deploys into a not-yet-ready DB.
- name: Ensure datastores healthy (cold-start safe)
run: docker compose -f docker-compose-traefik.yml up -d --wait --wait-timeout 120 unorouter-new-api-postgres unorouter-new-api-redis
- name: Ensure support services
run: docker compose -f docker-compose-traefik.yml up -d unorouter-new-api-pgbackup unorouter-new-api-logrotate unorouter-mcp
# Local tag only; single node needs no registry push. A 2nd node later would
# require pushing to a registry (a remote node can't see a local image).
- name: Build app image
run: docker build -t "$NEWAPI_IMAGE" -t unorouter-new-api:latest .
# stack deploy does not auto-load .env, so source it first (else ${POSTGRES_*}
# interpolate empty). NEWAPI_HOST_DIR from $HOME keeps the stack host-agnostic.
- name: Deploy app stack (rolling)
run: |
set -a; . ./.env; set +a
export NEWAPI_HOST_DIR="$HOME/new-api"
docker stack deploy -c docker-stack-traefik.yml --resolve-image never newapi
# Swarm tasks are named newapi_<service>.<n>.<id>, so resolve by name filter.
# Must match on $NEWAPI_IMAGE: during a start-first roll the OLD task is
# still healthy and would pass a plain health check.
- name: Wait for master healthy
run: |
for i in $(seq 1 120); do
cid=$(docker ps -q -f name=newapi_newapi-master -f health=healthy | head -1)
if [ -n "$cid" ] \
&& [ "$(docker inspect "$cid" --format '{{.Config.Image}}')" = "$NEWAPI_IMAGE" ] \
&& docker exec "$cid" wget -q -O /dev/null http://localhost:3000/api/status; then
echo "master healthy on $NEWAPI_IMAGE"; exit 0
fi
echo "waiting for master... ($i)"; sleep 2
done
echo "master did not become healthy on $NEWAPI_IMAGE in time"; docker service ps newapi_newapi-master --no-trunc; exit 1
- name: Wait for slaves healthy
run: |
for i in $(seq 1 120); do
n=$(docker ps -q -f name=newapi_newapi-slave -f health=healthy \
| xargs -r docker inspect --format '{{.Config.Image}}' \
| grep -cx "$NEWAPI_IMAGE" || true)
if [ "$n" -ge 2 ]; then
echo "$n slaves healthy on $NEWAPI_IMAGE"; exit 0
fi
echo "waiting for slaves... ($i, healthy on new image: $n)"; sleep 2
done
echo "slaves did not become healthy on $NEWAPI_IMAGE in time"; docker service ps newapi_newapi-slave --no-trunc; exit 1
# - name: Docker run debug
# run: docker compose -f docker-compose-debug.yml up -d --build
- name: remove unused images, containers, volumes, and networks
run: docker image prune -a -f --filter "until=24h" && docker container prune -f --filter "until=24h" && docker volume prune -f && docker network prune -f --filter "until=24h"