Skip to content

Commit 0bad4c4

Browse files
DAdjadjclaude
andcommitted
Fix self-update: use helper container to avoid race condition
The old approach ran docker stop/rm/run from inside the container being updated. When docker stop killed the container, the shell died with it, so docker rm and docker run never executed — leaving the container dead (white screen) or stuck in an update loop. Now spawns a separate helper container that runs docker compose up -d from the outside, so it survives the bridge-bank container being recreated. Existing users not using Watchtower/Portainer need a one-time manual update: docker compose pull && docker compose up -d Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f223809 commit 0bad4c4

File tree

1 file changed

+19
-21
lines changed

1 file changed

+19
-21
lines changed

app/web/server.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -802,33 +802,31 @@ def update_run():
802802
if image_is_same and container_current:
803803
return jsonify({"up_to_date": True})
804804
logger.info("Recreating container (new_image=%s, container_outdated=%s)", not image_is_same, not container_current)
805-
# docker compose up -d can't work from inside the container because it
806-
# can't stop itself. Read mount/port config from the running container
807-
# and recreate with docker run.
805+
# Find the host path of the compose file from the /compose mount
808806
import json as _json
809807
mounts_json = subprocess.run(
810808
["docker", "inspect", "--format", "{{json .Mounts}}", CONTAINER_NAME],
811809
capture_output=True, text=True, timeout=5
812810
).stdout.strip()
813-
ports_json = subprocess.run(
814-
["docker", "inspect", "--format", "{{json .HostConfig.PortBindings}}", CONTAINER_NAME],
815-
capture_output=True, text=True, timeout=5
816-
).stdout.strip()
817-
vol_args = []
811+
compose_host_path = ""
818812
for m in _json.loads(mounts_json or "[]"):
819-
ro = ":ro" if not m.get("RW", True) else ""
820-
vol_args += ["-v", f"{m['Source']}:{m['Destination']}{ro}"]
821-
port_args = []
822-
for container_port, bindings in _json.loads(ports_json or "{}").items():
823-
for b in (bindings or []):
824-
host_port = b.get("HostPort", "")
825-
if host_port:
826-
port_args += ["-p", f"{host_port}:{container_port.split('/')[0]}"]
827-
run_parts = ["docker", "run", "-d", "--name", CONTAINER_NAME, "--restart", "unless-stopped"] + port_args + vol_args + [IMAGE_NAME]
828-
run_cmd = " ".join(run_parts)
829-
full_cmd = f"sleep 2 && docker stop {CONTAINER_NAME} && docker rm {CONTAINER_NAME} && {run_cmd}"
830-
logger.info("Update command: %s", full_cmd)
831-
subprocess.Popen(["sh", "-c", full_cmd], start_new_session=True)
813+
if m.get("Destination") == "/compose":
814+
compose_host_path = m["Source"]
815+
break
816+
if not compose_host_path:
817+
return jsonify({"error": "Compose file mount not found. Make sure the docker-compose.yml directory is mounted at /compose."}), 400
818+
# Spawn a helper container to run docker compose up -d. This avoids the
819+
# race condition of trying to stop/recreate ourselves from within.
820+
compose_cmd = f"sleep 2 && docker compose -f /compose/docker-compose.yml up -d --force-recreate"
821+
helper_cmd = [
822+
"docker", "run", "-d", "--rm",
823+
"-v", "/var/run/docker.sock:/var/run/docker.sock",
824+
"-v", f"{compose_host_path}:/compose:ro",
825+
IMAGE_NAME,
826+
"sh", "-c", compose_cmd,
827+
]
828+
logger.info("Update via helper container: %s", " ".join(helper_cmd))
829+
subprocess.run(helper_cmd, capture_output=True, text=True, timeout=15)
832830
db.set_setting("update_available", "0")
833831
return jsonify({"updating": True})
834832
except FileNotFoundError:

0 commit comments

Comments
 (0)