From 26798773cb26ad95bd537877efa2cd94aa5cb20c Mon Sep 17 00:00:00 2001 From: anguobao123 <205642466+anguobao123@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:05:45 +0800 Subject: [PATCH] fix(deploy): support web updates with Apple container --- deploy/APPLE_CONTAINER.md | 12 +++++- deploy/README.md | 2 +- deploy/apple-container.sh | 61 ++++++++++++++++++++++++++-- deploy/tests/apple-container-test.sh | 10 +++++ deploy/tests/fixtures/bin/container | 17 +++++++- 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/deploy/APPLE_CONTAINER.md b/deploy/APPLE_CONTAINER.md index dfce11402b..9e44f24408 100644 --- a/deploy/APPLE_CONTAINER.md +++ b/deploy/APPLE_CONTAINER.md @@ -6,7 +6,7 @@ Sub2API can run as a native three-service stack with Apple's `container` CLI. Th Apple `container` support is intended for local development and operator-managed deployments on a Mac. Docker Compose remains the recommended production deployment path. -Apple `container` 1.1 does not provide restart policies, automatic startup, workload health scheduling, a Docker API socket, or full Compose orchestration. `apple-container.sh` supplies ordered startup and readiness checks when you invoke it, but it is not a continuously running supervisor. +Apple `container` 1.1 does not provide restart policies, automatic startup, workload health scheduling, a Docker API socket, or full Compose orchestration. `apple-container.sh` supplies ordered startup and readiness checks when you invoke it. Inside the application container, a small supervisor relaunches the Sub2API process after the Web UI requests a restart; it does not restart stopped containers or the stack itself. ## Requirements @@ -146,7 +146,7 @@ The script creates only resources carrying the `org.sub2api.stack=apple-containe | Network | `sub2api-apple` | | Volumes | `sub2api-apple-data`, `sub2api-apple-postgres-data`, `sub2api-apple-redis-data` | -The PostgreSQL volume is mounted at `/var/lib/postgresql`, retaining PostgreSQL 18's default child data directory. Sub2API and Redis also store data in child directories below their Apple volume mount points. This is required because Apple named volumes do not have Docker's copy-up and mount-point ownership behavior. +The PostgreSQL volume is mounted at `/var/lib/postgresql`, retaining PostgreSQL 18's default child data directory. Sub2API data and its updatable runtime binary use separate child directories in `sub2api-apple-data`; Redis also stores data below its Apple volume mount point. This is required because Apple named volumes do not have Docker's copy-up and mount-point ownership behavior. ## Networking @@ -158,6 +158,14 @@ The application container is intentionally recreated by every `up` and `restart` The script checks the published `/health` endpoint from macOS before reporting success. Approve the Local Network prompt on first startup. If the internal probe succeeds but the host-port probe fails with a connection reset, enable Local Network access for `container-runtime-linux`, run `container system stop` followed by `container system start`, and then run `up` again. Runtime upgrades may prompt for permission again. +## Web UI Updates + +The Web UI uses the same update flow as the Docker deployment: it downloads a release over GitHub, atomically replaces the active executable, and asks the application to restart. Docker supplies the restart policy in a Compose deployment; `apple-container.sh` supplies an equivalent process supervisor inside the Apple application container. + +The active executable lives in `sub2api-apple-data` so a later `up`, `restart`, or `up --recreate` does not discard an update downloaded from the Web UI. The script records the configured base image ID alongside it; when `APPLE_CONTAINER_SUB2API_IMAGE` resolves to a different image ID, the image's `/app/sub2api` becomes the new active executable. This keeps explicit image upgrades authoritative while preserving in-place updates across routine application-container recreation. + +If GitHub is not reachable directly, set `UPDATE_PROXY_URL` to a proxy address reachable from the Apple container VM. A proxy listening only on the Mac's `127.0.0.1` is not reachable as `127.0.0.1` from inside the VM; use an appropriately restricted host gateway listener instead. + ## Backup and Upgrade Pin image release tags or digests in `.env` before using this workflow for persistent data. Before an application or database image upgrade, create backups while the stack is healthy: diff --git a/deploy/README.md b/deploy/README.md index b4f8d094f4..f38257e397 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -42,7 +42,7 @@ Apple-silicon Macs running macOS 26 can run the complete Sub2API, PostgreSQL, an ./apple-container.sh logs app -f ``` -The script uses Apple named volumes, starts dependencies in order, and performs live readiness checks. It does not provide a continuous restart supervisor; run `./apple-container.sh up` after a host reboot. Docker Compose remains the recommended production deployment path. +The script uses Apple named volumes, starts dependencies in order, and performs live readiness checks. The application container supervises the Sub2API process so the Web UI's update-and-restart flow can relaunch an updated binary. It does not provide host-level automatic startup; run `./apple-container.sh up` after a host reboot. Docker Compose remains the recommended production deployment path. See [APPLE_CONTAINER.md](./APPLE_CONTAINER.md) for configuration, upgrades, persistence, networking behavior, and limitations. diff --git a/deploy/apple-container.sh b/deploy/apple-container.sh index cb646f1c62..5a44ae5b30 100755 --- a/deploy/apple-container.sh +++ b/deploy/apple-container.sh @@ -469,8 +469,8 @@ EOF } prepare_app_environment() { - [[ -n "${POSTGRES_ADDRESS}" && -n "${REDIS_ADDRESS}" ]] || \ - die "Dependency network addresses are not available." + [[ -n "${POSTGRES_ADDRESS}" && -n "${REDIS_ADDRESS}" && -n "${APP_IMAGE_ID}" ]] || \ + die "Dependency network addresses or application image ID are not available." cp "${ENV_FILE}" "${APP_ENV_FILE}" cat >>"${APP_ENV_FILE}" <"${image_marker}.new" + mv "${image_marker}.new" "$image_marker" + rm -f "${runtime_binary}.backup" +fi +chown -R sub2api:sub2api "$runtime_dir" + +child_pid="" +stop() { + trap - TERM INT + if [ -n "$child_pid" ]; then + kill -TERM "$child_pid" 2>/dev/null || true + wait "$child_pid" 2>/dev/null || true + fi + exit 0 +} +trap stop TERM INT + +while true; do + su-exec sub2api "$runtime_binary" & + child_pid=$! + set +e + wait "$child_pid" + status=$? + set -e + child_pid="" + echo "Sub2API exited with status ${status}; restarting in 1 second..." >&2 + sleep 1 +done +' \ >/dev/null } +application_image_id() { + container image inspect "${APP_IMAGE}" | \ + plutil -extract 0.id raw -o - - +} + ensure_container() { local container_name=$1 local create_function=$2 @@ -680,6 +732,9 @@ cmd_up() { ensure_image_available "${APP_IMAGE}" ensure_image_available "${POSTGRES_IMAGE}" ensure_image_available "${REDIS_IMAGE}" + APP_IMAGE_ID="$(application_image_id)" || \ + die "Unable to inspect application image ID for ${APP_IMAGE}." + [[ -n "${APP_IMAGE_ID}" ]] || die "Application image ID must not be empty." if [[ "${recreate}" == true ]]; then delete_container_if_present "${APP_CONTAINER}" diff --git a/deploy/tests/apple-container-test.sh b/deploy/tests/apple-container-test.sh index d1a3aa7c1e..f4551118c5 100755 --- a/deploy/tests/apple-container-test.sh +++ b/deploy/tests/apple-container-test.sh @@ -48,6 +48,16 @@ assert_exists "${STATE_DIR}/containers/sub2api-apple" assert_exists "${STATE_DIR}/containers/sub2api-apple-postgres" assert_exists "${STATE_DIR}/containers/sub2api-apple-redis" assert_exists "${STATE_DIR}/running/sub2api-apple" +grep -q '^while true; do$' "${STATE_DIR}/create-arguments/sub2api-apple" || \ + fail "app container does not supervise the Sub2API process" +grep -q '^ su-exec sub2api "$runtime_binary" &$' "${STATE_DIR}/create-arguments/sub2api-apple" || \ + fail "app supervisor does not launch the updatable Sub2API binary" +grep -q '^trap stop TERM INT$' "${STATE_DIR}/create-arguments/sub2api-apple" || \ + fail "app supervisor does not handle container stop signals" +grep -q '^runtime_binary="$runtime_dir/sub2api"$' "${STATE_DIR}/create-arguments/sub2api-apple" || \ + fail "app container does not run its updatable binary from persistent storage" +grep -q '^APPLE_CONTAINER_SUB2API_IMAGE_ID=fake-image-id$' "${STATE_DIR}/env-files/sub2api-apple" || \ + fail "app container did not receive the inspected base image ID" [[ ! -s "${STATE_DIR}/network-subnets/sub2api-apple" ]] || \ fail "up passed a subnet when APPLE_CONTAINER_NETWORK_SUBNET was unset" "${SCRIPT}" status >/dev/null diff --git a/deploy/tests/fixtures/bin/container b/deploy/tests/fixtures/bin/container index 3fd75457bb..62ae9f83d3 100755 --- a/deploy/tests/fixtures/bin/container +++ b/deploy/tests/fixtures/bin/container @@ -9,6 +9,8 @@ mkdir -p \ "${STATE_DIR}/networks" \ "${STATE_DIR}/network-subnets" \ "${STATE_DIR}/volumes" \ + "${STATE_DIR}/create-arguments" \ + "${STATE_DIR}/env-files" \ "${STATE_DIR}/unowned/container" \ "${STATE_DIR}/unowned/network" \ "${STATE_DIR}/unowned/volume" @@ -129,19 +131,26 @@ case "${command}" in image) subcommand=${1-} case "${subcommand}" in - inspect|pull) exit 0 ;; + inspect) printf '[{"id":"fake-image-id"}]\n' ;; + pull) exit 0 ;; *) exit 1 ;; esac ;; create) + create_arguments=("$@") name="" + env_file="" while [[ $# -gt 0 ]]; do case "$1" in --name) name=$2 shift 2 ;; - --label|--network|--platform|--ulimit|--env-file|--volume|--entrypoint|--publish) + --env-file) + env_file=$2 + shift 2 + ;; + --label|--network|--platform|--ulimit|--volume|--entrypoint|--publish) shift 2 ;; *) @@ -150,6 +159,10 @@ case "${command}" in esac done [[ -n "${name}" ]] + printf '%s\n' "${create_arguments[@]}" >"${STATE_DIR}/create-arguments/${name}" + if [[ -n "${env_file}" ]]; then + cp "${env_file}" "${STATE_DIR}/env-files/${name}" + fi touch "${STATE_DIR}/containers/${name}" ;; inspect)