fix/node-modules-named-volumes (#94)

Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/94
This commit is contained in:
johba 2025-11-13 18:17:56 +01:00
parent 19bac420d0
commit 1c6f118f6b
9 changed files with 182 additions and 87 deletions

60
.dockerignore Normal file
View file

@ -0,0 +1,60 @@
# Node.js dependencies (should be in named volumes, not copied to build context)
**/node_modules/
node_modules/
# Build outputs
**/dist/
**/build/
**/.next/
**/.nuxt/
# Caches
**/.cache/
**/.vite/
**/.ponder/
**/.turbo/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment files (should be generated in containers)
**/.env.local
**/.env.*.local
# Testing
**/coverage/
**/.nyc_output/
# Git
.git/
.gitignore
.gitattributes
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Documentation
*.md
!README.md
# Temporary files
tmp/
temp/
*.tmp

View file

@ -50,7 +50,17 @@
- **Container Orchestration**: `docker-compose.yml` has NO `depends_on` declarations. All service ordering is handled in `scripts/dev.sh` via phased startup with explicit health checks. - **Container Orchestration**: `docker-compose.yml` has NO `depends_on` declarations. All service ordering is handled in `scripts/dev.sh` via phased startup with explicit health checks.
- **Startup Phases**: (1) Create all containers, (2) Start anvil+postgres and wait for healthy, (3) Start bootstrap and wait for completion, (4) Start ponder and wait for healthy, (5) Start webapp/landing/txn-bot, (6) Start caddy. - **Startup Phases**: (1) Create all containers, (2) Start anvil+postgres and wait for healthy, (3) Start bootstrap and wait for completion, (4) Start ponder and wait for healthy, (5) Start webapp/landing/txn-bot, (6) Start caddy.
- **Logging Configuration**: All services have log rotation configured (max 10MB per file, 3 files max = 30MB per container) to prevent disk bloat. Logs are automatically rotated by Docker. - **Logging Configuration**: All services have log rotation configured (max 10MB per file, 3 files max = 30MB per container) to prevent disk bloat. Logs are automatically rotated by Docker.
- **Disk Management**: `./scripts/dev.sh stop` automatically prunes unused Docker resources. For aggressive cleanup, run `./scripts/cleanup-disk.sh`. - **Disk Management** (Portable, No Per-Machine Setup Required):
- **20GB Hard Limit**: The stack enforces a 20GB total Docker disk usage limit (images + containers + volumes + build cache).
- **Pre-flight Checks**: `./scripts/dev.sh start` checks Docker disk usage before starting and refuses to start if over 20GB.
- **Aggressive Auto-Cleanup on Stop**: `./scripts/dev.sh stop` automatically prunes ALL unused Docker resources including build cache (the primary cause of bloat).
- **Named Volumes for node_modules**: All Node.js services (ponder, webapp, landing, txnBot) use named Docker volumes for `node_modules/` instead of writing to the host filesystem. This prevents host pollution (20-30GB savings) and ensures `docker system prune --volumes` cleans them up.
- **npm Best Practices**: All entrypoints use `npm ci` (not `npm install`) for reproducible builds and `npm cache clean --force` to remove ~50-100MB of cache per service.
- **PostgreSQL WAL Limits**: Postgres configured with `wal_level=minimal`, `max_wal_size=128MB`, and `archive_mode=off` to prevent unbounded WAL file growth in the postgres volume.
- **Log Rotation**: All containers limited to 30MB logs (10MB × 3 files) via docker-compose logging configuration.
- **.dockerignore**: Excludes `node_modules/`, caches, and build outputs from Docker build context to speed up builds and reduce image size.
- **Monitoring**: The stack displays current Docker disk usage on startup and warns at 80% (16GB).
- **Note**: Docker has no built-in portable disk quotas. All limits are enforced via aggressive pruning, bounded configurations, and isolation of dependencies to Docker volumes.
## Guardrails & Tips ## Guardrails & Tips
- `token0isWeth` flips amount semantics; confirm ordering before seeding or interpreting liquidity. - `token0isWeth` flips amount semantics; confirm ordering before seeding or interpreting liquidity.

View file

@ -31,16 +31,16 @@ if [[ ! -f "$REQUIRED_DIST" ]]; then
fi fi
cd "$LANDING_DIR" cd "$LANDING_DIR"
DEPS_MARKER="/tmp/.landing-deps-installed"
if [[ ! -d node_modules || ! -f "$DEPS_MARKER" ]]; then # Check if node_modules is populated (named volume may be empty on first run)
if [[ ! -d node_modules/.bin ]]; then
echo "[landing-entrypoint] Installing dependencies..." echo "[landing-entrypoint] Installing dependencies..."
npm install --no-save --loglevel error 2>&1 || { npm ci --loglevel error && npm cache clean --force 2>&1 || {
echo "[landing-entrypoint] npm install failed, trying with --force" echo "[landing-entrypoint] npm ci failed, trying npm install"
npm install --force --no-save --loglevel error npm install --no-save --loglevel error && npm cache clean --force
} }
touch "$DEPS_MARKER" || true
else else
echo "[landing-entrypoint] Using cached node_modules" echo "[landing-entrypoint] Using cached node_modules from volume"
fi fi
export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1} export CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING:-1}

View file

@ -65,16 +65,15 @@ if [[ ! -f "$REQUIRED_DIST" ]]; then
exit 1 exit 1
fi fi
DEPS_MARKER="/tmp/.ponder-deps-installed" # Check if node_modules is populated (named volume may be empty on first run)
if [[ ! -d node_modules || ! -f "$DEPS_MARKER" ]]; then if [[ ! -d node_modules/.bin ]]; then
echo "[ponder-entrypoint] Installing dependencies..." echo "[ponder-entrypoint] Installing dependencies..."
npm install --no-save --loglevel error 2>&1 || { npm ci --loglevel error && npm cache clean --force 2>&1 || {
echo "[ponder-entrypoint] npm install failed, trying with --force" echo "[ponder-entrypoint] npm ci failed, trying npm install"
npm install --force --no-save --loglevel error npm install --no-save --loglevel error && npm cache clean --force
} }
touch "$DEPS_MARKER" || true
else else
echo "[ponder-entrypoint] Using cached node_modules" echo "[ponder-entrypoint] Using cached node_modules from volume"
fi fi
# Load and export all environment variables from .env.local # Load and export all environment variables from .env.local

View file

@ -37,16 +37,16 @@ if [[ ! -f "$REQUIRED_DIST" ]]; then
fi fi
cd "$BOT_DIR" cd "$BOT_DIR"
DEPS_MARKER="/tmp/.txnbot-deps-installed"
if [[ ! -d node_modules || ! -f "$DEPS_MARKER" ]]; then # Check if node_modules is populated (named volume may be empty on first run)
if [[ ! -d node_modules/.bin ]]; then
echo "[txn-bot-entrypoint] Installing txn-bot dependencies..." echo "[txn-bot-entrypoint] Installing txn-bot dependencies..."
npm install --no-save --loglevel error 2>&1 || { npm ci --loglevel error && npm cache clean --force 2>&1 || {
echo "[txn-bot-entrypoint] npm install failed, trying with --force" echo "[txn-bot-entrypoint] npm ci failed, trying npm install"
npm install --force --no-save --loglevel error npm install --no-save --loglevel error && npm cache clean --force
} }
touch "$DEPS_MARKER" || true
else else
echo "[txn-bot-entrypoint] Using cached node_modules" echo "[txn-bot-entrypoint] Using cached node_modules from volume"
fi fi
echo "[txn-bot-entrypoint] Building TypeScript..." echo "[txn-bot-entrypoint] Building TypeScript..."

View file

@ -41,16 +41,16 @@ fi
source "$CONTRACT_ENV" source "$CONTRACT_ENV"
cd "$APP_DIR" cd "$APP_DIR"
DEPS_MARKER="/tmp/.webapp-deps-installed"
if [[ ! -d node_modules || ! -f "$DEPS_MARKER" ]]; then # Check if node_modules is populated (named volume may be empty on first run)
if [[ ! -d node_modules/.bin ]]; then
echo "[frontend-entrypoint] Installing dependencies..." echo "[frontend-entrypoint] Installing dependencies..."
npm install --no-save --loglevel error 2>&1 || { npm ci --loglevel error && npm cache clean --force 2>&1 || {
echo "[frontend-entrypoint] npm install failed, trying with --force" echo "[frontend-entrypoint] npm ci failed, trying npm install"
npm install --force --no-save --loglevel error npm install --no-save --loglevel error && npm cache clean --force
} }
touch "$DEPS_MARKER" || true
else else
echo "[frontend-entrypoint] Using cached node_modules" echo "[frontend-entrypoint] Using cached node_modules from volume"
fi fi
export VITE_DEFAULT_CHAIN_ID=${VITE_DEFAULT_CHAIN_ID:-31337} export VITE_DEFAULT_CHAIN_ID=${VITE_DEFAULT_CHAIN_ID:-31337}

View file

@ -34,6 +34,18 @@ services:
postgres: postgres:
image: docker.io/library/postgres:16-alpine image: docker.io/library/postgres:16-alpine
command:
- "postgres"
- "-c"
- "wal_level=minimal"
- "-c"
- "max_wal_size=128MB"
- "-c"
- "max_wal_senders=0"
- "-c"
- "archive_mode=off"
- "-c"
- "checkpoint_timeout=30min"
environment: environment:
- POSTGRES_USER=ponder - POSTGRES_USER=ponder
- POSTGRES_PASSWORD=ponder_local - POSTGRES_PASSWORD=ponder_local
@ -82,6 +94,7 @@ services:
- .:/workspace:z - .:/workspace:z
- .git:/workspace/.git:ro,z - .git:/workspace/.git:ro,z
- ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z - ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z
- ponder_node_modules:/workspace/services/ponder/node_modules
working_dir: /workspace working_dir: /workspace
environment: environment:
- CHOKIDAR_USEPOLLING=1 - CHOKIDAR_USEPOLLING=1
@ -112,6 +125,7 @@ services:
- .:/workspace:z - .:/workspace:z
- .git:/workspace/.git:ro,z - .git:/workspace/.git:ro,z
- ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z - ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z
- webapp_node_modules:/workspace/web-app/node_modules
working_dir: /workspace working_dir: /workspace
environment: environment:
- CHOKIDAR_USEPOLLING=1 - CHOKIDAR_USEPOLLING=1
@ -140,6 +154,7 @@ services:
- .:/workspace:z - .:/workspace:z
- .git:/workspace/.git:ro,z - .git:/workspace/.git:ro,z
- ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z - ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z
- landing_node_modules:/workspace/landing/node_modules
working_dir: /workspace working_dir: /workspace
environment: environment:
- CHOKIDAR_USEPOLLING=1 - CHOKIDAR_USEPOLLING=1
@ -166,6 +181,7 @@ services:
- .:/workspace:z - .:/workspace:z
- .git:/workspace/.git:ro,z - .git:/workspace/.git:ro,z
- ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z - ./kraiken-lib/dist:/workspace/kraiken-lib/dist:ro,z
- txnbot_node_modules:/workspace/services/txnBot/node_modules
working_dir: /workspace working_dir: /workspace
environment: environment:
- GIT_BRANCH=${GIT_BRANCH:-} - GIT_BRANCH=${GIT_BRANCH:-}
@ -199,3 +215,7 @@ services:
volumes: volumes:
postgres-data: postgres-data:
ponder_node_modules:
webapp_node_modules:
landing_node_modules:
txnbot_node_modules:

View file

@ -1,55 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Disk cleanup utility for Harb stack
# Use this to aggressively free up disk space
cd "$(dirname "$0")/.."
echo "=== Harb Stack Disk Cleanup ==="
echo ""
# Detect container runtime
if command -v docker &> /dev/null; then
RUNTIME_CMD="docker"
elif command -v podman &> /dev/null; then
RUNTIME_CMD="podman"
else
echo "Error: docker/podman not found"
exit 1
fi
echo "Current disk usage:"
df -h / | tail -1
echo ""
# Stop the stack first
if [[ -f "./scripts/dev.sh" ]]; then
echo "Stopping stack..."
./scripts/dev.sh stop || true
fi
echo ""
echo "Pruning Docker resources..."
echo " - Stopped containers"
echo " - Unused volumes"
echo " - Dangling images"
echo " - Build cache"
echo ""
# Aggressive pruning
${RUNTIME_CMD} system prune -af --volumes
echo ""
echo "Cleaning npm caches..."
rm -rf ~/.npm ~/.cache 2>/dev/null || true
echo ""
echo "Cleaning journal logs..."
sudo journalctl --vacuum-size=500M 2>/dev/null || echo " (skipped: needs sudo)"
echo ""
echo "Final disk usage:"
df -h / | tail -1
echo ""
echo "[ok] Cleanup complete"

View file

@ -11,6 +11,7 @@ readonly PONDER_TIMEOUT=120 # Must index bootstrap events
readonly WEBAPP_TIMEOUT=120 # npm install + Vite startup readonly WEBAPP_TIMEOUT=120 # npm install + Vite startup
readonly CADDY_TIMEOUT=20 # Proxy starts instantly readonly CADDY_TIMEOUT=20 # Proxy starts instantly
readonly POLL_INTERVAL=2 # Check health every N seconds readonly POLL_INTERVAL=2 # Check health every N seconds
readonly MAX_DOCKER_DISK_GB=20 # Maximum Docker disk usage in GB
PID_FILE=/tmp/kraiken-watcher.pid PID_FILE=/tmp/kraiken-watcher.pid
PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$PWD")} PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$PWD")}
@ -36,6 +37,58 @@ container_name() {
echo "${PROJECT_NAME}_${service}_1" echo "${PROJECT_NAME}_${service}_1"
} }
# Check Docker disk usage and warn if approaching limits
check_docker_disk_usage() {
if ! command -v docker &> /dev/null; then
return 0 # Skip if Docker not available
fi
# Get total Docker disk usage in GB (works on Linux and macOS)
local total_size_bytes
total_size_bytes=$(docker system df --format '{{.Size}}' 2>/dev/null | \
sed 's/[^0-9.]//g' | awk '{sum+=$1} END {print sum}' || echo "0")
# Parse the actual usage more accurately
local docker_df_output
docker_df_output=$(docker system df 2>/dev/null || echo "")
if [[ -z "$docker_df_output" ]]; then
return 0 # Docker not running
fi
# Extract total reclaimable space (more accurate than parsing Size)
local total_gb
total_gb=$(echo "$docker_df_output" | tail -n 1 | awk '{print $NF}' | sed 's/GB//; s/MB/\/1024/; s/KB/\/1048576/' | bc -l 2>/dev/null || echo "0")
# Alternative: sum up all TYPE sizes (column 3 has the SIZE)
local images_size containers_size volumes_size build_cache_size
images_size=$(echo "$docker_df_output" | grep "Images" | awk '{print $3}' | sed 's/GB$//; s/MB$/\/1024/; s/KB$/\/1048576/; s/B$/\/1073741824/' | sed 's/^$/0/' | bc -l 2>/dev/null || echo "0")
containers_size=$(echo "$docker_df_output" | grep "Containers" | awk '{print $3}' | sed 's/GB$//; s/MB$/\/1024/; s/KB$/\/1048576/; s/B$/\/1073741824/' | sed 's/^$/0/' | bc -l 2>/dev/null || echo "0")
volumes_size=$(echo "$docker_df_output" | grep "Local Volumes" | awk '{print $3}' | sed 's/GB$//; s/MB$/\/1024/; s/KB$/\/1048576/; s/B$/\/1073741824/' | sed 's/^$/0/' | bc -l 2>/dev/null || echo "0")
build_cache_size=$(echo "$docker_df_output" | grep "Build Cache" | awk '{print $3}' | sed 's/GB$//; s/MB$/\/1024/; s/KB$/\/1048576/; s/B$/\/1073741824/' | sed 's/^$/0/' | bc -l 2>/dev/null || echo "0")
total_gb=$(echo "$images_size + $containers_size + $volumes_size + $build_cache_size" | bc -l 2>/dev/null || echo "0")
# Round to 1 decimal place
total_gb=$(printf "%.1f" "$total_gb" 2>/dev/null || echo "0")
echo " Docker disk usage: ${total_gb}GB / ${MAX_DOCKER_DISK_GB}GB limit"
# Warn if approaching 80% of limit (16GB)
if (( $(echo "$total_gb > 16" | bc -l 2>/dev/null || echo "0") )); then
echo " [!!] WARNING: Docker disk usage is high!"
echo " [!!] Run './scripts/cleanup-disk.sh' to free up space"
fi
# Hard stop if over limit
if (( $(echo "$total_gb > $MAX_DOCKER_DISK_GB" | bc -l 2>/dev/null || echo "0") )); then
echo ""
echo "ERROR: Docker disk usage exceeds ${MAX_DOCKER_DISK_GB}GB limit!"
echo "Run './scripts/cleanup-disk.sh' to free up space, then try again."
exit 1
fi
}
cleanup_existing() { cleanup_existing() {
# Kill any existing watch scripts # Kill any existing watch scripts
pkill -f "watch-kraiken-lib.sh" 2>/dev/null || true pkill -f "watch-kraiken-lib.sh" 2>/dev/null || true
@ -101,6 +154,9 @@ wait_for_exited() {
start_stack() { start_stack() {
local stack_start_time=$(date +%s) local stack_start_time=$(date +%s)
# Check Docker disk usage before starting
check_docker_disk_usage
# Clean up any existing processes first # Clean up any existing processes first
cleanup_existing cleanup_existing
@ -163,9 +219,14 @@ stop_stack() {
cleanup_existing cleanup_existing
${COMPOSE_CMD} down ${COMPOSE_CMD} down
# Prune Docker resources to prevent disk bloat # Aggressive pruning to prevent disk bloat
echo " Pruning Docker resources..." echo " Pruning Docker resources (images, containers, volumes, build cache)..."
${RUNTIME_CMD} system prune -f --volumes 2>&1 | grep -E "Total reclaimed|deleted" || true
# Prune build cache aggressively (this is usually the biggest culprit)
${RUNTIME_CMD} builder prune -af 2>&1 | grep -E "Total|deleted" || true
# Prune all unused data (containers, networks, images, volumes)
${RUNTIME_CMD} system prune -af --volumes 2>&1 | grep -E "Total reclaimed|deleted" || true
echo "[ok] Stack stopped and cleaned" echo "[ok] Stack stopped and cleaned"
} }