diff --git a/README.md b/README.md
index 7979d6b..9facd8e 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,10 @@ If you want to learn more about Quarkus, please visit its website: https://quark
## Building and deploying natively
Build the native image with the Mandrel-based multi-stage Dockerfile and push it
-directly to a remote Docker host via [unregistry](https://github.com/psviderski/unregistry)
-(no external registry required; only missing layers are transferred over SSH):
+to a remote Docker host over an SSH tunnel. The script starts a temporary
+`registry:2` container on the remote (localhost only), forwards it to the build
+machine, pushes the image through that tunnel, then pulls and retags it on the
+remote Docker daemon. No external registry is required.
```shell script
./scripts/build-and-push.sh --remote deploy@prod.example.com
@@ -16,7 +18,8 @@ directly to a remote Docker host via [unregistry](https://github.com/psviderski/
The script reads `artifactId` and `version` from `pom.xml` for the image name
and tag, builds for `linux/amd64`, and verifies the image is present on the
-remote after pushing. See `./scripts/build-and-push.sh --help` for all flags.
+remote after pushing. The temporary registry container and SSH tunnel are removed
+when the script finishes. See `./scripts/build-and-push.sh --help` for all flags.
It can be invoked from any directory: by default it resolves the project root
from the script's own location. Override with `--project-dir PATH` or the
diff --git a/pom.xml b/pom.xml
index 383d922..9d526f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
4.0.0
com.example
test-quarkus
- 1.0.0-SNAPSHOT
+ 1.0.1
jar
Test Quarkus
diff --git a/scripts/build-and-push.sh b/scripts/build-and-push.sh
index eda4c5d..055a79a 100755
--- a/scripts/build-and-push.sh
+++ b/scripts/build-and-push.sh
@@ -1,20 +1,18 @@
#!/usr/bin/env bash
#
# Build the native Quarkus image and push it to a remote Docker host
-# via docker-pussh (unregistry): https://github.com/psviderski/unregistry
+# via a temporary registry:2 on the remote, reached through an SSH tunnel.
#
# - Image name/tag default to pom.xml properties (quarkus.application.*)
# - Builds for linux/amd64 by default (cross-compile from arm64 hosts)
-# - Transfers only missing layers over SSH (no external registry required)
+# - Transfers layers over the tunnel (no external registry required)
set -euo pipefail
IFS=$'\n\t'
readonly DOCKERFILE="src/main/docker/Dockerfile"
readonly DEFAULT_PLATFORM="linux/amd64"
-readonly UNREGISTRY_PLUGIN="https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh"
-readonly PLUGIN_DIR="${HOME}/.docker/cli-plugins"
-readonly PLUGIN_PATH="${PLUGIN_DIR}/docker-pussh"
+readonly TEMP_REGISTRY_IMAGE="registry:2"
readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
REMOTE=""
@@ -56,7 +54,7 @@ usage() {
Usage: $(basename "$0") --remote USER@HOST[:PORT] [options]
Build the native Quarkus image and push it to a remote Docker host using
-unregistry (docker-pussh).
+a temporary Docker registry on the remote, reached through an SSH tunnel.
Required:
--remote TARGET SSH destination. Either 'user@host[:port]'
@@ -72,10 +70,10 @@ Working directory (defaults to the parent of the script's directory):
(override with env PROJECT_DIR)
Build options:
- --skip-build Skip docker build, only pussh an existing image
+ --skip-build Skip docker build, only push an existing image
(platform is fixed to ${DEFAULT_PLATFORM})
-SSH options forwarded to docker pussh:
+SSH options:
--ssh-key PATH Private key file (overrides SSH config)
--ssh-config PATH SSH config file (default: ~/.ssh/config)
@@ -178,6 +176,126 @@ resolve_ssh_alias() {
printf '%s@%s:%s|%s\n' "$user" "$hostname" "$port" "$identity"
}
+random_port() {
+ echo $((55000 + RANDOM % 10536))
+}
+
+# REMOTE_TARGET is user@host:port; OpenSSH needs -p and user@host separately.
+parse_ssh_target() {
+ if [[ "$REMOTE_TARGET" == *:* ]]; then
+ SSH_PORT="${REMOTE_TARGET##*:}"
+ SSH_HOST_USER="${REMOTE_TARGET%:*}"
+ else
+ SSH_PORT="22"
+ SSH_HOST_USER="$REMOTE_TARGET"
+ fi
+}
+
+# Populates global ssh_args for ssh(1).
+build_ssh_args() {
+ ssh_args=()
+ [[ -n "$SSH_KEY" ]] && ssh_args+=(-i "$SSH_KEY")
+ [[ -n "$SSH_CONFIG" ]] && ssh_args+=(-F "$SSH_CONFIG")
+ [[ "$SSH_PORT" != "22" ]] && ssh_args+=(-p "$SSH_PORT")
+ ssh_args+=("$SSH_HOST_USER")
+}
+
+run_remote() {
+ ssh "${ssh_args[@]}" "$@"
+}
+
+# Sets REMOTE_DOCKER to "docker" or "sudo -n docker".
+detect_remote_docker() {
+ if run_remote "docker info" >/dev/null 2>&1; then
+ REMOTE_DOCKER="docker"
+ return 0
+ fi
+ if run_remote "sudo -n docker info" >/dev/null 2>&1; then
+ REMOTE_DOCKER="sudo -n docker"
+ return 0
+ fi
+ die "Cannot run docker on ${REMOTE_TARGET}. Ensure Docker is installed and the SSH user may run docker (or passwordless sudo docker)."
+}
+
+REG_PUSH_CONTAINER=""
+REG_REMOTE_PORT=""
+REG_LOCAL_PORT=""
+TUNNEL_PID=""
+LOCAL_REGISTRY_REF=""
+
+cleanup_push_resources() {
+ [[ -n "${LOCAL_REGISTRY_REF:-}" ]] \
+ && docker rmi "$LOCAL_REGISTRY_REF" >/dev/null 2>&1 || true
+ if [[ -n "${TUNNEL_PID:-}" ]]; then
+ kill "$TUNNEL_PID" >/dev/null 2>&1 || true
+ wait "$TUNNEL_PID" 2>/dev/null || true
+ fi
+ if [[ -n "${REG_PUSH_CONTAINER:-}" && -n "${REMOTE_DOCKER:-}" && -n "${REMOTE_TARGET:-}" ]]; then
+ parse_ssh_target
+ build_ssh_args
+ run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true
+ fi
+}
+
+start_remote_registry() {
+ local attempt output
+ run_remote "${REMOTE_DOCKER} image inspect ${TEMP_REGISTRY_IMAGE}" >/dev/null 2>&1 \
+ || run_remote "${REMOTE_DOCKER} pull ${TEMP_REGISTRY_IMAGE}"
+
+ for attempt in $(seq 1 10); do
+ REG_REMOTE_PORT=$(random_port)
+ REG_PUSH_CONTAINER="reg-push-$$-${REG_REMOTE_PORT}"
+ if output=$(run_remote "${REMOTE_DOCKER} run -d \
+ --name ${REG_PUSH_CONTAINER} \
+ -p 127.0.0.1:${REG_REMOTE_PORT}:5000 \
+ ${TEMP_REGISTRY_IMAGE}" 2>&1); then
+ return 0
+ fi
+ run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true
+ if ! echo "$output" | grep -qi bind; then
+ die "Failed to start temporary registry on remote:\n${output}"
+ fi
+ done
+ die "Failed to start temporary registry on remote (port binding)."
+}
+
+start_ssh_tunnel() {
+ local attempt
+ for attempt in $(seq 1 10); do
+ REG_LOCAL_PORT=$(random_port)
+ if command -v nc >/dev/null 2>&1 && nc -z 127.0.0.1 "$REG_LOCAL_PORT" 2>/dev/null; then
+ continue
+ fi
+ ssh "${ssh_args[@]}" \
+ -o ExitOnForwardFailure=yes \
+ -N \
+ -L "127.0.0.1:${REG_LOCAL_PORT}:127.0.0.1:${REG_REMOTE_PORT}" &
+ TUNNEL_PID=$!
+ sleep 1
+ if kill -0 "$TUNNEL_PID" 2>/dev/null; then
+ return 0
+ fi
+ TUNNEL_PID=""
+ done
+ die "Failed to establish SSH tunnel to remote registry."
+}
+
+push_via_registry_tunnel() {
+ LOCAL_REGISTRY_REF="localhost:${REG_LOCAL_PORT}/${IMAGE_NAME}:${IMAGE_TAG}"
+ local remote_registry_ref="localhost:${REG_REMOTE_PORT}/${IMAGE_NAME}:${IMAGE_TAG}"
+
+ log "Tagging ${IMAGE_REF} as ${LOCAL_REGISTRY_REF}…"
+ docker tag "$IMAGE_REF" "$LOCAL_REGISTRY_REF"
+
+ log "Pushing ${LOCAL_REGISTRY_REF} through SSH tunnel…"
+ docker push "$LOCAL_REGISTRY_REF"
+
+ log "Pulling image on remote and retagging as ${IMAGE_REF}…"
+ run_remote "${REMOTE_DOCKER} pull ${remote_registry_ref}"
+ run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${IMAGE_REF}"
+ run_remote "${REMOTE_DOCKER} rmi ${remote_registry_ref}" >/dev/null || true
+}
+
# ---------- arg parsing ----------
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -226,26 +344,17 @@ log "Dockerfile: ${DOCKERFILE}"
log "Remote: ${REMOTE_TARGET}"
require docker
+require ssh
docker info >/dev/null 2>&1 \
|| die "Docker daemon is not reachable. Start Docker Desktop (or your daemon) and retry."
-if ! docker pussh --help >/dev/null 2>&1; then
- cat >&2 <