diff --git a/deployment/caddy/Caddyfile b/deployment/caddy/Caddyfile index cf1b3c7..d9b1285 100644 --- a/deployment/caddy/Caddyfile +++ b/deployment/caddy/Caddyfile @@ -1,3 +1,18 @@ +{ + servers { + listener_wrappers { + proxy_protocol + tls + } + + # Only trust PROXY protocol from local forwarder. + trusted_proxies static 127.0.0.1/32 ::1/128 + } + + http_port 8080 + https_port 8443 +} + fromchat.ru { reverse_proxy frontend:8301 { header_up X-Real-IP {remote_host} @@ -102,12 +117,4 @@ git.fromchat.ru { events 500 } } -} - -api.getgadgets.toolbox-io.ru { - reverse_proxy 95.165.0.162:8400 -} - -getgadgets.toolbox-io.ru { - reverse_proxy 95.165.0.162:8401 } \ No newline at end of file diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 4bf9f64..865a62a 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -167,8 +167,8 @@ services: - production restart: unless-stopped ports: - - "80:80" - - "443:443" + - "127.0.0.1:8080:8080" + - "127.0.0.1:8443:8443" extra_hosts: - "host.docker.internal:host-gateway" volumes: @@ -179,6 +179,22 @@ services: networks: - public + haproxy: + image: haproxy:latest + profiles: + - production + restart: unless-stopped + network_mode: host + # Image defaults to USER haproxy (non-root); that user cannot bind 80/443 on host. + # Rootful Docker does not change that — only the container user does. + user: "0:0" + cap_add: + - NET_BIND_SERVICE + depends_on: + - caddy + volumes: + - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + postgres: build: context: . diff --git a/deployment/haproxy.cfg b/deployment/haproxy.cfg new file mode 100644 index 0000000..ced9a09 --- /dev/null +++ b/deployment/haproxy.cfg @@ -0,0 +1,26 @@ +global + maxconn 4096 + # Start as root to bind 80/443, then drop privileges. + user haproxy + group haproxy + +defaults + no log + mode tcp + timeout connect 5s + timeout client 2m + timeout server 2m + +frontend fe_http_80 + bind 0.0.0.0:80 + default_backend be_caddy_http + +backend be_caddy_http + server caddy_http 127.0.0.1:8080 send-proxy-v2 + +frontend fe_https_443 + bind 0.0.0.0:443 + default_backend be_caddy_https + +backend be_caddy_https + server caddy_https 127.0.0.1:8443 send-proxy-v2 diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 5323164..e032e6c 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -31,25 +31,19 @@ substep() { echo -e "${MAGENTA}${BOLD}🚀 Deployment${NC}\n" -# Read password with asterisks + read_password() { local password="" local char local old_stty - - # Save current terminal settings + old_stty=$(stty -g 2>/dev/null) - - # Disable echo stty -echo 2>/dev/null - - # Read characters one by one + while IFS= read -rs -n 1 char; do - # Check for Enter key (empty means Enter was pressed) if [ -z "$char" ]; then break fi - # Check for backspace/delete (ASCII 127) if [ "$char" = $'\177' ] || [ "$char" = $'\b' ]; then if [ ${#password} -gt 0 ]; then password="${password%?}" @@ -60,8 +54,7 @@ read_password() { printf "*" >&2 fi done - - # Restore terminal settings + stty "$old_stty" 2>/dev/null echo "" >&2 echo "$password" @@ -95,6 +88,20 @@ REPO_NAME="FromChat" DEPLOY_PATH="~/actions-runner/_work/$REPO_NAME/$REPO_NAME" PLATFORM="linux/arm64" +# Prefer plain `docker build` when target arch matches host arch. +# This avoids Docker Desktop buildx export/load issues and is faster for same-arch builds. +HOST_ARCH_RAW="$(uname -m 2>/dev/null || echo "")" +case "$HOST_ARCH_RAW" in + arm64|aarch64) HOST_ARCH="arm64" ;; + x86_64|amd64) HOST_ARCH="amd64" ;; + *) HOST_ARCH="$HOST_ARCH_RAW" ;; +esac +PLATFORM_ARCH="${PLATFORM##*/}" +USE_DOCKER_BUILD=false +if [ -n "$HOST_ARCH" ] && [ "$HOST_ARCH" = "$PLATFORM_ARCH" ]; then + USE_DOCKER_BUILD=true +fi + # Check if server is provided if [ -z "$SERVER" ]; then error "Server not specified. Usage: $0 [user@host] [deployment_path] [platform]" @@ -146,48 +153,15 @@ if [ "$KEY_LOADED" = false ]; then fi fi -# Check if SSH key authentication already works -if ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$SERVER" "echo 'SSH key works'" >/dev/null 2>&1; then - # SSH key already works, no need to copy - true -else - # Check if our public key is already on the server - KEY_CONTENT=$(cat "$SSH_KEY_PUB_FILE") - if ssh -o BatchMode=no -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$SERVER" " - grep -q '$KEY_CONTENT' ~/.ssh/authorized_keys 2>/dev/null - " >/dev/null 2>&1; then - # Key exists but authentication failed - might be permissions issue - error "SSH key found on server but authentication failed. Check server SSH configuration." - exit 1 - else - # Key not on server, need to copy it - substep "SSH password: " -n - SSH_PASSWORD=$(read_password) - - if [ -z "$SSH_PASSWORD" ]; then - error "No SSH password provided" - exit 1 - fi - - substep "Copying SSH key to server..." - if command -v expect >/dev/null 2>&1; then - expect << EOF >/dev/null 2>&1 -spawn ssh-copy-id -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i "$SSH_KEY_PUB_FILE" "$SERVER" -expect "password:" -send "$SSH_PASSWORD\r" -expect eof -EOF - if [ $? -eq 0 ]; then - true - else - error "Failed to copy SSH key to server" - exit 1 - fi - else - error "expect not available - cannot copy SSH key" - exit 1 - fi - fi +# Require key-based SSH auth; do not attempt to copy keys automatically. +if ! ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$SERVER" "echo 'SSH key works'" >/dev/null 2>&1; then + error "SSH key authentication failed for $SERVER" + echo " Copy your public key to the server, then re-run deploy:" + echo " ssh-copy-id -i \"$SSH_KEY_PUB_FILE\" \"$SERVER\"" + echo "" + echo " Or manually append this key to ~/.ssh/authorized_keys on the server:" + echo " $(cat "$SSH_KEY_PUB_FILE")" + exit 1 fi # ============================================================================ @@ -195,15 +169,7 @@ fi # ============================================================================ SUDO_PASSWORD="" -# If SSH password was provided, try using it for sudo first -if [ -n "$SSH_PASSWORD" ]; then - if echo "$SSH_PASSWORD" | ssh "$SERVER" "sudo -S -v" > /dev/null 2>&1; then - SUDO_PASSWORD="$SSH_PASSWORD" - export SUDO_PASSWORD - fi -fi - -# If we don't have a working sudo password yet, prompt for it +# Prompt for sudo password (optional; leave blank for passwordless sudo) if [ -z "$SUDO_PASSWORD" ]; then while true; do substep "Sudo password: " -n @@ -266,11 +232,6 @@ start_docker_desktop() { return 1 } -# Check buildx -if ! docker buildx version > /dev/null 2>&1; then - error "Docker buildx not available. Install Docker Desktop." -fi - # Check Docker daemon if ! check_docker_daemon; then warning "Docker daemon is not running" @@ -279,35 +240,42 @@ if ! check_docker_daemon; then fi fi -# Setup buildx builder -step "Setting up buildx builder" -BUILDER_NAME="fromchat-builder" -BUILDER_EXISTS=false - -if docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then - BUILDER_EXISTS=true - if ! docker buildx use "$BUILDER_NAME" > /dev/null 2>&1; then - substep "Recreating builder..." - docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true - BUILDER_EXISTS=false - elif ! docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then - substep "Recreating builder (inspection failed)..." - docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true - BUILDER_EXISTS=false +if [ "$USE_DOCKER_BUILD" = false ]; then + # Check buildx + if ! docker buildx version > /dev/null 2>&1; then + error "Docker buildx not available. Install Docker Desktop." fi -fi -if [ "$BUILDER_EXISTS" = false ]; then - substep "Creating builder with persistent cache..." - docker buildx create \ - --name "$BUILDER_NAME" \ - --driver docker-container \ - --driver-opt image=moby/buildkit:latest \ - --use \ - --bootstrap > /dev/null 2>&1 -fi + # Setup buildx builder + step "Setting up buildx builder" + BUILDER_NAME="fromchat-builder" + BUILDER_EXISTS=false -docker buildx use "$BUILDER_NAME" > /dev/null 2>&1 + if docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then + BUILDER_EXISTS=true + if ! docker buildx use "$BUILDER_NAME" > /dev/null 2>&1; then + substep "Recreating builder..." + docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true + BUILDER_EXISTS=false + elif ! docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then + substep "Recreating builder (inspection failed)..." + docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true + BUILDER_EXISTS=false + fi + fi + + if [ "$BUILDER_EXISTS" = false ]; then + substep "Creating builder with persistent cache..." + docker buildx create \ + --name "$BUILDER_NAME" \ + --driver docker-container \ + --driver-opt image=moby/buildkit:latest \ + --use \ + --bootstrap > /dev/null 2>&1 + fi + + docker buildx use "$BUILDER_NAME" > /dev/null 2>&1 +fi # Detect services step "Detecting services" @@ -378,20 +346,439 @@ for SERVICE in $SERVICES; do fi fi - BUILDX_ARGS=(buildx build --platform "$PLATFORM" --file "$DOCKERFILE" --tag "$IMAGE_TAG" --load) - if [ -n "$BUILD_TARGET" ]; then - BUILDX_ARGS+=(--target "$BUILD_TARGET") - fi - BUILDX_ARGS+=("$BUILD_CONTEXT") + if [ "$USE_DOCKER_BUILD" = true ]; then + DOCKER_BUILD_ARGS=(build --platform "$PLATFORM" --file "$DOCKERFILE" --tag "$IMAGE_TAG") + if [ -n "$BUILD_TARGET" ]; then + DOCKER_BUILD_ARGS+=(--target "$BUILD_TARGET") + fi + DOCKER_BUILD_ARGS+=("$BUILD_CONTEXT") - if docker "${BUILDX_ARGS[@]}"; then - echo -e " ${GREEN}✓${NC} Built ${CYAN}$SERVICE${NC}" - BUILT_IMAGES+=("$IMAGE_TAG") - echo "" + if docker "${DOCKER_BUILD_ARGS[@]}"; then + echo -e " ${GREEN}✓${NC} Built ${CYAN}$SERVICE${NC}" + BUILT_IMAGES+=("$IMAGE_TAG") + echo "" + else + error "Build failed for $SERVICE" + exit 1 + fi else - error "Build failed for $SERVICE" - exit 1 + # On macOS Docker Desktop, --load can hang for a long time at "sending tarball". + # Use the docker exporter explicitly to load into the local Docker daemon. + BUILDX_ARGS=(buildx build --platform "$PLATFORM" --file "$DOCKERFILE" --tag "$IMAGE_TAG" --output=type=docker) + if [ -n "$BUILD_TARGET" ]; then + BUILDX_ARGS+=(--target "$BUILD_TARGET") + fi + BUILDX_ARGS+=("$BUILD_CONTEXT") + + if docker "${BUILDX_ARGS[@]}"; then + echo -e " ${GREEN}✓${NC} Built ${CYAN}$SERVICE${NC}" + BUILT_IMAGES+=("$IMAGE_TAG") + echo "" + else + error "Build failed for $SERVICE" + exit 1 + fi fi + + true + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = true ]; then + true + fi + + if [ "$USE_DOCKER_BUILD" = false ]; then + true + fi + + true done success "Build complete! ${#BUILT_IMAGES[@]} image(s) ready" @@ -412,26 +799,37 @@ fi # Detect images based on docker-compose.yml (prefer explicit `image:` entries; fall back to built tags) cd "$DEPLOYMENT_DIR" COMPOSE_SERVICES=$(docker compose -f docker-compose.yml config --services 2>/dev/null || true) -IMAGES=() +PUSH_IMAGES=() +EXTERNAL_IMAGES=() for S in $COMPOSE_SERVICES; do IMAGE_FROM_COMPOSE=$(jq -r --arg s "$S" '.services[$s].image // empty' <<< "$COMPOSE_JSON") + HAS_BUILD=$(jq -r --arg s "$S" '(.services[$s].build // empty) | (if . == "" then "" else "yes" end)' <<< "$COMPOSE_JSON") if [ -n "$IMAGE_FROM_COMPOSE" ]; then - IMAGES+=("$IMAGE_FROM_COMPOSE") + # If it has an explicit image and no build section, it likely won't exist locally (and doesn't need pussh). + if [ -z "$HAS_BUILD" ]; then + EXTERNAL_IMAGES+=("$IMAGE_FROM_COMPOSE") + else + # If a service has both build+image, treat it as a built image (pussh). + PUSH_IMAGES+=("$IMAGE_FROM_COMPOSE") + fi else # If service has a build section (we built it above), use the tag pattern used during build TAG="${PROJECT_NAME}-${S}:latest" # Only include the tag if the image exists locally (avoid pushing unrelated images) if docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${TAG}$"; then - IMAGES+=("$TAG") + PUSH_IMAGES+=("$TAG") fi fi done # Deduplicate while preserving order -if [ ${#IMAGES[@]} -gt 0 ]; then - IMAGES=($(printf "%s\n" "${IMAGES[@]}" | awk '!seen[$0]++')) +if [ ${#PUSH_IMAGES[@]} -gt 0 ]; then + PUSH_IMAGES=($(printf "%s\n" "${PUSH_IMAGES[@]}" | awk '!seen[$0]++')) +fi +if [ ${#EXTERNAL_IMAGES[@]} -gt 0 ]; then + EXTERNAL_IMAGES=($(printf "%s\n" "${EXTERNAL_IMAGES[@]}" | awk '!seen[$0]++')) fi # Verify that all built images are among the detected images to be pushed. @@ -441,7 +839,7 @@ MATCHING_BUILT=0 MISSING_FROM_DETECTED=() for BI in "${BUILT_IMAGES[@]}"; do found=false - for DI in "${IMAGES[@]}"; do + for DI in "${PUSH_IMAGES[@]}"; do if [ "$BI" = "$DI" ]; then found=true break @@ -454,9 +852,9 @@ for BI in "${BUILT_IMAGES[@]}"; do fi done -# Also list detected images that weren't built locally (these are likely external images) +# Also list push images that weren't built locally (these are likely prebuilt local images) NOT_BUILT_DETECTED=() -for DI in "${IMAGES[@]}"; do +for DI in "${PUSH_IMAGES[@]}"; do built=false for BI in "${BUILT_IMAGES[@]}"; do if [ "$DI" = "$BI" ]; then @@ -481,33 +879,44 @@ if [ "$BUILT_COUNT" -ne "$MATCHING_BUILT" ]; then exit 1 fi -if [ ${#IMAGES[@]} -eq 0 ]; then +if [ ${#PUSH_IMAGES[@]} -eq 0 ] && [ ${#EXTERNAL_IMAGES[@]} -eq 0 ]; then error "No images found in docker-compose.yml or built locally for project ${PROJECT_NAME}" fi # Pre-pull unregistry image if needed UNREGISTRY_IMAGE="ghcr.io/psviderski/unregistry" -if ! ssh "$SERVER" "docker images --format '{{.Repository}}:{{.Tag}}' | grep -q '^${UNREGISTRY_IMAGE}$'" 2>/dev/null; then +if ! ssh "$SERVER" "sudo docker images --format '{{.Repository}}:{{.Tag}}' | grep -q '^${UNREGISTRY_IMAGE}$'" 2>/dev/null; then substep "Pulling unregistry image (one-time setup)..." - ssh "$SERVER" "docker pull ${UNREGISTRY_IMAGE}" > /dev/null 2>&1 || true + ssh -tt "$SERVER" "sudo docker pull ${UNREGISTRY_IMAGE}" fi # Transfer images step "Transferring images" -PUSH_FAILED=0 -for IMAGE in "${IMAGES[@]}"; do +for IMAGE in "${PUSH_IMAGES[@]}"; do substep "Pushing ${CYAN}$IMAGE${NC}..." if docker pussh "$IMAGE" "$SERVER"; then echo "" else echo -e " ${RED}✗${NC} Failed to push ${CYAN}$IMAGE${NC}" - PUSH_FAILED=1 echo "" + exit 1 fi done -if [ $PUSH_FAILED -eq 1 ]; then - error "Image transfer failed" +# Pull external images directly on the server (no pussh) +if [ ${#EXTERNAL_IMAGES[@]} -gt 0 ]; then + step "Pulling external images on server" + for IMAGE in "${EXTERNAL_IMAGES[@]}"; do + substep "Pulling ${CYAN}$IMAGE${NC}..." + # Allocate a TTY and do not redirect output so failures are visible. + if ssh -tt "$SERVER" "sudo docker pull $(printf '%q' "$IMAGE")"; then + echo "" + else + echo -e " ${RED}✗${NC} Failed to pull ${CYAN}$IMAGE${NC} on server" + echo "" + exit 1 + fi + done fi # Transfer files