The live directory /home/denny/stacks/desfoto belongs to denny, but the SSH deploy identity cannot traverse /home/denny (0750). Run the directory change and the compose command together under the owning identity via 'sudo -n -u denny bash -c' instead of a bare 'cd' as the SSH user. Applies to the deploy, the stack teardown and both rollback paths.
190 lines
7.8 KiB
Bash
Executable File
190 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Release deployment for desfoto.de.
|
|
#
|
|
# .ocauto/deploy <release-sha>
|
|
#
|
|
# Ships the committed static build to the production host, starts the stack
|
|
# behind the existing Traefik instance, and removes the obsolete
|
|
# "desfoto.de -> dennyschulz.de" redirect from the shared /srv/stack project.
|
|
#
|
|
# Rollback: the state before every release is snapshotted to
|
|
# /home/denny/stacks/desfoto-releases/<stamp>-<sha>/
|
|
# including the shared stack file that the release modified. Restore it with
|
|
# ROLLBACK_TO=<dir> .ocauto/deploy rollback
|
|
# The snapshot is written even for the very first release, when no site is live
|
|
# yet; rolling back to that snapshot then removes the new stack again and puts
|
|
# the previous /srv/stack configuration back.
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
root="$(pwd)"
|
|
release_sha="${1:-}"
|
|
remote="${DESFOTO_REMOTE:-prod-main}"
|
|
live_dir="/home/denny/stacks/desfoto"
|
|
releases_dir="/home/denny/stacks/desfoto-releases"
|
|
stack_dir="/srv/stack"
|
|
|
|
log() { printf '[deploy] %s\n' "$1"; }
|
|
die() { printf '[deploy] ERROR: %s\n' "$1" >&2; exit 1; }
|
|
|
|
if [ "$release_sha" = "rollback" ]; then
|
|
target="${ROLLBACK_TO:-}"
|
|
[ -n "$target" ] || die "ROLLBACK_TO must name a snapshot directory"
|
|
log "rolling back to $target"
|
|
# shellcheck disable=SC2029
|
|
ssh "$remote" "set -euo pipefail
|
|
sudo test -d '$target'
|
|
if sudo test -d '$target/site'; then
|
|
sudo rsync -a --delete --chown=denny:denny '$target/site/' '$live_dir/site/'
|
|
sudo cp '$target/nginx.conf' '$live_dir/nginx.conf'
|
|
sudo cp '$target/compose.yml' '$live_dir/compose.yml'
|
|
sudo cp '$target/compose.vps.yml' '$live_dir/compose.vps.yml'
|
|
sudo -n -u denny bash -c \"cd '$live_dir' && docker compose -f compose.yml -f compose.vps.yml up -d --remove-orphans\"
|
|
else
|
|
echo '[deploy] initial-state snapshot: removing the desfoto stack again'
|
|
if sudo test -d '$live_dir'; then
|
|
sudo -n -u denny bash -c \"cd '$live_dir' && docker compose -f compose.yml -f compose.vps.yml down --remove-orphans\"
|
|
fi
|
|
fi
|
|
if sudo test -f '$target/docker-compose.yml.stack-backup'; then
|
|
echo '[deploy] restoring the previous shared stack configuration'
|
|
sudo cp '$target/docker-compose.yml.stack-backup' '$stack_dir/docker-compose.yml'
|
|
cd '$stack_dir'
|
|
sudo docker compose up -d --no-deps landing
|
|
fi"
|
|
log "rollback started"
|
|
exit 0
|
|
fi
|
|
|
|
[ -n "$release_sha" ] || die "usage: .ocauto/deploy <release-sha|rollback>"
|
|
[ "$(git rev-parse HEAD)" = "$release_sha" ] || die "HEAD does not match $release_sha"
|
|
[ -z "$(git status --porcelain -- site nginx.conf compose.yml compose.vps.yml)" ] \
|
|
|| die "the deployable files have uncommitted changes"
|
|
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT
|
|
|
|
log "packing $release_sha"
|
|
tar -czf "$tmp/desfoto-$release_sha.tgz" -C "$root" site nginx.conf compose.yml compose.vps.yml
|
|
|
|
log "uploading"
|
|
scp -q "$tmp/desfoto-$release_sha.tgz" "$remote:/tmp/desfoto-$release_sha.tgz"
|
|
|
|
log "deploying on $remote"
|
|
# shellcheck disable=SC2029
|
|
ssh "$remote" "REMOTE_SHA='$release_sha' LIVE_DIR='$live_dir' RELEASES_DIR='$releases_dir' STACK_DIR='$stack_dir' bash -s" <<'REMOTE'
|
|
set -euo pipefail
|
|
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
snapshot="$RELEASES_DIR/$stamp-$REMOTE_SHA"
|
|
archive="/tmp/desfoto-$REMOTE_SHA.tgz"
|
|
|
|
# 1. Snapshot the current state so a rollback stays possible. This also runs for
|
|
# the very first release (no site live yet) so ROLLBACK_TO always has a target.
|
|
sudo mkdir -p "$snapshot" "$RELEASES_DIR"
|
|
if sudo test -d "$LIVE_DIR"; then
|
|
echo "[deploy] snapshot -> $snapshot"
|
|
sudo cp -a "$LIVE_DIR/." "$snapshot/"
|
|
else
|
|
echo "[deploy] no previous release; $snapshot records the initial state"
|
|
fi
|
|
sudo ln -sfn "$snapshot" "$RELEASES_DIR/previous"
|
|
|
|
# 2. Unpack the new release.
|
|
echo "[deploy] unpacking $archive"
|
|
sudo mkdir -p "$LIVE_DIR"
|
|
sudo rm -rf "$LIVE_DIR/site"
|
|
sudo tar -xzf "$archive" -C "$LIVE_DIR"
|
|
sudo chown -R denny:denny "$LIVE_DIR"
|
|
sudo rm -f "$archive"
|
|
printf '%s\n' "$REMOTE_SHA" | sudo tee "$LIVE_DIR/RELEASE" >/dev/null
|
|
|
|
# 3. Start or update the stack. The live directory belongs to denny and the SSH
|
|
# login cannot traverse /home/denny, so the directory change and the compose
|
|
# command run together in one shell under the owning identity.
|
|
sudo -n -u denny bash -c "cd '$LIVE_DIR' && docker compose -f compose.yml -f compose.vps.yml up -d --remove-orphans"
|
|
|
|
# 4. Wait for the container health check.
|
|
for _ in $(seq 1 30); do
|
|
status="$(sudo docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' desfoto-web-1 2>/dev/null || echo missing)"
|
|
[ "$status" = "healthy" ] && break
|
|
[ "$status" = "missing" ] && break
|
|
sleep 2
|
|
done
|
|
echo "[deploy] desfoto-web-1: $status"
|
|
if [ "$status" != "healthy" ]; then
|
|
echo "[deploy] ERROR: container is not healthy" >&2
|
|
sudo docker logs --tail 40 desfoto-web-1 >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
# 5. Remove the obsolete desfoto.de redirect from the shared stack project.
|
|
# The new stack already owns desfoto.de through a higher Traefik priority,
|
|
# this only deletes the now-dead labels so Traefik no longer advertises them.
|
|
# Stripped are Traefik label lines that mention desfoto (the old routers
|
|
# "desfoto-redirect"/"desfoto-redirect-http", the middleware
|
|
# "desfoto-to-website" and any related key) plus the comment that grouped
|
|
# them; every other line is copied verbatim. The result is validated as a
|
|
# Compose project *before* it replaces the live file, and the replacement
|
|
# itself is a single atomic rename.
|
|
stack_file="$STACK_DIR/docker-compose.yml"
|
|
if sudo grep -q 'desfoto' "$stack_file"; then
|
|
echo "[deploy] removing obsolete desfoto redirect labels from $stack_file"
|
|
sudo cp "$stack_file" "$STACK_DIR/docker-compose.yml.bak.$stamp"
|
|
sudo cp "$stack_file" "$snapshot/docker-compose.yml.stack-backup"
|
|
|
|
# The temp file lives next to the original (same filesystem, hidden name that
|
|
# Compose ignores) so the final mv is atomic.
|
|
stack_tmp="$(sudo mktemp "$STACK_DIR/.docker-compose.yml.desfoto.XXXXXX")"
|
|
set +e
|
|
# Strip only lines that are Traefik labels mentioning desfoto (list or map
|
|
# syntax, key or value side) plus the comment that grouped the old redirect.
|
|
# The generic "desfoto" substring filter of the previous version could have
|
|
# deleted unrelated lines; this one is anchored to label lines.
|
|
sudo awk '
|
|
/^[[:space:]]*-?[[:space:]]*"?traefik\..*desfoto/ { next }
|
|
/# desfoto\.de -> kanonische Fotografen-Website/ { next }
|
|
{ print }
|
|
' "$stack_file" | sudo tee "$stack_tmp" >/dev/null
|
|
strip_status="${PIPESTATUS[0]}"
|
|
set -e
|
|
if [ "$strip_status" -ne 0 ]; then
|
|
sudo rm -f "$stack_tmp"
|
|
echo "[deploy] ERROR: cannot read $stack_file (awk exit $strip_status)" >&2
|
|
exit 1
|
|
fi
|
|
sudo chown --reference="$stack_file" "$stack_tmp"
|
|
sudo chmod --reference="$stack_file" "$stack_tmp"
|
|
|
|
# Validate the stripped file as the same Compose project before touching the
|
|
# live configuration. The project directory stays $STACK_DIR because the first
|
|
# -f file lives there.
|
|
validate=(docker compose -f "$stack_tmp")
|
|
if sudo test -f "$STACK_DIR/docker-compose.override.yml"; then
|
|
validate+=(-f "$STACK_DIR/docker-compose.override.yml")
|
|
fi
|
|
validate+=(config --quiet)
|
|
if ! ( cd "$STACK_DIR" && sudo "${validate[@]}" ); then
|
|
sudo rm -f "$stack_tmp"
|
|
echo "[deploy] ERROR: stripped stack file is not valid; live file left untouched" >&2
|
|
exit 1
|
|
fi
|
|
|
|
sudo mv "$stack_tmp" "$stack_file"
|
|
remaining="$(sudo grep -n 'desfoto' "$stack_file" || true)"
|
|
if [ -n "$remaining" ]; then
|
|
echo "[deploy] WARNING: desfoto references remain in $stack_file:" >&2
|
|
printf '%s\n' "$remaining" >&2
|
|
fi
|
|
|
|
( cd "$STACK_DIR" && sudo docker compose up -d --no-deps landing )
|
|
echo "[deploy] landing recreated without the desfoto redirect"
|
|
else
|
|
echo "[deploy] obsolete redirect labels already absent"
|
|
fi
|
|
|
|
# 6. Record the release marker.
|
|
echo "[deploy] done: $REMOTE_SHA"
|
|
REMOTE
|
|
|
|
log "release $release_sha deployed"
|