mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
`bin/deploy` builds with plain `docker build`, which produces an image for the architecture of the machine running it. Pushing that to a tag whose current manifest is a multi-arch list replaces the list outright, and every host on the other architecture fails the next pull with `exec format error`. This is not hypothetical — it happened to `:latest` on 2026-08-18 and was reported within hours as #427. The release workflow builds amd64 and arm64 and joins them into a manifest; a homelab deploy from an x86 workstation then silently flattened that to amd64-only. The registry has been repaired by re-pointing `:latest` at the `:1.28.1` manifest list. Two changes so it cannot recur: - `bin/deploy` inspects the target tag first and exits 65 if it already resolves to a manifest list, naming the private-tag fix and the pull-only alternative for deploying a released image. - `bin/deploy.env.example` shipped `IMAGE="akitaonrails/ai-memory:latest"`, so the documented setup pointed the operator's build at the public release tag by default. It now defaults to `:homelab`. Refs #427 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
4.4 KiB
Bash
Executable File
124 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build + push + deploy ai-memory to a homelab Docker host.
|
|
#
|
|
# Reads SERVER, DEPLOY_DIR, and IMAGE from bin/deploy.env (which is
|
|
# .gitignored — see bin/deploy.env.example for the template). The
|
|
# script itself is host-agnostic; everything operator-specific lives
|
|
# in that sourced file.
|
|
#
|
|
# Initial setup (one-time, before the first deploy):
|
|
#
|
|
# 1. Copy bin/deploy.env.example to bin/deploy.env and fill in
|
|
# your homelab specifics.
|
|
# 2. Copy docker/docker-compose.prod.yml.example to
|
|
# docker/docker-compose.prod.yml; adjust ports / volumes if
|
|
# needed.
|
|
# 3. Copy docker/.env.production.example to docker/.env.production
|
|
# and fill in API keys (LLM + embeddings).
|
|
# 4. SSH into the homelab and create the deploy dir:
|
|
# ssh "$SERVER" "sudo mkdir -p $DEPLOY_DIR && \
|
|
# sudo chown $(whoami): $DEPLOY_DIR"
|
|
# 5. scp the compose + env files up there:
|
|
# scp docker/docker-compose.prod.yml "$SERVER:$DEPLOY_DIR/docker-compose.yml"
|
|
# scp docker/.env.production "$SERVER:$DEPLOY_DIR/.env.production"
|
|
# 6. Now `bin/deploy` works.
|
|
#
|
|
# See docs/deploy.md for the full setup walkthrough.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
ENV_FILE="$SCRIPT_DIR/deploy.env"
|
|
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
cat >&2 <<EOF
|
|
==> bin/deploy.env not found.
|
|
|
|
This file holds your homelab-specific values (server, deploy dir,
|
|
image tag) and is gitignored. Copy the template and fill it in:
|
|
|
|
cp bin/deploy.env.example bin/deploy.env
|
|
\$EDITOR bin/deploy.env
|
|
|
|
Then re-run bin/deploy. See docs/deploy.md for details.
|
|
EOF
|
|
exit 64
|
|
fi
|
|
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_FILE"
|
|
|
|
: "${SERVER:?SERVER not set in bin/deploy.env}"
|
|
: "${DEPLOY_DIR:?DEPLOY_DIR not set in bin/deploy.env}"
|
|
: "${IMAGE:?IMAGE not set in bin/deploy.env}"
|
|
|
|
SSH_OPTS=("-F" "/dev/null" "-o" "StrictHostKeyChecking=accept-new")
|
|
CONTAINER_NAME="${CONTAINER_NAME:-ai-memory}"
|
|
|
|
cd "$REPO_ROOT"
|
|
|
|
# Fail closed if $IMAGE already carries a multi-architecture manifest.
|
|
#
|
|
# `docker build` below produces an image for THIS machine's architecture
|
|
# only. Pushing that to a tag whose current manifest is a multi-arch list
|
|
# replaces the list, and every host on the other architecture starts
|
|
# failing with `exec format error` on the next pull.
|
|
#
|
|
# That is not hypothetical: it happened to `:latest` on 2026-08-18 (issue
|
|
# #427). `bin/release` + the release workflow publish amd64 and arm64 and
|
|
# join them into a manifest; a homelab deploy from an x86 workstation then
|
|
# silently flattened it to amd64-only. Release tags are published by CI —
|
|
# a deploy has no business overwriting one.
|
|
if docker manifest inspect "$IMAGE" >/dev/null 2>&1 &&
|
|
docker manifest inspect "$IMAGE" 2>/dev/null | grep -q '"manifests"'; then
|
|
cat >&2 <<EOF
|
|
==> REFUSING to overwrite a multi-architecture tag.
|
|
|
|
$IMAGE currently resolves to a multi-arch manifest (published by
|
|
the release workflow). This script builds for $(uname -m) only, so
|
|
pushing would drop every other architecture and break those hosts
|
|
with "exec format error".
|
|
|
|
Deploy a private tag instead — set IMAGE in bin/deploy.env to
|
|
something like:
|
|
|
|
IMAGE="akitaonrails/ai-memory:homelab"
|
|
|
|
and point docker-compose.yml on the server at that tag. Release
|
|
tags (:latest, :X.Y.Z) are published by CI only.
|
|
|
|
To deploy the released image without rebuilding, skip this script
|
|
and just pull it on the server:
|
|
|
|
ssh \$SERVER "cd \$DEPLOY_DIR && docker compose pull && docker compose up -d"
|
|
EOF
|
|
exit 65
|
|
fi
|
|
|
|
echo "==> Building Docker image: $IMAGE"
|
|
docker build -t "$IMAGE" -f docker/Dockerfile .
|
|
|
|
echo "==> Pushing image to registry..."
|
|
docker push "$IMAGE"
|
|
|
|
echo "==> Deploying to $SERVER:$DEPLOY_DIR"
|
|
# `docker compose pull` fetches the new tag; `down` stops the old
|
|
# container; `up -d` starts the new one with the new tag. We don't
|
|
# `docker compose build` on the server because the binary travels
|
|
# baked into the image.
|
|
ssh "${SSH_OPTS[@]}" "$SERVER" "cd $DEPLOY_DIR && \
|
|
docker compose pull && \
|
|
docker compose down && \
|
|
docker compose up -d"
|
|
|
|
echo "==> Waiting for container to come up..."
|
|
sleep 3
|
|
ssh "${SSH_OPTS[@]}" "$SERVER" \
|
|
"docker inspect --format='{{.State.Status}} ({{.State.Health.Status}})' $CONTAINER_NAME"
|
|
|
|
echo "==> Deploy complete."
|
|
echo
|
|
echo " Container: $CONTAINER_NAME on $SERVER"
|
|
echo " Probe MCP: curl ${SERVER#*@}:49374/mcp"
|