- Ensure consistent directory and file permissions on StorageBox mounts for improved container access across application and database services. - Introduce application-specific `storagebox_uid`/`gid` variables for more granular ownership control. - Enhance StorageBox mount reliability by adding systemd reload and remount handlers for configuration changes. - Add root credentials to Patroni's etcd configuration for authenticated communication. - Update all relevant documentation and deployment scripts to use the `iklimco` Docker stack name for database services. - Re-encrypt production vault secrets to include the new etcd password.
13 KiB
08 — Deploy Pipeline Update (Prod)
Context
- File:
.gitea/workflows/deploy-prod.yml - Same changes as test pipeline (
test-env-setup/07-deploy-pipeline-update.md), adapted for prod paths and prod runner. - Prod-specific differences from test:
SPRING_PROFILES_ACTIVE=prod(nottest) in Run APISIX Init- DB hostnames:
postgresql,mongodb(Swarm overlay DNS — same as test) - Storagebox paths via env vars (
SWAG_CERT_DIR,SWAG_CONFIG_DIR, vb.) instead of local host paths - Extra steps: Update DNS Records (GoDaddy API), Wait for etcd
Step 1 — Remove manual cert scp lines from Initialize Workspace
# DELETE from "Initialize Servers" step:
scp -P 23 ${{ vars.STORAGEBOX_USER }}@${{ vars.STORAGEBOX_USER }}.your-storagebox.de:prod/app/iklim.co/ssl/STAR.iklim.co.full.crt ./STAR.iklim.co.full.crt
scp -P 23 ${{ vars.STORAGEBOX_USER }}@${{ vars.STORAGEBOX_USER }}.your-storagebox.de:prod/app/iklim.co/ssl/STAR.iklim.co_key.pem ./STAR.iklim.co_key.pem
Also remove from Prepare Init Files:
# DELETE or make conditional:
sudo cp STAR.iklim.co.full.crt STAR.iklim.co_key.pem /opt/iklimco/ssl/
Step 2 — Add Update DNS Records step
Insert after Docker Login to Harbor and before Prepare SWAG Directories.
- name: Update DNS Records
run: |
set -a; . ./.env; . ./.env.secrets.swag; set +a
FLOATING_IP="${{ vars.PROD_FLOATING_IP }}"
DOMAIN="iklim.co"
for record in api apigw rabbitmq grafana; do
CURRENT=$(curl -s \
-H "Authorization: sso-key ${GODADDY_KEY}:${GODADDY_SECRET}" \
"https://api.godaddy.com/v1/domains/${DOMAIN}/records/A/${record}" \
2>/dev/null | jq -r '.[0].data // empty' 2>/dev/null || true)
if [ "$CURRENT" = "$FLOATING_IP" ]; then
echo "✅ ${record}.${DOMAIN} → ${FLOATING_IP} (exists, skipping)"
else
curl -sf -X PUT \
-H "Authorization: sso-key ${GODADDY_KEY}:${GODADDY_SECRET}" \
-H "Content-Type: application/json" \
"https://api.godaddy.com/v1/domains/${DOMAIN}/records/A/${record}" \
-d "[{\"data\":\"${FLOATING_IP}\",\"ttl\":600}]"
echo "✅ ${record}.${DOMAIN} → ${FLOATING_IP} (added/updated)"
fi
done
working-directory: /workspace/iklim.co
GODADDY_KEYandGODADDY_SECRETare read from.env.secrets.swag.PROD_FLOATING_IPmust be defined as a Gitea project variable (terraform output prod_floating_ip).jqis required — it must have been added to theUpdate Apt Repositorystep:apt-get install -y gettext tree jq. Runs on every deploy; existing and correct records are skipped (idempotent).
Step 3 — Add Prepare SWAG Directories step
Insert before Bootstrap Vault TLS Placeholder:
- name: Prepare SWAG Directories
run: |
set -a; . ./.env; . ./.env.secrets.swag; set +a
mkdir -p "$SWAG_CONFIG_DIR/dns-conf" "$SWAG_SITE_CONFS_DIR"
envsubst < template/swag/dns-conf/godaddy.ini.tpl | docker run --rm -i \
-v "${SWAG_CONFIG_DIR}/dns-conf:/output" \
alpine sh -c "cat > /output/godaddy.ini && chmod 600 /output/godaddy.ini"
echo "✅ godaddy.ini written"
export RESTRICTED_IPS_BLOCK="$(echo "$RESTRICTED_IPS" | tr ',' '\n' | sed 's|.*| allow &;|')"
SWAG_VARS='${API_SUBDOMAIN}${APIGW_SUBDOMAIN}${GRAFANA_SUBDOMAIN}${RABBITMQ_SUBDOMAIN}${RESTRICTED_IPS_BLOCK}'
for tpl in template/swag/site-confs/*.conf.tpl; do
fname=$(basename "${tpl%.tpl}")
envsubst "$SWAG_VARS" < "$tpl" | docker run --rm -i \
-v "${SWAG_SITE_CONFS_DIR}:/output" \
alpine sh -c "cat > /output/${fname}"
echo "✅ ${fname}"
done
cat template/swag/site-confs/default.conf | docker run --rm -i \
-v "${SWAG_SITE_CONFS_DIR}:/output" \
alpine sh -c "cat > /output/default.conf"
echo "✅ SWAG directories ready"
SWAG_CTR=$(docker ps -q -f name=iklimco_swag 2>/dev/null | head -1)
if [ -n "$SWAG_CTR" ]; then
docker exec "$SWAG_CTR" nginx -t && docker exec "$SWAG_CTR" nginx -s reload
echo "✅ SWAG nginx reloaded"
fi
working-directory: /workspace/iklim.co
.envis sourced first soAPI_SUBDOMAIN=api.iklim.co(prod values) are used. Ensure these vars are inprod/secrets/iklim.co/.env.prodon storagebox.
Step 4 — Add Wait for etcd step
Insert after Deploy Swarm Stack and before Run APISIX Init.
APISIX reads its entire configuration from etcd; init script will fail silently if etcd is not ready.
- name: Wait for etcd
run: |
echo "⏳ Waiting for Patroni etcd..."
for i in $(seq 1 30); do
if docker run --rm --network iklimco-net alpine \
sh -c "wget -qO- http://etcd:2379/health 2>/dev/null | grep -q '\"health\":\"true\"'"; then
echo "✅ Patroni etcd ready"
break
fi
[ "$i" -eq 30 ] && echo "❌ Patroni etcd did not become ready in time" && exit 1
echo " attempt $i/30 — waiting 5s..."
sleep 5
done
Note: In prod, APISIX uses the 3-node Patroni etcd cluster on DB nodes (
etcd/02/03:2379) via the/apisixprefix — resolved throughiklimco-netoverlay DNS aliases defined indocker-stack-db.prod.yml. The standaloneetcdservice from the base stack is disabled (replicas: 0in the prod overlay) and removed from the service list by a post-deploy step. This step waits for Patroni etcd (etcd:2379) to be healthy before running the APISIX init script.
Step 5 — Add Run APISIX Init step
Insert after Wait for etcd and before Bootstrap SWAG Certificate.
- name: Run APISIX Init
run: |
set -a; . ./.env; . ./.env.secrets.shared; set +a
echo "⏳ Waiting for Swarm APISIX..."
until curl -sf -o /dev/null \
-H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
"http://apisix:9180/apisix/admin/upstreams" 2>/dev/null; do
sleep 5
done
export SPRING_PROFILES_ACTIVE=prod
/bin/bash init/apisix-core/init.sh
echo "✅ APISIX routes configured"
working-directory: /workspace/iklim.co
Prod-specific:
SPRING_PROFILES_ACTIVE=prod— test pipeline usestest.APISIX_ADMIN_KEYis sourced from.env.secrets.shared. The init script is idempotent (PUT semantics); safe to re-run on subsequent deploys. Withreplicas: 3in prod, all APISIX instances read the same etcd state — no per-replica init needed.
Step 6 — Add Bootstrap SWAG Certificate step
Insert after Run APISIX Init:
- name: Bootstrap SWAG Certificate
run: |
set -a; . ./.env; set +a
echo "Waiting for SWAG container to start..."
SWAG_CTR=""
for i in $(seq 1 24); do
SWAG_CTR=$(docker ps -q -f name=iklimco_swag 2>/dev/null | head -1)
[ -n "$SWAG_CTR" ] && break
sleep 10
done
if [ -z "$SWAG_CTR" ]; then
echo "❌ SWAG container did not start"
exit 1
fi
CERT_PATH="/config/etc/letsencrypt/live/iklim.co/fullchain.pem"
echo "Waiting for cert (up to 10 min)..."
for i in $(seq 1 20); do
if docker exec "$SWAG_CTR" test -f "$CERT_PATH" 2>/dev/null; then
echo "✅ Cert obtained"
break
fi
echo " attempt $i/20 — waiting 30s..."
sleep 30
done
if ! docker exec "$SWAG_CTR" test -f "$CERT_PATH" 2>/dev/null; then
echo "❌ SWAG did not obtain cert. Logs:"
docker service logs iklimco_swag --tail 50
exit 1
fi
docker exec "$SWAG_CTR" cat "$CERT_PATH" | \
docker run --rm -i -v "${SWAG_CERT_DIR}:/output" alpine \
sh -c "cat > /output/STAR.iklim.co.full.crt && chmod 644 /output/STAR.iklim.co.full.crt"
docker exec "$SWAG_CTR" cat "/config/etc/letsencrypt/live/iklim.co/privkey.pem" | \
docker run --rm -i -v "${SWAG_CERT_DIR}:/output" alpine \
sh -c "cat > /output/STAR.iklim.co_key.pem && chmod 644 /output/STAR.iklim.co_key.pem"
echo "✅ Cert bootstrapped to ${SWAG_CERT_DIR}/"
working-directory: /workspace/iklim.co
Step 7 — Add Run Database Init Scripts step
Insert after Bootstrap SWAG Certificate and before Review Environment.
- name: Run Database Init Scripts
run: |
set -a; . ./.env; . ./.env.secrets.shared; set +a
echo "⏳ Waiting for PostgreSQL..."
until docker run --rm --network iklimco-net \
-e PGPASSWORD="${DATABASE_POSTGRES_ROOT_PASSWD}" \
postgis/postgis:18-3.6 \
pg_isready -h postgresql -U "${DATABASE_POSTGRES_ROOT_USER}" -q 2>/dev/null; do
sleep 5
done
for sql_file in $(ls ./init/postgresql/*.sql 2>/dev/null | sort); do
echo "▶ $(basename "$sql_file")"
docker run --rm -i --network iklimco-net \
-e PGPASSWORD="${DATABASE_POSTGRES_ROOT_PASSWD}" \
postgis/postgis:18-3.6 \
psql -h postgresql -U "${DATABASE_POSTGRES_ROOT_USER}" < "$sql_file"
done
echo "⏳ Waiting for MongoDB..."
until docker run --rm --network iklimco-net mongo:8.3.2 \
mongosh "mongodb://${DATABASE_MONGODB_ROOT_USER}:${DATABASE_MONGODB_ROOT_PASSWD}@mongodb/admin" \
--eval "db.runCommand({ping:1})" --quiet 2>/dev/null; do
sleep 5
done
for js_file in $(ls ./init/mongodb/*.js 2>/dev/null | sort); do
echo "▶ $(basename "$js_file")"
docker run --rm -i --network iklimco-net mongo:8.3.2 \
mongosh "mongodb://${DATABASE_MONGODB_ROOT_USER}:${DATABASE_MONGODB_ROOT_PASSWD}@mongodb/admin" \
--quiet < "$js_file"
done
echo "✅ Database init scripts completed"
working-directory: /workspace/iklim.co
Prod-specific: DB hostnames are
postgresqlandmongodb(Swarm VIP service names). Test pipeline usespostgresql/mongodb(unqualified aliases within the same stack). SQL and JS files are generated byPrepare Init Filesstep viainit_postgresql/init_mongodbfunctions incommon-functions-prod.sh. Step is idempotent — scripts useCREATE IF NOT EXISTS/createCollectionsemantics.
Step 8 — Microservice prod deploy overlay
Each microservice has its own docker-stack-service.prod.yml overlay file. This file contains prod-specific replicas: 3 and max_replicas_per_node: 1 settings.
In microservice deploy pipelines (deploy-prod.yml), the docker stack deploy command should be:
docker stack deploy \
-c BE-<ServiceName>/docker-stack-service.yml \
-c BE-<ServiceName>/docker-stack-service.prod.yml \
iklimco
For example, for BE-Authentication:
docker stack deploy \
-c BE-Authentication/docker-stack-service.yml \
-c BE-Authentication/docker-stack-service.prod.yml \
iklimco
When a new microservice is added,
BE-<ServiceName>/docker-stack-service.prod.ymlmust be created and the pipeline must include this overlay.
Step 9 — Ensure subdomain env vars are in prod .env
Add to prod/secrets/iklim.co/.env.prod on storagebox:
API_SUBDOMAIN=api.iklim.co
APIGW_SUBDOMAIN=apigw.iklim.co
RABBITMQ_SUBDOMAIN=rabbitmq.iklim.co
GRAFANA_SUBDOMAIN=grafana.iklim.co
Step 10 — Final step order for prod pipeline
To prevent concurrent deploys, a Gitea Actions concurrency block is added per pipeline:
concurrency:
group: prod-deploy
cancel-in-progress: false
With cancel-in-progress: false, a new run waits in the queue until the previous one finishes; Gitea UI shows it as "queued" and does not return an error.
- Checkout Branch
- Prepare Folders
- Set up SSH Key and Add to known_hosts
- Update Apt Repository and Install Required Tools (
gettext tree jq) - Fetch Service Secret Files
- Initialize Workspace ← cert scp lines removed
- Upload Updated Secrets to Storagebox
- Provision Vault AppRole IDs and Docker Secrets
- Upload Updated Env to Storagebox
- Prepare Init Files ← cert copy lines removed
- Initialize Docker Swarm
- Docker Login to Harbor
- Update DNS Records ← NEW (GoDaddy API, idempotent)
- Prepare SWAG Directories ← NEW (
$SWAG_CONFIG_DIR/dns-conf; renders nginx conf templates) - Bootstrap Vault TLS Placeholder
- Deploy Swarm Stack
- Wait for etcd ← NEW (Patroni etcd
etcd:2379overlay DNS) - Run APISIX Init ← NEW (
SPRING_PROFILES_ACTIVE=prod) - Bootstrap SWAG Certificate ← NEW
- Run Database Init Scripts ← NEW (
postgresql,mongodb) - Review Environment