Deploy: drop cpus: limits and stop assuming docker compose exists
Two NAS realities, both hit on a real Synology box. - "NanoCPUs" error part-way through the project build: `cpus:` maps to the NanoCPUs field, which DSM's Docker daemon rejects. Removed from all three services (and from .env.prod.example). mem_limit uses the much older Memory field and works, so memory caps remain the throttle. Noted in the header next to the other DSM constraints. - `docker compose build` on DSM: "'compose' is not a docker command". DSM and QTS ship the standalone `docker-compose` binary with no plugin subcommand, so every one of the script's 21 compose invocations was wrong for the target platform. The script now detects which is present, and only passes `--wait` when it finds v2 — on v1 it falls back to the health_gate it already had, which is also now run after `up` on both paths. That the build reached container creation before failing says their compose parses the rest of the file (anchors, profiles, service_completed_successfully, top-level name), so this was a daemon-level rejection, not a schema one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nux3LCu4zP9nih4BRhPyHw
This commit is contained in:
+37
-5
@@ -33,8 +33,33 @@ grn() { printf '\033[32m%s\033[0m\n' "$*"; }
|
|||||||
ylw() { printf '\033[33m%s\033[0m\n' "$*"; }
|
ylw() { printf '\033[33m%s\033[0m\n' "$*"; }
|
||||||
die() { red "error: $*" >&2; exit 1; }
|
die() { red "error: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Which compose is on this box. Synology DSM and QNAP QTS ship the standalone
|
||||||
|
# `docker-compose` binary and have no `docker compose` subcommand, so detect
|
||||||
|
# rather than assume. COMPOSE_WAIT is empty on v1, which has no `--wait`; the
|
||||||
|
# script's own health_gate covers that case.
|
||||||
|
COMPOSE_BIN=()
|
||||||
|
COMPOSE_WAIT=""
|
||||||
|
detect_compose() {
|
||||||
|
[ "${#COMPOSE_BIN[@]}" -gt 0 ] && return 0
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_BIN=(docker compose)
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_BIN=(docker-compose)
|
||||||
|
else
|
||||||
|
die "no compose found: neither 'docker compose' nor 'docker-compose' is available"
|
||||||
|
fi
|
||||||
|
local ver
|
||||||
|
ver="$("${COMPOSE_BIN[@]}" version 2>/dev/null | head -1)"
|
||||||
|
case "$ver" in
|
||||||
|
*v2.*|*" 2."*) COMPOSE_WAIT="--wait" ;;
|
||||||
|
*) COMPOSE_WAIT=""
|
||||||
|
ylw "compose v1 detected (${ver:-unknown}) — using the script's own health checks" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
compose() {
|
compose() {
|
||||||
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
|
detect_compose
|
||||||
|
"${COMPOSE_BIN[@]}" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read KEY=VALUE pairs the way compose's .env parser does, WITHOUT sourcing.
|
# Read KEY=VALUE pairs the way compose's .env parser does, WITHOUT sourcing.
|
||||||
@@ -301,7 +326,10 @@ cmd_up() {
|
|||||||
# the `init` service fixes ownership. This line must not be fatal, because
|
# the `init` service fixes ownership. This line must not be fatal, because
|
||||||
# DATA_ROOT is typically root-owned while you may be running as yourself.
|
# DATA_ROOT is typically root-owned while you may be running as yourself.
|
||||||
mkdir -p "${DATA_ROOT}"/{postgres,blobs,redis,backups,meili,state} 2>/dev/null || true
|
mkdir -p "${DATA_ROOT}"/{postgres,blobs,redis,backups,meili,state} 2>/dev/null || true
|
||||||
compose "${profile[@]}" up -d "${build[@]}" --wait
|
detect_compose
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
compose "${profile[@]}" up -d "${build[@]}" $COMPOSE_WAIT
|
||||||
|
health_gate || die "services did not come up healthy — check: ./deploy/webmail.sh logs"
|
||||||
record_version "${IMAGE_TAG:-local}"
|
record_version "${IMAGE_TAG:-local}"
|
||||||
grn "up. open http://<this-host>:${WEB_PORT:-8088}/"
|
grn "up. open http://<this-host>:${WEB_PORT:-8088}/"
|
||||||
}
|
}
|
||||||
@@ -395,7 +423,8 @@ cmd_update() {
|
|||||||
grn "updated to ${target}"
|
grn "updated to ${target}"
|
||||||
else
|
else
|
||||||
red "health gate failed — rolling back to ${previous}"
|
red "health gate failed — rolling back to ${previous}"
|
||||||
IMAGE_TAG="$previous" compose up -d --wait || true
|
# shellcheck disable=SC2086
|
||||||
|
IMAGE_TAG="$previous" compose up -d $COMPOSE_WAIT || true
|
||||||
die "update to ${target} failed; production is back on ${previous}"
|
die "update to ${target} failed; production is back on ${previous}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -405,7 +434,8 @@ cmd_rollback() {
|
|||||||
local target="${1:-$(cat "$(state_dir)/previous" 2>/dev/null || echo "")}"
|
local target="${1:-$(cat "$(state_dir)/previous" 2>/dev/null || echo "")}"
|
||||||
[ -n "$target" ] || die "no previous version recorded; pass a tag: ./deploy/webmail.sh rollback v1.0.0"
|
[ -n "$target" ] || die "no previous version recorded; pass a tag: ./deploy/webmail.sh rollback v1.0.0"
|
||||||
ylw "rolling back to ${target}"
|
ylw "rolling back to ${target}"
|
||||||
IMAGE_TAG="$target" compose up -d --wait
|
# shellcheck disable=SC2086
|
||||||
|
IMAGE_TAG="$target" compose up -d $COMPOSE_WAIT
|
||||||
if health_gate; then
|
if health_gate; then
|
||||||
record_version "$target"
|
record_version "$target"
|
||||||
set_env_value IMAGE_TAG "$target"
|
set_env_value IMAGE_TAG "$target"
|
||||||
@@ -459,7 +489,9 @@ cmd_restore() {
|
|||||||
|| die "restore failed; the database was left as it was"
|
|| die "restore failed; the database was left as it was"
|
||||||
|
|
||||||
echo "starting application containers"
|
echo "starting application containers"
|
||||||
compose up -d --wait
|
# shellcheck disable=SC2086
|
||||||
|
compose up -d $COMPOSE_WAIT
|
||||||
|
health_gate || ylw "services are still starting; check: ./deploy/webmail.sh status"
|
||||||
grn "restored. If the blob store was lost too, mail bodies will refill on next sync."
|
grn "restored. If the blob store was lost too, mail bodies will refill on next sync."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -21,6 +21,10 @@
|
|||||||
# Start: ./deploy/webmail.sh up
|
# Start: ./deploy/webmail.sh up
|
||||||
# Manual: docker compose up -d (reads .env from this directory)
|
# Manual: docker compose up -d (reads .env from this directory)
|
||||||
#
|
#
|
||||||
|
# Likewise, no `cpus:` limits: that maps to NanoCPUs, which DSM's Docker
|
||||||
|
# daemon rejects ("NanoCPUs can not be set"). mem_limit uses the much older
|
||||||
|
# Memory field and works fine, so memory caps are the throttle here.
|
||||||
|
#
|
||||||
# Interpolation here must stay on the oldest, dumbest forms: ${VAR} and
|
# Interpolation here must stay on the oldest, dumbest forms: ${VAR} and
|
||||||
# ${VAR:-default}. DSM's bundled Compose rejects `${VAR:?message}` with
|
# ${VAR:-default}. DSM's bundled Compose rejects `${VAR:?message}` with
|
||||||
# "invalid interpolation format", and rejects `include:` outright. Required
|
# "invalid interpolation format", and rejects `include:` outright. Required
|
||||||
@@ -224,7 +228,6 @@ services:
|
|||||||
migrate: { condition: service_completed_successfully }
|
migrate: { condition: service_completed_successfully }
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
mem_limit: ${API_MEM_LIMIT:-512m}
|
mem_limit: ${API_MEM_LIMIT:-512m}
|
||||||
cpus: ${API_CPUS:-1.0}
|
|
||||||
logging: *logging
|
logging: *logging
|
||||||
|
|
||||||
worker-sync:
|
worker-sync:
|
||||||
@@ -243,7 +246,6 @@ services:
|
|||||||
migrate: { condition: service_completed_successfully }
|
migrate: { condition: service_completed_successfully }
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
mem_limit: ${WORKER_SYNC_MEM_LIMIT:-512m}
|
mem_limit: ${WORKER_SYNC_MEM_LIMIT:-512m}
|
||||||
cpus: ${WORKER_CPUS:-1.5}
|
|
||||||
# Leases are released on SIGTERM (workers/run.py targets <10 s); give it room.
|
# Leases are released on SIGTERM (workers/run.py targets <10 s); give it room.
|
||||||
stop_grace_period: 30s
|
stop_grace_period: 30s
|
||||||
logging: *logging
|
logging: *logging
|
||||||
@@ -262,7 +264,6 @@ services:
|
|||||||
migrate: { condition: service_completed_successfully }
|
migrate: { condition: service_completed_successfully }
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
mem_limit: ${WORKER_JOBS_MEM_LIMIT:-640m}
|
mem_limit: ${WORKER_JOBS_MEM_LIMIT:-640m}
|
||||||
cpus: ${WORKER_CPUS:-1.5}
|
|
||||||
stop_grace_period: 30s
|
stop_grace_period: 30s
|
||||||
logging: *logging
|
logging: *logging
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ STREAM_PARSE_MB=8
|
|||||||
PER_HOST_CONN_CAP=10
|
PER_HOST_CONN_CAP=10
|
||||||
HOT_BUDGET_PER_WORKER=50
|
HOT_BUDGET_PER_WORKER=50
|
||||||
|
|
||||||
|
# Container caps. Memory only: CPU limits (`cpus:`) are deliberately absent
|
||||||
|
# because Synology's Docker daemon rejects them (NanoCPUs).
|
||||||
# Container caps. Total without Meilisearch: ~2.2 GB of *limits*; real steady
|
# Container caps. Total without Meilisearch: ~2.2 GB of *limits*; real steady
|
||||||
# usage for one or two mail accounts is around 700 MB-1 GB.
|
# usage for one or two mail accounts is around 700 MB-1 GB.
|
||||||
PG_MEM_LIMIT=768m
|
PG_MEM_LIMIT=768m
|
||||||
@@ -102,10 +104,8 @@ PG_EFFECTIVE_CACHE_SIZE=768MB
|
|||||||
REDIS_MEM_LIMIT=160m
|
REDIS_MEM_LIMIT=160m
|
||||||
REDIS_MAXMEMORY=96mb
|
REDIS_MAXMEMORY=96mb
|
||||||
API_MEM_LIMIT=512m
|
API_MEM_LIMIT=512m
|
||||||
API_CPUS=1.0
|
|
||||||
WORKER_SYNC_MEM_LIMIT=512m
|
WORKER_SYNC_MEM_LIMIT=512m
|
||||||
WORKER_JOBS_MEM_LIMIT=640m
|
WORKER_JOBS_MEM_LIMIT=640m
|
||||||
WORKER_CPUS=1.5
|
|
||||||
WEB_MEM_LIMIT=96m
|
WEB_MEM_LIMIT=96m
|
||||||
BACKUP_MEM_LIMIT=256m
|
BACKUP_MEM_LIMIT=256m
|
||||||
MEILI_MEM_LIMIT=1g
|
MEILI_MEM_LIMIT=1g
|
||||||
|
|||||||
Reference in New Issue
Block a user