The firewall script deleted its rules before re-inserting them, and Docker restarted the stack on its own (restart: unless-stopped) with no ordering against the firewall unit, so port 8090 could be open to the LAN while the rules were reapplied. - mealquest-firewall.sh fails closed: if the chain is not already exactly right, it inserts the DROP at position 1 first, deletes its other tagged rules by number, then inserts the two RETURN rules above it. A correct chain is left untouched. - New mealquest.service owns the compose stack: Requires= and After= mealquest-firewall.service, PartOf=docker.service. - compose.yaml: restart: "no" on both services, so Docker never starts them before the firewall. - RUNBOOK: deploy, rollback, restore and key rotation start and stop the stack with systemctl; documents the stack unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>