From 2073be2a78ae7487091cd3c25adb2404de02f242 Mon Sep 17 00:00:00 2001 From: rdwr-rahulk Date: Wed, 16 Sep 2026 16:58:02 +0530 Subject: [PATCH 1/3] V 1.5.3 --- .env.example | 8 ++++++++ DEPLOYMENT.md | 8 +++++++- Dockerfile | 6 ++++++ README.md | 9 +++++++-- docker-compose.build.yaml | 18 ++++++++++++++++++ docker-compose.yaml | 18 ++++++++++++++++++ install.sh | 27 +++++++++++++++++++++++++++ 7 files changed, 91 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 8752222..900f0b9 100644 --- a/.env.example +++ b/.env.example @@ -36,5 +36,13 @@ SMTP_PASSWORD=your-smtp-password/api key value # No credentials needed in env — syslog is unauthenticated (UDP/TCP). # Configure host, port, protocol, and facility entirely in watchdog-config.yaml. +# ── Docker socket access (non-root hardening) ──────────────────────────────── +#GID = Group ID — a number Linux uses to identify a user group (the group equivalent of a user's UID). +# GID of the group that owns /var/run/docker.sock on this host. The container +# runs as a non-root user and joins this group (via group_add in +# docker-compose.yaml) to read the socket. install.sh auto-detects this; for +# manual setups find it with: stat -c '%g' /var/run/docker.sock +DOCKER_GID=999 + # ── Tuning ──────────────────────────────────────────────────────────────────── LOG_LEVEL=INFO diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 85360c8..50613b9 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -49,6 +49,8 @@ This guide covers initial deployment, alert channel configuration, and ongoing o | Git | Required to clone the repository | | Host permissions | Root, or membership in the `docker` group (to read `/var/run/docker.sock`) | +> The watchdog container itself runs as a non-root user and joins the host's `docker` group at runtime (via the `DOCKER_GID` value in `.env`, auto-detected by `install.sh`) to read the socket — see [2.2 Configure Environment Variables (.env)](#22-configure-environment-variables-env). + ```bash docker compose version ``` @@ -115,6 +117,10 @@ SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../... SMTP_USERNAME=your-smtp-username/login email SMTP_PASSWORD=your-smtp-password/api key value +# Docker socket access (non-root container) — GID of the group that owns +# /var/run/docker.sock on this host. Find it with: stat -c '%g' /var/run/docker.sock +DOCKER_GID=999 + # Tuning (optional — defaults shown) LOG_LEVEL=INFO ``` @@ -723,7 +729,7 @@ docker compose logs docker-container-watchdog ``` Common causes: -- `/var/run/docker.sock` is not accessible — ensure the host socket exists and the container has read access +- `/var/run/docker.sock` is not accessible — ensure the host socket exists and the container has read access. The container runs as a non-root user and needs `DOCKER_GID` in `.env` to match the socket's actual group (`stat -c '%g' /var/run/docker.sock`) — see [2.2 Configure Environment Variables (.env)](#22-configure-environment-variables-env) - Missing `.env` file — run `cp .env.example .env` and fill in values ### Alert Notifications Not Received diff --git a/Dockerfile b/Dockerfile index 778f5fc..be62abe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,10 @@ RUN pip install --no-cache-dir \ # Copy agent COPY watchdog.py . +# Non-root — the docker.sock group membership needed to read the socket is +# granted at runtime via `group_add` in docker-compose.yaml (GID varies per host). +RUN useradd --uid 1000 --create-home --shell /usr/sbin/nologin watchdog \ + && chown -R watchdog:watchdog /app +USER watchdog + CMD ["python3", "-u", "watchdog.py"] diff --git a/README.md b/README.md index 11ff3c3..b22f35c 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,14 @@ Two additional deduplication rules suppress redundant alerts at the event level: | Item | Size | |------|------| | Docker image (`watchdog:latest`) | ~180 MB (python:3.11-slim base + dependencies) | -| Running container (memory) | ~50–80 MB | +| Running container (memory) | ~50–80 MB baseline; capped at `mem_limit: 256m` in `docker-compose.yaml` | | `watchdog.tar` export | ~170 MB | -> **Note:** The values above are approximate and may vary depending on the host operating system, Docker version, and installed dependencies. +> **Note:** The values above are approximate and may vary depending on the host operating system, Docker version, and installed dependencies. If usage approaches the 256 MB limit (check with `docker stats docker-container-watchdog`), raise `mem_limit`/`mem_reservation` in `docker-compose.yaml` rather than removing the limit. + +### Container Hardening + +The container runs as a non-root user (UID 1000) with a read-only root filesystem, no extra Linux capabilities (`cap_drop: ALL`), and `no-new-privileges` set. Access to `/var/run/docker.sock` is granted by adding the container's user to the host's `docker` group via `group_add` — the GID is auto-detected by `install.sh` and stored as `DOCKER_GID` in `.env` (see [DEPLOYMENT.md](DEPLOYMENT.md#22-configure-environment-variables-env)). CPU and process count are also capped (`cpus: "0.50"`, `pids_limit: 200`) so a leak or runaway condition is contained to this container instead of the host. ### Python Dependencies @@ -556,6 +560,7 @@ Probe selection is automatic: containers with a Docker `HEALTHCHECK` are monitor | Version | Date | Author | Changes | |---------|------------|--------|---------| +| 1.5.3 | 2026-09-16 | Rahul Kumar | Resource Limits Enforced | | 1.5.2 | 2026-08-31 | Rahul Kumar | Updated error message"Suppressing expected Cyber Controller SQL dump syntax-check container termination (exit 137) alert" | | 1.5.1 | 2026-08-31 | Rahul Kumar | fixed ignore Dynamic container crash alert | | 1.5.0 | 2026-08-27 | Rahul Kumar | Added ignore Dynamic container crash alert | diff --git a/docker-compose.build.yaml b/docker-compose.build.yaml index 85a98fb..505b458 100644 --- a/docker-compose.build.yaml +++ b/docker-compose.build.yaml @@ -26,6 +26,24 @@ services: - /var/run/docker.sock:/var/run/docker.sock:ro - ./watchdog-config.yaml:/etc/watchdog/watchdog-config.yaml:ro - ./watchdog:/var/log/watchdog + # ── Hardening ──────────────────────────────────────────────────────────── + user: "1000:1000" # non-root + group_add: + - "${DOCKER_GID:-999}" # host docker.sock group GID — auto-detected into .env by install.sh + read_only: true # immutable root filesystem + tmpfs: + - /tmp + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + # ── Resource limits — contain a leak/runaway to this container, not the host ─ + mem_limit: 256m + mem_reservation: 128m + memswap_limit: 256m # no swap beyond mem_limit — hits the limit and restarts instead of degrading the host + mem_swappiness: 0 # avoid swapping this container's pages — needed when host kernel lacks swap accounting (memswap_limit is then unenforceable) + cpus: "0.50" + pids_limit: 200 healthcheck: test: ["CMD", "python3", "-c", "import docker; docker.from_env().ping()"] interval: 30s diff --git a/docker-compose.yaml b/docker-compose.yaml index 25284ea..e5443e6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -32,6 +32,24 @@ services: - ./watchdog.py:/app/watchdog.py:ro # Persistent log storage — written directly to host folder - ./watchdog:/var/log/watchdog + # ── Hardening ──────────────────────────────────────────────────────────── + user: "1000:1000" # non-root + group_add: + - "${DOCKER_GID:-999}" # host docker.sock group GID — auto-detected into .env by install.sh + read_only: true # immutable root filesystem + tmpfs: + - /tmp + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + # ── Resource limits — contain a leak/runaway to this container, not the host ─ + mem_limit: 256m + mem_reservation: 128m + memswap_limit: 256m # no swap beyond mem_limit — hits the limit and restarts instead of degrading the host + mem_swappiness: 0 # avoid swapping this container's pages — needed when host kernel lacks swap accounting (memswap_limit is then unenforceable) + cpus: "0.50" + pids_limit: 200 healthcheck: test: ["CMD", "python3", "-c", "import docker; docker.from_env().ping()"] interval: 30s diff --git a/install.sh b/install.sh index 4200d3e..d7884b8 100644 --- a/install.sh +++ b/install.sh @@ -118,9 +118,30 @@ setup_log_directory() { else print_info "Log directory already exists: ${LOG_DIR}" fi + # Container runs as non-root (UID 1000) — own it directly instead of opening it to all local users + if chown 1000:1000 "$LOG_DIR" 2>/dev/null; then + chmod 750 "$LOG_DIR" + else + print_warning "Could not chown ${LOG_DIR} to UID 1000 (not running as root?) — falling back to group-writable permissions" + chmod 775 "$LOG_DIR" + fi print_info "Logs will be written to: ${LOG_DIR}/watchdog.log" } +################################################################################ +# Docker Socket Group Detection +################################################################################ + +detect_docker_gid() { + # GID that owns /var/run/docker.sock — the non-root container joins this + # group (via group_add in docker-compose.yaml) to read the socket. + if [ -S /var/run/docker.sock ]; then + stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock 2>/dev/null || echo "999" + else + echo "999" + fi +} + ################################################################################ # Docker Image ################################################################################ @@ -334,6 +355,11 @@ configure_all() { WATCHDOG_HOST=$(_prompt "Hostname to display in alerts" "$HOST_DEFAULT") echo "" + # ── Docker socket GID (container runs non-root; needs group access to the socket) ── + local DOCKER_GID + DOCKER_GID=$(detect_docker_gid) + print_info "Docker socket GID detected: ${DOCKER_GID}" + # ── Write .env ──────────────────────────────────────────────────────────── { printf "# .env — generated by install.sh on %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" @@ -349,6 +375,7 @@ configure_all() { "$SNMP_V3_AUTH_KEY" "$SNMP_V3_PRIV_KEY" fi printf "# Alert identity\nWATCHDOG_HOST=%s\n\n" "$WATCHDOG_HOST" + printf "# Docker socket access (non-root container)\nDOCKER_GID=%s\n\n" "$DOCKER_GID" printf "# Tuning\nLOG_LEVEL=INFO\n" } > "$ENV_FILE" chmod 600 "$ENV_FILE" From 5b224a29a1e3a0311b6715160844646875eb5d96 Mon Sep 17 00:00:00 2001 From: rdwr-rahulk Date: Thu, 17 Sep 2026 13:51:12 +0530 Subject: [PATCH 2/3] Updated v 1.5.3 --- .env.example | 4 +++- DEPLOYMENT.md | 4 +++- README.md | 4 ++-- docker-compose.build.yaml | 5 ++++- docker-compose.yaml | 5 ++++- install.sh | 41 ++++++++++++++++++++++++++++++--------- 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index 900f0b9..f0c363e 100644 --- a/.env.example +++ b/.env.example @@ -42,7 +42,9 @@ SMTP_PASSWORD=your-smtp-password/api key value # runs as a non-root user and joins this group (via group_add in # docker-compose.yaml) to read the socket. install.sh auto-detects this; for # manual setups find it with: stat -c '%g' /var/run/docker.sock -DOCKER_GID=999 +# Replace the placeholder below with that number — docker compose refuses to +# start the container while DOCKER_GID is unset or left as this placeholder. +DOCKER_GID=REPLACE_WITH_DOCKER_SOCKET_GID # ── Tuning ──────────────────────────────────────────────────────────────────── LOG_LEVEL=INFO diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 50613b9..b5f87c7 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -119,7 +119,9 @@ SMTP_PASSWORD=your-smtp-password/api key value # Docker socket access (non-root container) — GID of the group that owns # /var/run/docker.sock on this host. Find it with: stat -c '%g' /var/run/docker.sock -DOCKER_GID=999 +# Replace the placeholder below with that number — docker compose refuses to +# start the container while DOCKER_GID is unset or left as this placeholder. +DOCKER_GID=REPLACE_WITH_DOCKER_SOCKET_GID # Tuning (optional — defaults shown) LOG_LEVEL=INFO diff --git a/README.md b/README.md index b22f35c..dc148a8 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,11 @@ Two additional deduplication rules suppress redundant alerts at the event level: | Running container (memory) | ~50–80 MB baseline; capped at `mem_limit: 256m` in `docker-compose.yaml` | | `watchdog.tar` export | ~170 MB | -> **Note:** The values above are approximate and may vary depending on the host operating system, Docker version, and installed dependencies. If usage approaches the 256 MB limit (check with `docker stats docker-container-watchdog`), raise `mem_limit`/`mem_reservation` in `docker-compose.yaml` rather than removing the limit. +> **Note:** The values above are approximate and may vary depending on the host operating system, Docker version, and installed dependencies. If usage approaches the 256 MB limit (check with `docker stats docker-container-watchdog`), raise `mem_limit`/`mem_reservation` in `docker-compose.yaml` rather than removing the limit — and raise `memswap_limit` to at least the new `mem_limit` in **both** `docker-compose.yaml` and `docker-compose.build.yaml`, since Docker rejects a config where `memswap_limit` is lower than `mem_limit`. ### Container Hardening -The container runs as a non-root user (UID 1000) with a read-only root filesystem, no extra Linux capabilities (`cap_drop: ALL`), and `no-new-privileges` set. Access to `/var/run/docker.sock` is granted by adding the container's user to the host's `docker` group via `group_add` — the GID is auto-detected by `install.sh` and stored as `DOCKER_GID` in `.env` (see [DEPLOYMENT.md](DEPLOYMENT.md#22-configure-environment-variables-env)). CPU and process count are also capped (`cpus: "0.50"`, `pids_limit: 200`) so a leak or runaway condition is contained to this container instead of the host. +The container runs as a non-root user (UID 1000) with a read-only root filesystem, no extra Linux capabilities (`cap_drop: ALL`), and `no-new-privileges` set. These controls, together with the CPU/process caps (`cpus: "0.50"`, `pids_limit: 200`), contain a leak or runaway condition in the watchdog process to this container instead of the host — but they do **not** sandbox the Docker API. Access to `/var/run/docker.sock` is granted by adding the container's user to the host's `docker` group via `group_add` (GID auto-detected by `install.sh`, stored as `DOCKER_GID` in `.env` — see [DEPLOYMENT.md](DEPLOYMENT.md#22-configure-environment-variables-env)), and that socket is effectively host-root-equivalent: anything with access to it can create privileged containers or bind-mount the host filesystem. A compromise of the watchdog process itself is therefore **not** contained by `cap_drop`, `no-new-privileges`, or the read-only filesystem — treat `docker.sock` access as the primary trust boundary when deciding who/what can reach this host. ### Python Dependencies diff --git a/docker-compose.build.yaml b/docker-compose.build.yaml index 505b458..be7fd2b 100644 --- a/docker-compose.build.yaml +++ b/docker-compose.build.yaml @@ -29,7 +29,9 @@ services: # ── Hardening ──────────────────────────────────────────────────────────── user: "1000:1000" # non-root group_add: - - "${DOCKER_GID:-999}" # host docker.sock group GID — auto-detected into .env by install.sh + # host docker.sock group GID — auto-detected into .env by install.sh. + # Required (no fallback): a wrong/guessed GID silently breaks socket access. + - "${DOCKER_GID:?Set DOCKER_GID in .env to the docker.sock GID - run install.sh or see DEPLOYMENT.md}" read_only: true # immutable root filesystem tmpfs: - /tmp @@ -41,6 +43,7 @@ services: mem_limit: 256m mem_reservation: 128m memswap_limit: 256m # no swap beyond mem_limit — hits the limit and restarts instead of degrading the host + # must stay >= mem_limit if you raise it, or Docker rejects this config mem_swappiness: 0 # avoid swapping this container's pages — needed when host kernel lacks swap accounting (memswap_limit is then unenforceable) cpus: "0.50" pids_limit: 200 diff --git a/docker-compose.yaml b/docker-compose.yaml index e5443e6..8033eca 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -35,7 +35,9 @@ services: # ── Hardening ──────────────────────────────────────────────────────────── user: "1000:1000" # non-root group_add: - - "${DOCKER_GID:-999}" # host docker.sock group GID — auto-detected into .env by install.sh + # host docker.sock group GID — auto-detected into .env by install.sh. + # Required (no fallback): a wrong/guessed GID silently breaks socket access. + - "${DOCKER_GID:?Set DOCKER_GID in .env to the docker.sock GID - run install.sh or see DEPLOYMENT.md}" read_only: true # immutable root filesystem tmpfs: - /tmp @@ -47,6 +49,7 @@ services: mem_limit: 256m mem_reservation: 128m memswap_limit: 256m # no swap beyond mem_limit — hits the limit and restarts instead of degrading the host + # must stay >= mem_limit if you raise it, or Docker rejects this config mem_swappiness: 0 # avoid swapping this container's pages — needed when host kernel lacks swap accounting (memswap_limit is then unenforceable) cpus: "0.50" pids_limit: 200 diff --git a/install.sh b/install.sh index d7884b8..9390c3a 100644 --- a/install.sh +++ b/install.sh @@ -118,12 +118,18 @@ setup_log_directory() { else print_info "Log directory already exists: ${LOG_DIR}" fi - # Container runs as non-root (UID 1000) — own it directly instead of opening it to all local users - if chown 1000:1000 "$LOG_DIR" 2>/dev/null; then + # Container runs as non-root (UID 1000:GID 1000) — must own the dir and any + # pre-existing files (e.g. log/rotated files left by an older root-run + # version) directly; group-writable permissions don't help unless the + # container's GID is actually a member of that group. + if chown -R 1000:1000 "$LOG_DIR" 2>/dev/null; then chmod 750 "$LOG_DIR" else - print_warning "Could not chown ${LOG_DIR} to UID 1000 (not running as root?) — falling back to group-writable permissions" - chmod 775 "$LOG_DIR" + print_error "Could not chown ${LOG_DIR} to UID 1000:GID 1000 (not running as root?)" + echo "" + echo " Re-run this script with sudo, or fix ownership manually:" + echo " sudo chown -R 1000:1000 ${LOG_DIR}" + exit 1 fi print_info "Logs will be written to: ${LOG_DIR}/watchdog.log" } @@ -135,11 +141,19 @@ setup_log_directory() { detect_docker_gid() { # GID that owns /var/run/docker.sock — the non-root container joins this # group (via group_add in docker-compose.yaml) to read the socket. - if [ -S /var/run/docker.sock ]; then - stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock 2>/dev/null || echo "999" - else - echo "999" + # No silent fallback: a wrong guessed GID breaks socket access at container + # start with a confusing error, so treat detection failure as fatal here. + if [ ! -S /var/run/docker.sock ]; then + print_error "/var/run/docker.sock not found — cannot determine its group GID" + return 1 + fi + local gid + gid=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock 2>/dev/null) + if [ -z "$gid" ]; then + print_error "Could not determine the GID that owns /var/run/docker.sock" + return 1 fi + echo "$gid" } ################################################################################ @@ -241,6 +255,15 @@ configure_all() { IFS= read -r RECONFIG if [[ ! "$RECONFIG" =~ ^[Yy]$ ]]; then print_info "Keeping existing configuration" + # Upgrade path: older installs may predate DOCKER_GID — add it now + # rather than silently starting with the unset/wrong-GID fallback. + if [ -f "$ENV_FILE" ] && ! grep -q '^DOCKER_GID=' "$ENV_FILE"; then + print_warning "Existing .env is missing DOCKER_GID — detecting and adding it" + local MIGRATED_GID + MIGRATED_GID=$(detect_docker_gid) || exit 1 + printf "\n# Docker socket access (non-root container)\nDOCKER_GID=%s\n" "$MIGRATED_GID" >> "$ENV_FILE" + print_success "Added DOCKER_GID=${MIGRATED_GID} to ${ENV_FILE}" + fi return 0 fi echo "" @@ -357,7 +380,7 @@ configure_all() { # ── Docker socket GID (container runs non-root; needs group access to the socket) ── local DOCKER_GID - DOCKER_GID=$(detect_docker_gid) + DOCKER_GID=$(detect_docker_gid) || exit 1 print_info "Docker socket GID detected: ${DOCKER_GID}" # ── Write .env ──────────────────────────────────────────────────────────── From e1f8b5d564469fef14cdfc12792164398596998a Mon Sep 17 00:00:00 2001 From: rdwr-rahulk Date: Thu, 17 Sep 2026 14:14:05 +0530 Subject: [PATCH 3/3] Fixed 1.5.3 --- DEPLOYMENT.md | 10 +++++++++ install.sh | 57 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index b5f87c7..d3a318f 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -362,6 +362,16 @@ Download [`watchdog.tar` — pre-built Docker image for offline installation](ht docker load -i watchdog.tar ``` +#### Prepare the log directory + +The container runs as UID/GID 1000 and writes to the bind-mounted `./watchdog` directory. Create it and set ownership **before** the first `docker compose up`, otherwise Compose creates it as `root` and the non-root container cannot create `watchdog.log` — `RotatingFileHandler` fails at startup and the `restart: always` container loops. (Option A's `install.sh` does this for you.) + +```bash +mkdir -p watchdog +sudo chown -R 1000:1000 watchdog +sudo chmod 750 watchdog +``` + #### Start the container ```bash diff --git a/install.sh b/install.sh index 9390c3a..d1c9f10 100644 --- a/install.sh +++ b/install.sh @@ -118,19 +118,30 @@ setup_log_directory() { else print_info "Log directory already exists: ${LOG_DIR}" fi - # Container runs as non-root (UID 1000:GID 1000) — must own the dir and any - # pre-existing files (e.g. log/rotated files left by an older root-run - # version) directly; group-writable permissions don't help unless the - # container's GID is actually a member of that group. - if chown -R 1000:1000 "$LOG_DIR" 2>/dev/null; then - chmod 750 "$LOG_DIR" - else - print_error "Could not chown ${LOG_DIR} to UID 1000:GID 1000 (not running as root?)" + # Container runs as non-root (UID 1000:GID 1000) — it must own the dir and any + # pre-existing files (logs left by an older root-run version), or + # RotatingFileHandler cannot create/append watchdog.log and the container + # restart-loops. Elevate with sudo when we are not already root. + local PRIV="" + if [ "$EUID" -ne 0 ]; then + if command -v sudo &>/dev/null; then + PRIV="sudo" + else + print_error "Not running as root and sudo is not available — cannot set ${LOG_DIR} ownership to UID 1000:GID 1000" + echo "" + echo " Re-run this script as root, or fix ownership manually before starting:" + echo " chown -R 1000:1000 ${LOG_DIR} && chmod 750 ${LOG_DIR}" + exit 1 + fi + fi + if ! $PRIV chown -R 1000:1000 "$LOG_DIR"; then + print_error "Failed to set ownership of ${LOG_DIR} to UID 1000:GID 1000" echo "" - echo " Re-run this script with sudo, or fix ownership manually:" - echo " sudo chown -R 1000:1000 ${LOG_DIR}" + echo " Fix ownership manually before starting:" + echo " sudo chown -R 1000:1000 ${LOG_DIR} && sudo chmod 750 ${LOG_DIR}" exit 1 fi + $PRIV chmod 750 "$LOG_DIR" print_info "Logs will be written to: ${LOG_DIR}/watchdog.log" } @@ -255,14 +266,24 @@ configure_all() { IFS= read -r RECONFIG if [[ ! "$RECONFIG" =~ ^[Yy]$ ]]; then print_info "Keeping existing configuration" - # Upgrade path: older installs may predate DOCKER_GID — add it now - # rather than silently starting with the unset/wrong-GID fallback. - if [ -f "$ENV_FILE" ] && ! grep -q '^DOCKER_GID=' "$ENV_FILE"; then - print_warning "Existing .env is missing DOCKER_GID — detecting and adding it" - local MIGRATED_GID - MIGRATED_GID=$(detect_docker_gid) || exit 1 - printf "\n# Docker socket access (non-root container)\nDOCKER_GID=%s\n" "$MIGRATED_GID" >> "$ENV_FILE" - print_success "Added DOCKER_GID=${MIGRATED_GID} to ${ENV_FILE}" + # Upgrade path: older installs predate DOCKER_GID, and `cp .env.example .env` + # leaves a non-numeric placeholder. Treat anything that isn't a bare number + # as "not configured" and (re)detect it, or group_add gets a bad value and + # the container fails to start. + if [ -f "$ENV_FILE" ]; then + local CURRENT_GID + CURRENT_GID=$(grep -E '^DOCKER_GID=' "$ENV_FILE" | tail -n1 | cut -d= -f2 | tr -d '[:space:]') + if ! [[ "$CURRENT_GID" =~ ^[0-9]+$ ]]; then + print_warning "Existing .env has a missing or invalid DOCKER_GID ('${CURRENT_GID}') — detecting and setting it" + local MIGRATED_GID + MIGRATED_GID=$(detect_docker_gid) || exit 1 + if grep -qE '^DOCKER_GID=' "$ENV_FILE"; then + sed -i.bak -E "s|^DOCKER_GID=.*|DOCKER_GID=${MIGRATED_GID}|" "$ENV_FILE" && rm -f "${ENV_FILE}.bak" + else + printf "\n# Docker socket access (non-root container)\nDOCKER_GID=%s\n" "$MIGRATED_GID" >> "$ENV_FILE" + fi + print_success "Set DOCKER_GID=${MIGRATED_GID} in ${ENV_FILE}" + fi fi return 0 fi