Skip to content

Commit e1298a9

Browse files
committed
prep for real deploy
1 parent 78ee932 commit e1298a9

6 files changed

Lines changed: 262 additions & 107 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 98 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
name: Deploy
22

3+
run-name: >-
4+
${{
5+
github.event_name == 'release' && format('Deploy prod: {0}', github.event.release.tag_name) ||
6+
format('Deploy dev: {0}', github.sha)
7+
}}
8+
39
concurrency:
4-
group: deploy-${{ github.ref }}
10+
group: >-
11+
deploy-${{
12+
github.event_name == 'release' && format('prod-{0}', github.event.release.tag_name) ||
13+
format('dev-{0}', github.ref)
14+
}}
515
cancel-in-progress: true
616

717
on:
818
push:
9-
branches: [ main ]
19+
branches: [main]
20+
release:
21+
types: [published]
1022
workflow_dispatch:
23+
inputs:
24+
no_cache:
25+
description: Build without Docker cache
26+
required: false
27+
default: false
28+
type: boolean
1129

1230
env:
1331
REGISTRY: ghcr.io
@@ -18,45 +36,61 @@ permissions:
1836
packages: write
1937

2038
jobs:
21-
build:
22-
name: Build & Push Image
39+
deploy:
40+
name: Deploy
2341
runs-on: ubuntu-latest
24-
outputs:
25-
image_tag: ${{ steps.meta.outputs.version }}
42+
2643
steps:
27-
- uses: actions/checkout@v4
44+
- name: Checkout
45+
uses: actions/checkout@v4
46+
47+
- name: Set deployment vars
48+
id: vars
49+
run: |
50+
if [[ "${{ github.event_name }}" == "release" ]]; then
51+
echo "image_tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
52+
echo "host=${{ secrets.SSH_HOST_PROD }}" >> $GITHUB_OUTPUT
53+
echo "env_file=.env.enc" >> $GITHUB_OUTPUT
54+
echo "stack_name=underlay" >> $GITHUB_OUTPUT
55+
else
56+
echo "image_tag=${{ github.sha }}" >> $GITHUB_OUTPUT
57+
echo "host=${{ secrets.SSH_HOST_DEV }}" >> $GITHUB_OUTPUT
58+
echo "env_file=.env.dev.enc" >> $GITHUB_OUTPUT
59+
echo "stack_name=underlay-dev" >> $GITHUB_OUTPUT
60+
fi
2861
29-
- uses: docker/setup-buildx-action@v3
62+
- name: Set up Docker Buildx
63+
uses: docker/setup-buildx-action@v3
3064

31-
- uses: docker/login-action@v3
65+
- name: Log in to GHCR
66+
uses: docker/login-action@v3
3267
with:
3368
registry: ${{ env.REGISTRY }}
3469
username: ${{ github.actor }}
3570
password: ${{ secrets.GITHUB_TOKEN }}
3671

37-
- id: meta
72+
- name: Extract metadata
73+
id: meta
3874
uses: docker/metadata-action@v5
3975
with:
4076
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
41-
tags: |
42-
type=sha,prefix=
43-
type=raw,value=latest,enable={{is_default_branch}}
4477

45-
- uses: docker/build-push-action@v5
78+
- name: Build and push
79+
uses: docker/build-push-action@v6
4680
with:
4781
context: .
4882
target: production
4983
push: true
50-
tags: ${{ steps.meta.outputs.tags }}
51-
labels: ${{ steps.meta.outputs.labels }}
84+
provenance: false
85+
sbom: false
86+
no-cache: ${{ inputs.no_cache || false }}
5287
cache-from: type=gha
5388
cache-to: type=gha,mode=max
89+
tags: |
90+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }}
91+
${{ steps.meta.outputs.tags }}
92+
labels: ${{ steps.meta.outputs.labels }}
5493

55-
deploy:
56-
name: Deploy
57-
needs: build
58-
runs-on: ubuntu-latest
59-
steps:
6094
- name: Start SSH agent
6195
uses: webfactory/ssh-agent@v0.9.0
6296
with:
@@ -65,34 +99,34 @@ jobs:
6599
- name: Add known hosts
66100
run: |
67101
mkdir -p ~/.ssh
68-
ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts
102+
ssh-keyscan -H "${{ steps.vars.outputs.host }}" >> ~/.ssh/known_hosts
69103
70104
- name: Deploy over SSH
71105
env:
72106
SSH_USER: ${{ secrets.SSH_USER }}
73-
SSH_HOST: ${{ secrets.SSH_HOST }}
107+
SSH_HOST: ${{ steps.vars.outputs.host }}
74108
REPO: ${{ github.repository }}
75109
BRANCH: ${{ github.ref_name }}
76110
GHCR_USER: ${{ secrets.GHCR_USER }}
77111
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
78-
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
79-
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
80-
ENV_FILE: ${{ vars.ENV_FILE || '.env.enc' }}
81-
DEPLOY_PATH: ${{ vars.DEPLOY_PATH || '/opt/underlay' }}
112+
IMAGE_TAG: ${{ steps.vars.outputs.image_tag }}
113+
ENV_FILE: ${{ steps.vars.outputs.env_file }}
114+
STACK_NAME: ${{ steps.vars.outputs.stack_name }}
82115
run: |
83116
ssh "${SSH_USER}@${SSH_HOST}" \
84-
"env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE='${IMAGE}' IMAGE_TAG='${IMAGE_TAG}' ENV_FILE='${ENV_FILE}' bash -s -- '${REPO}' '${BRANCH}' '${DEPLOY_PATH}'" <<'EOS'
117+
"env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' ENV_FILE='${ENV_FILE}' STACK_NAME='${STACK_NAME}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS'
85118
set -euo pipefail
86119
87120
REPO="${1:?missing repo}"
88121
BRANCH="${2:-main}"
89-
APP_DIR="${3:-/opt/underlay}"
90122
91-
: "${IMAGE:?missing IMAGE}"
92123
: "${IMAGE_TAG:?missing IMAGE_TAG}"
93124
: "${GHCR_USER:?missing GHCR_USER}"
94125
: "${GHCR_TOKEN:?missing GHCR_TOKEN}"
126+
: "${STACK_NAME:?missing STACK_NAME}"
95127
128+
REPO_NAME="${REPO##*/}"
129+
APP_DIR="/srv/${REPO_NAME}"
96130
REPO_SSH="git@github.com:${REPO}.git"
97131
98132
ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null
@@ -105,38 +139,54 @@ jobs:
105139
fi
106140
107141
cd "${APP_DIR}"
108-
git fetch --prune origin
109-
git checkout "${BRANCH}"
110-
git pull origin "${BRANCH}"
142+
git fetch --prune --tags origin
143+
git checkout --detach "${IMAGE_TAG}"
111144
112145
: "${ENV_FILE:?missing ENV_FILE}"
146+
umask 077
113147
sops -d --input-type dotenv --output-type dotenv "$ENV_FILE" > .env
114148
115-
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin
149+
# Init swarm if not already active
150+
if ! sudo docker info --format '{{.Swarm.LocalNodeState}}' | grep -qx active; then
151+
sudo docker swarm init
152+
fi
153+
154+
echo "$GHCR_TOKEN" | sudo docker login ghcr.io -u "$GHCR_USER" --password-stdin
155+
156+
sudo docker pull "ghcr.io/${REPO}:${IMAGE_TAG}"
116157
117-
export IMAGE IMAGE_TAG
118-
docker compose pull
119-
docker compose up -d --remove-orphans
158+
# Deploy/update stack
159+
sudo env IMAGE="ghcr.io/${REPO}" IMAGE_TAG="$IMAGE_TAG" \
160+
docker stack deploy -c docker-compose.yml \
161+
--with-registry-auth --resolve-image always --prune "${STACK_NAME}"
120162
121-
wait_healthy() {
122-
local timeout="${1:-120}"
163+
sudo docker stack services "${STACK_NAME}"
164+
165+
# Wait for rollout
166+
wait_rollout() {
167+
local svc="$1" timeout="${2:-300}"
123168
local end=$((SECONDS + timeout))
124169
while (( SECONDS < end )); do
125-
if ! docker compose ps | grep -qiE 'starting|unhealthy|restarting'; then
126-
echo "All services running"
127-
docker compose ps
170+
local desired running state
171+
desired="$(sudo docker service inspect "$svc" --format '{{.Spec.Mode.Replicated.Replicas}}' 2>/dev/null || echo "")"
172+
running="$(sudo docker service ps "$svc" --filter desired-state=running --format '{{.CurrentState}}' 2>/dev/null | grep -c '^Running' || true)"
173+
state="$(sudo docker service inspect "$svc" --format '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{end}}' 2>/dev/null || echo "")"
174+
echo " $svc: desired=$desired running=$running state=$state"
175+
if [[ -n "$desired" && "$running" == "$desired" ]] && { [[ -z "$state" ]] || [[ "$state" == "completed" ]]; }; then
176+
echo " $svc rollout complete"
128177
return 0
129178
fi
130-
echo "Waiting for services..."
131179
sleep 5
132180
done
133-
echo "Rollout timeout"
134-
docker compose ps
181+
echo "Rollout timeout for $svc"
135182
return 1
136183
}
137-
wait_healthy 120
138184
139-
docker image prune -a --filter "until=72h" -f
185+
wait_rollout "${STACK_NAME}_app" 300
186+
wait_rollout "${STACK_NAME}_cron" 120
187+
188+
# Cleanup old images
189+
sudo docker image prune -a --filter "until=72h" -f
140190
141-
echo "Deployed to $(hostname)"
191+
echo "Deployed ${STACK_NAME} @ ${IMAGE_TAG} to $(hostname)"
142192
EOS

Caddyfile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Host-level Caddy — manages TLS for all domains.
2+
# Deploy to /etc/caddy/Caddyfile on the server.
3+
# Reload with: systemctl reload caddy
4+
5+
www.underlay.org {
6+
# API routes → Fastify (prod)
7+
handle /api/* {
8+
reverse_proxy localhost:3001
9+
}
10+
handle /assets/* {
11+
reverse_proxy localhost:3001
12+
}
13+
handle /uploads/* {
14+
reverse_proxy localhost:3001
15+
}
16+
17+
# Everything else → Astro SSR (prod)
18+
handle {
19+
reverse_proxy localhost:4322
20+
}
21+
}
22+
23+
dev.underlay.org {
24+
# API routes → Fastify (dev)
25+
handle /api/* {
26+
reverse_proxy localhost:3000
27+
}
28+
handle /assets/* {
29+
reverse_proxy localhost:3000
30+
}
31+
handle /uploads/* {
32+
reverse_proxy localhost:3000
33+
}
34+
35+
# Everything else → Astro SSR (dev)
36+
handle {
37+
reverse_proxy localhost:4321
38+
}
39+
}

docker-compose.dev.yml

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Development compose — source-mounted for fast reload.
22
# Start with: ./dev.sh
3+
# Access directly at localhost:4321 (Astro) and localhost:3000 (API).
34

4-
name: underlay
5+
name: underlay-dev
56

67
services:
78
postgres:
@@ -10,15 +11,29 @@ services:
1011
POSTGRES_USER: underlay
1112
POSTGRES_PASSWORD: underlay
1213
POSTGRES_DB: underlay
14+
command: >
15+
-c shared_buffers=256MB
16+
-c effective_cache_size=512MB
17+
-c work_mem=8MB
18+
-c maintenance_work_mem=64MB
19+
-c max_connections=50
1320
ports:
14-
- "5432:5432"
21+
- "5433:5432"
1522
volumes:
16-
- pgdata:/var/lib/postgresql/data
23+
- pgdata-dev:/var/lib/postgresql/data
24+
networks:
25+
- dev
26+
- dbaccess-dev
1727
healthcheck:
18-
test: [ "CMD-SHELL", "pg_isready -U underlay" ]
28+
test: ["CMD-SHELL", "pg_isready -U underlay"]
1929
interval: 5s
2030
timeout: 3s
2131
retries: 5
32+
deploy:
33+
resources:
34+
limits:
35+
memory: 1g
36+
cpus: "1.0"
2237

2338
minio:
2439
image: minio/minio:latest
@@ -30,13 +45,17 @@ services:
3045
- "9000:9000"
3146
- "9001:9001"
3247
volumes:
33-
- miniodata:/data
48+
- miniodata-dev:/data
49+
networks:
50+
- dev
3451

3552
createbucket:
3653
image: minio/mc:latest
3754
depends_on:
3855
minio:
3956
condition: service_started
57+
networks:
58+
- dev
4059
entrypoint: >
4160
/bin/sh -c "
4261
sleep 2;
@@ -66,14 +85,23 @@ services:
6685
- ./tsconfig.json:/app/tsconfig.json
6786
- ./drizzle.config.ts:/app/drizzle.config.ts
6887
- ./tools:/app/tools
69-
# Keep container's own node_modules
7088
- /app/node_modules
71-
ports:
72-
- "${APP_PORT:-4321}:4321"
73-
- "${API_PORT:-3000}:3000"
89+
networks:
90+
- dev
91+
deploy:
92+
resources:
93+
limits:
94+
memory: 512m
95+
cpus: "1.0"
7496
command: ["sh", "-c", "npm run db:migrate && npm run db:seed && npm run dev:server"]
7597
restart: unless-stopped
7698

99+
networks:
100+
dev:
101+
driver: bridge
102+
dbaccess-dev:
103+
driver: bridge
104+
77105
volumes:
78-
pgdata:
79-
miniodata:
106+
pgdata-dev:
107+
miniodata-dev:

0 commit comments

Comments
 (0)