fix(deploy): ship server/frontend via registry push, not save|load
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 26s
ci / e2e (push) Skipped
ci / publish (push) Skipped

The gateway compose pulls server/frontend from the web-01 registry
(100.94.185.103:5000, tags main-<sha> + :latest), so `docker save | docker
load` + a local retag does NOT stick — the next `docker-compose up` pulls
`:latest` and silently reverts to the last-pushed image (a green edge on the
old image hid this). Rewrite the server/frontend path to: build on the build
host → tag :latest + :main-<sha> → push to the registry → `compose pull +
up --force-recreate` in /opt/clawmates (the real project dir, not the stale
/root/clawmates) → verify the RUNNING image id equals the pushed one (fail
loudly on mismatch instead of trusting HTTP 200). Agent :dev images stay on
the save|load path (not in any registry).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-28 10:01:05 +02:00
co-authored by Claude Opus 4.8
parent bf4ef4c4bf
commit bf32da949f
+57 -20
View File
@@ -1,21 +1,35 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Deploy ClawMates: build the server, frontend, node daemon, AND the agent runtime # Deploy ClawMates.
# images on the build host (tank), then load + recreate on the gateway. The agent #
# images (agent-base / agent-browser / agent-terminal) are locally-built dev images # The server + frontend run on the gateway from images pulled from the web-01
# — not in any registry — so a `docker pull` can't fetch them. This script loads # registry (100.94.185.103:5000) — /opt/clawmates/docker-compose.yml references
# them onto the gateway AND every fleet node, so agent/terminal containers never # `$REGISTRY/clawmates/{server,frontend}:latest`, and `docker-compose up` PULLS
# 404 with "No such image" (the failure mode this script exists to prevent). # them. So the ONLY reliable way to ship server/frontend is: build on the build
# host → push to the registry → pull + recreate on the gateway. A `docker save |
# docker load` + local retag does NOT work here — the next `up` pulls `:latest`
# from the registry and silently reverts to whatever was last pushed. (That is
# exactly the footgun this rewrite removes; the old save|load path is gone.)
#
# The agent runtime images (agent-base / agent-browser / agent-terminal) are
# locally-built dev images NOT in any registry, so those still ship via
# `docker save | docker load` to the gateway AND every fleet node (a `docker
# pull` can't fetch them; the load prevents "No such image" 404s).
# #
# Usage: scripts/deploy.sh # full deploy # Usage: scripts/deploy.sh # full deploy
# IMAGES_ONLY=1 scripts/deploy.sh # just (re)build + load the agent images # IMAGES_ONLY=1 scripts/deploy.sh # just (re)build + load the agent images
# #
# Override hosts via env: BUILD_HOST, GW, NODES, TAG. # Override via env: BUILD_HOST, GW, GW_DIR, NODES, TAG, REGISTRY.
set -euo pipefail set -euo pipefail
BUILD_HOST=${BUILD_HOST:-osobh@tank} # builds the images + the linux daemon BUILD_HOST=${BUILD_HOST:-osobh@tank} # builds + pushes the server/frontend images
GW=${GW:-gw-04} # the gateway (server + frontend + postgres) GW=${GW:-gw-04} # the gateway (server + frontend + postgres)
GW_DIR=${GW_DIR:-/opt/clawmates} # the compose project dir ON the gateway
REGISTRY=${REGISTRY:-100.94.185.103:5000} # web-01 registry the compose pulls from
NODES=${NODES:-"morpheus osobh@tank architect"} # fleet nodes that provision agent containers NODES=${NODES:-"morpheus osobh@tank architect"} # fleet nodes that provision agent containers
TAG=${TAG:-latest} TAG=${TAG:-latest}
# Immutable per-deploy tag so a deploy is traceable and rollback is a repoint,
# not a rebuild. Falls back to a timestamp-free literal when not in a git tree.
SHA=$(git rev-parse --short HEAD 2>/dev/null || echo manual)
AGENT_IMAGES=(agent-base agent-browser agent-terminal) AGENT_IMAGES=(agent-base agent-browser agent-terminal)
load() { ssh "$BUILD_HOST" "docker save $1" | ssh "$2" "docker load"; } load() { ssh "$BUILD_HOST" "docker save $1" | ssh "$2" "docker load"; }
@@ -39,13 +53,20 @@ rsync -az --delete --exclude target/ --exclude node_modules/ --exclude .git/ \
./ "$BUILD_HOST":~/clawmates/ ./ "$BUILD_HOST":~/clawmates/
if [ -z "${IMAGES_ONLY:-}" ]; then if [ -z "${IMAGES_ONLY:-}" ]; then
echo "→ build server + frontend + daemon on $BUILD_HOST" echo "→ build + push server + frontend on $BUILD_HOST (registry $REGISTRY, sha $SHA)"
# Build, tag both :latest and the immutable :main-<sha>, and push to the
# registry the gateway pulls from. Pushing (not save|load) is what makes the
# deploy stick — see the header note.
ssh "$BUILD_HOST" 'set -e; cd ~/clawmates ssh "$BUILD_HOST" 'set -e; cd ~/clawmates
export PATH=$HOME/.cargo/bin:$PATH CARGO_NET_GIT_FETCH_WITH_CLI=true SQLX_OFFLINE=true export PATH=$HOME/.cargo/bin:$PATH CARGO_NET_GIT_FETCH_WITH_CLI=true SQLX_OFFLINE=true
cargo build --release -p clawmates-node cargo build --release -p clawmates-node
cp target/release/clawmates-node frontend/public/dl/clawmates-node-linux-amd64 cp target/release/clawmates-node frontend/public/dl/clawmates-node-linux-amd64
docker build -f images/server.Dockerfile -t clawmates/server:'"$TAG"' . for svc in server frontend; do
docker build -f images/frontend.Dockerfile -t clawmates/frontend:'"$TAG"' .' docker build -f images/$svc.Dockerfile -t '"$REGISTRY"'/clawmates/$svc:'"$TAG"' \
-t '"$REGISTRY"'/clawmates/$svc:main-'"$SHA"' .
docker push '"$REGISTRY"'/clawmates/$svc:main-'"$SHA"'
docker push '"$REGISTRY"'/clawmates/$svc:'"$TAG"'
done'
fi fi
echo "→ build agent runtime images on $BUILD_HOST" echo "→ build agent runtime images on $BUILD_HOST"
@@ -55,11 +76,17 @@ ssh "$BUILD_HOST" 'set -e; cd ~/clawmates
done' done'
if [ -z "${IMAGES_ONLY:-}" ]; then if [ -z "${IMAGES_ONLY:-}" ]; then
echo "→ load + recreate server + frontend on $GW" echo "→ pull + recreate server + frontend on $GW ($GW_DIR)"
ssh "$GW" "docker tag clawmates/server:$TAG clawmates/server:rollback 2>/dev/null || true # Snapshot the currently-deployed images as :rollback (a repoint, cheap) so a
docker tag clawmates/frontend:$TAG clawmates/frontend:rollback 2>/dev/null || true" # bad deploy can be reverted without a rebuild, then pull the freshly-pushed
load "clawmates/server:$TAG" "$GW" # images and recreate.
load "clawmates/frontend:$TAG" "$GW" ssh "$GW" "set -e
for svc in server frontend; do
docker tag $REGISTRY/clawmates/\$svc:$TAG $REGISTRY/clawmates/\$svc:rollback 2>/dev/null || true
done
cd $GW_DIR
docker-compose -p clawmates pull server frontend
docker-compose -p clawmates up -d --force-recreate server frontend"
fi fi
echo "→ load agent runtime images onto $GW + every fleet node" echo "→ load agent runtime images onto $GW + every fleet node"
@@ -71,9 +98,19 @@ for img in "${AGENT_IMAGES[@]}"; do
done done
if [ -z "${IMAGES_ONLY:-}" ]; then if [ -z "${IMAGES_ONLY:-}" ]; then
echo "→ recreate server + frontend" echo "→ verify"
ssh "$GW" "cd /root/clawmates && docker-compose -p clawmates up -d --force-recreate server frontend" # Verify the RUNNING image matches what we just pushed — not just that the
sleep 6 # edge is up. A green edge on the OLD image is the silent-revert failure mode
curl -s -o /dev/null -w "edge HTTP %{http_code}\n" -m 10 https://clawmates.work/ || true # this check exists to catch.
want=$(ssh "$GW" "docker image inspect -f '{{.Id}}' $REGISTRY/clawmates/server:main-$SHA 2>/dev/null" || true)
got=$(ssh "$GW" "docker inspect -f '{{.Image}}' clawmates_server_1 2>/dev/null" || true)
if [ -n "$want" ] && [ "$want" = "$got" ]; then
echo " server running expected image ($SHA): ${got:7:12}"
else
echo " ✗ server image MISMATCH — running ${got:7:12}, expected main-$SHA (${want:7:12})"
echo " the deploy did NOT take effect; check the registry pull on $GW"
exit 1
fi
ssh "$GW" 'curl -s -o /dev/null -w " edge HTTP %{http_code}\n" -m 10 https://clawmates.work/ || true'
fi fi
echo "✓ deploy complete" echo "✓ deploy complete"