fix(deploy): support web updates with Apple container

This commit is contained in:
anguobao123
2026-09-09 17:05:45 +08:00
parent 98d86915be
commit 26798773cb
5 changed files with 94 additions and 8 deletions
+10 -2
View File
@@ -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:
+1 -1
View File
@@ -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.
+58 -3
View File
@@ -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}" <<EOF
@@ -488,6 +488,7 @@ REDIS_HOST=${REDIS_ADDRESS}
REDIS_PORT=6379
REDIS_PASSWORD=${REDIS_PASSWORD}
DATA_DIR=/app/storage/data
APPLE_CONTAINER_SUB2API_IMAGE_ID=${APP_IMAGE_ID}
EOF
chmod 600 "${APP_ENV_FILE}"
}
@@ -533,10 +534,61 @@ create_app_container() {
--volume "${APP_VOLUME}:/app/storage" \
--entrypoint /bin/sh \
"${APP_IMAGE}" \
-c 'set -e; mkdir -p "$DATA_DIR"; chown -R sub2api:sub2api "$DATA_DIR"; exec su-exec sub2api /app/sub2api' \
-c '
set -e
mkdir -p "$DATA_DIR"
chown -R sub2api:sub2api "$DATA_DIR"
runtime_dir=/app/storage/runtime
runtime_binary="$runtime_dir/sub2api"
image_marker="$runtime_dir/base-image-id"
installed_image_id=""
if [ -f "$image_marker" ]; then
installed_image_id="$(cat "$image_marker")"
fi
if [ ! -x "$runtime_binary" ] || [ "$installed_image_id" != "$APPLE_CONTAINER_SUB2API_IMAGE_ID" ]; then
mkdir -p "$runtime_dir"
cp /app/sub2api "${runtime_binary}.new"
chown sub2api:sub2api "${runtime_binary}.new"
chmod 0755 "${runtime_binary}.new"
mv "${runtime_binary}.new" "$runtime_binary"
printf "%s\n" "$APPLE_CONTAINER_SUB2API_IMAGE_ID" >"${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}"
+10
View File
@@ -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
+15 -2
View File
@@ -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)