From 655822b953feb7d3f488eeb81d7d7bf591776bc0 Mon Sep 17 00:00:00 2001 From: Vojtech Rysanek Date: Tue, 5 May 2026 20:37:41 +0400 Subject: [PATCH] host-mount: replace named-volume driver_opts with direct service binds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version of docker-compose.host-mount.yml modified the 'data' named volume's driver_opts to point at /data with 'o: bind,rbind'. Docker named volumes have an immutability footgun: once a volume is created, its driver options are fixed for the life of the volume. Editing this file and re-running 'docker compose up -d' does NOT propagate the new options to existing volumes — they keep whatever options were in effect at create time. This bit a deployer (Groupon FoundryAI) on 2026-05-05: the volume was created before this overlay had bind,rbind, kept the old bind (non-recursive) propagation, and containers wrote to a shadowed subdirectory of the parent disk instead of the nested child mount. DuckDB went FATAL on a root-owned WAL during a routine container recreate; sign-in broke. Recovery required docker volume rm + manual data migration on every affected VM. Direct service-level bind mounts ('/host/path:/container/path') don't go through Docker's volume layer at all. They re-evaluate mount options every container start, and modern Docker Engine (20.10+) defaults to recursive bind for these. No options to forget, no immutable state to migrate, no shadow-mount class. Validated via 'docker compose config' merge — overlay correctly replaces 'data:/data' with bind type:none on app, extract, scheduler, telegram-bot, ws-gateway. Compose-spec version note: !override merge tag is part of the Compose Specification supported by Docker Compose v2.20+. Tested against Compose v5.1.3 used by Groupon's deployment. --- docker-compose.host-mount.yml | 84 ++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/docker-compose.host-mount.yml b/docker-compose.host-mount.yml index dd06534..af03c33 100644 --- a/docker-compose.host-mount.yml +++ b/docker-compose.host-mount.yml @@ -1,14 +1,41 @@ -# Bind-mount overlay — replaces the `data` named volume with a bind mount -# to /data on the host. +# Bind-mount overlay — replaces the `data` named volume with a direct +# host bind mount per service. # -# Use this when /data is a persistent disk mounted by the VM startup script, -# so Agnes data lives on the PD (not on the boot disk's Docker volume). +# Why direct service-level bind, not driver_opts on the named volume +# ------------------------------------------------------------------ +# The previous version of this file modified the `data` named volume's +# `driver_opts` to point at /data with `o: bind,rbind`. Docker named +# volumes have an immutability footgun: once a volume is created, its +# driver options are fixed for the life of the volume. Editing this +# file and re-running `docker compose up -d` does NOT propagate the +# new options to existing volumes — they keep whatever options were +# in effect at create time. # -# `bind,rbind` (recursive bind) is required when the host nests a second -# disk under /data — e.g. the dual-disk layout where sdb is mounted on /data -# and sdc on /data/state. A plain `bind` captures only the top-level mount -# and silently shadows the sub-mount with an empty subdirectory inside the -# container, causing the app to write to the wrong disk. +# This bit a deployer (Groupon FoundryAI) on 2026-05-05: the volume +# was created before this overlay had `bind,rbind`, kept the old +# `bind` (non-recursive) propagation, and containers wrote to a +# shadowed subdirectory of the parent disk instead of the nested +# child mount. DuckDB went FATAL on a root-owned WAL during a +# routine container recreate; sign-in broke. +# +# Direct service-level bind mounts (`/host/path:/container/path`) +# don't go through Docker's volume layer at all. They re-evaluate +# the mount options every container start, and modern Docker Engine +# (20.10+) defaults to recursive bind for these. No options to +# forget, no immutable state to migrate, no shadow-mount class. +# +# What this overlay does +# ---------------------- +# `volumes: !override` on each service replaces the base +# `data:/data` named-volume mount with a direct `/data:/data` host +# bind. The named volume `data:` declared at the bottom of +# docker-compose.yml is left intact (still useful for local-dev +# `compose up` without this overlay) but is no longer referenced +# by any service when the overlay is active. +# +# When the operator's host has a nested mount under /data (e.g. a +# separate state disk mounted at /data/state), the recursive bind +# carries that nested mount into every container automatically. # # Usage (combined with docker-compose.prod.yml): # docker compose \ @@ -17,11 +44,34 @@ # -f docker-compose.host-mount.yml \ # up -d # -# Do NOT use this overlay in CI — /data does not exist on GitHub runners. -volumes: - data: - driver: local - driver_opts: - type: none - o: bind,rbind - device: /data +# Do NOT use this overlay in CI — /data does not exist on GitHub +# runners. +# +# Compose-spec version requirement: !override merge tag is part of +# the Compose Specification supported by Docker Compose v2.20+ and +# the compose-go library used by Compose v5+. If you need to support +# older clients, fork this overlay into per-service files. + +services: + app: + volumes: !override + - /data:/data + - ./config:/app/config:ro + + extract: + volumes: !override + - /data:/data + - ./config:/app/config:ro + + scheduler: + volumes: !override + - /data:/data + - ./config:/app/config:ro + + telegram-bot: + volumes: !override + - /data:/data + + ws-gateway: + volumes: !override + - /data:/data