Docker: add cron-enabled gateway runtime and non-root global tool paths

This commit is contained in:
Ruslan Belkin 2026-02-27 18:06:50 -08:00
parent 2d67c9b2a0
commit 07287c7f27
5 changed files with 221 additions and 15 deletions

View File

@ -19,16 +19,32 @@ ENV PATH="/root/.bun/bin:${PATH}"
RUN corepack enable
ENV PNPM_HOME=/home/node/.local/share/pnpm
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV GOPATH=/home/node/go
ENV PATH="${PNPM_HOME}:${NPM_CONFIG_PREFIX}/bin:${GOPATH}/bin:${PATH}"
RUN mkdir -p "${PNPM_HOME}" "${NPM_CONFIG_PREFIX}/bin" "${GOPATH}/bin" && \
chown -R node:node /home/node/.local /home/node/.npm-global /home/node/go
# Install runtime helpers used by gateway entrypoint:
# - cron: enables `crontab` + cron daemon for scheduled jobs in containerized setups
# - gosu: drop privileges back to `node` after starting root-only services
# - jq: required by later build steps (Go/gogcli version discovery)
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cron gosu jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
WORKDIR /app
RUN chown node:node /app
ARG OPENCLAW_DOCKER_APT_PACKAGES=""
RUN if [ -n "$OPENCLAW_DOCKER_APT_PACKAGES" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $OPENCLAW_DOCKER_APT_PACKAGES && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $OPENCLAW_DOCKER_APT_PACKAGES && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY --chown=node:node ui/package.json ./ui/package.json
@ -47,15 +63,68 @@ RUN NODE_OPTIONS=--max-old-space-size=2048 pnpm install --frozen-lockfile
USER root
ARG OPENCLAW_INSTALL_BROWSER=""
RUN if [ -n "$OPENCLAW_INSTALL_BROWSER" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \
mkdir -p /home/node/.cache/ms-playwright && \
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright \
node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \
chown -R node:node /home/node/.cache/ms-playwright && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb && \
mkdir -p /home/node/.cache/ms-playwright && \
PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright \
node /app/node_modules/playwright-core/cli.js install --with-deps chromium && \
chown -R node:node /home/node/.cache/ms-playwright && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
fi
# ---- Install Go (official) ----
# Fetch the latest stable version from go.dev and install the correct arch.
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) GOARCH=amd64 ;; \
arm64) GOARCH=arm64 ;; \
*) echo "Unsupported arch: $arch" >&2; exit 1 ;; \
esac; \
GOVERSION="$(curl -fsSL 'https://go.dev/dl/?mode=json' | jq -r 'map(select(.stable==true)) | .[0].version')" ; \
echo "Installing Go ${GOVERSION} for linux-${GOARCH}"; \
curl -fsSL "https://go.dev/dl/${GOVERSION}.linux-${GOARCH}.tar.gz" -o /tmp/go.tgz; \
rm -rf /usr/local/go; \
tar -C /usr/local -xzf /tmp/go.tgz; \
rm -f /tmp/go.tgz; \
/usr/local/go/bin/go version
# Ensure Go is first in PATH (no old go ahead of it)
ENV PATH="/usr/local/go/bin:${PATH}"
# ---- Install gog (gogcli) ----
# Pin version by setting GOGCLI_TAG at build time, or default to latest release tag.
ARG GOGCLI_TAG=latest
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) GOGARCH=amd64 ;; \
arm64) GOGARCH=arm64 ;; \
*) echo "Unsupported arch: $arch" >&2; exit 1 ;; \
esac; \
if [ "$GOGCLI_TAG" = "latest" ]; then \
GOGCLI_TAG="$(curl -fsSL https://api.github.com/repos/steipete/gogcli/releases/latest | jq -r .tag_name)"; \
fi; \
ver="${GOGCLI_TAG#v}"; \
url="https://github.com/steipete/gogcli/releases/download/${GOGCLI_TAG}/gogcli_${ver}_linux_${GOGARCH}.tar.gz"; \
echo "Downloading: $url"; \
curl -fsSL "$url" -o /tmp/gogcli.tgz; \
tar -xzf /tmp/gogcli.tgz -C /tmp; \
install -m 0755 /tmp/gog /usr/local/bin/gog; \
rm -f /tmp/gog /tmp/gogcli.tgz; \
gog --help >/dev/null
# Install Linuxbrew in a node-writable prefix so brew installs work at runtime.
ENV HOMEBREW_PREFIX=/home/linuxbrew/.linuxbrew
ENV HOMEBREW_CELLAR=/home/linuxbrew/.linuxbrew/Cellar
ENV HOMEBREW_REPOSITORY=/home/linuxbrew/.linuxbrew/Homebrew
RUN set -eux; \
mkdir -p "${HOMEBREW_REPOSITORY}" "${HOMEBREW_CELLAR}" "${HOMEBREW_PREFIX}/bin"; \
curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip-components=1 -C "${HOMEBREW_REPOSITORY}"; \
ln -sf ../Homebrew/bin/brew "${HOMEBREW_PREFIX}/bin/brew"; \
chown -R node:node /home/linuxbrew
ENV PATH="${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin:${PATH}"
# Optionally install Docker CLI for sandbox container management.
# Build with: docker build --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1 ...
@ -90,6 +159,7 @@ RUN if [ -n "$OPENCLAW_INSTALL_DOCKER_CLI" ]; then \
fi
USER node
RUN brew --version
COPY --chown=node:node . .
# Normalize copied plugin/agent paths so plugin safety checks do not reject
# world-writable directories inherited from source file modes.
@ -99,6 +169,7 @@ RUN for dir in /app/extensions /app/.agent /app/.agents; do \
find "$dir" -type f -exec chmod 644 {} +; \
fi; \
done
RUN chmod +x scripts/docker/gateway-entrypoint.sh
RUN pnpm build
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
ENV OPENCLAW_PREFER_PNPM=1

View File

@ -1,6 +1,8 @@
services:
openclaw-gateway:
image: ${OPENCLAW_IMAGE:-openclaw:local}
user: root
entrypoint: ["/app/scripts/docker/gateway-entrypoint.sh"]
environment:
HOME: /home/node
TERM: xterm-256color
@ -10,6 +12,7 @@ services:
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-}
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
volumes:
- ./.env:/app/.env:ro
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
## Uncomment the lines below to enable sandbox isolation
@ -66,6 +69,7 @@ services:
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-}
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
volumes:
- ./.env:/app/.env:ro
- ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
- ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
stdin_open: true

View File

@ -200,6 +200,7 @@ export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}"
export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}"
export OPENCLAW_IMAGE="$IMAGE_NAME"
export OPENCLAW_DOCKER_APT_PACKAGES="${OPENCLAW_DOCKER_APT_PACKAGES:-}"
export OPENCLAW_INSTALL_BROWSER="${OPENCLAW_INSTALL_BROWSER:-}"
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
export OPENCLAW_ALLOW_INSECURE_PRIVATE_WS="${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}"
@ -382,13 +383,15 @@ upsert_env "$ENV_FILE" \
OPENCLAW_DOCKER_SOCKET \
DOCKER_GID \
OPENCLAW_INSTALL_DOCKER_CLI \
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS \
OPENCLAW_INSTALL_BROWSER
if [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
echo "==> Building Docker image: $IMAGE_NAME"
docker build \
--build-arg "OPENCLAW_DOCKER_APT_PACKAGES=${OPENCLAW_DOCKER_APT_PACKAGES}" \
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
--build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}" \
-t "$IMAGE_NAME" \
-f "$ROOT_DIR/Dockerfile" \
"$ROOT_DIR"

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
echo "gateway-entrypoint requires root (set docker compose service user: root)" >&2
exit 1
fi
if [ "${OPENCLAW_ENABLE_CRON:-1}" = "1" ] && command -v cron >/dev/null 2>&1; then
# Start cron daemon once; jobs are configured by user crontabs via `crontab`.
if ! pgrep -x cron >/dev/null 2>&1; then
cron
fi
fi
if command -v gosu >/dev/null 2>&1; then
exec gosu node "$@"
fi
exec su -s /bin/sh node -c "$*"

108
scripts/openclaw-keepawake.sh Executable file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
PID_FILE="${OPENCLAW_KEEPAWAKE_PID_FILE:-$HOME/.openclaw/run/keepawake.pid}"
usage() {
cat <<'USAGE'
Usage: scripts/openclaw-keepawake.sh <on|off|status|restart>
Keeps macOS awake using `caffeinate` for long-running OpenClaw Docker sessions.
Commands:
on Start keep-awake background process
off Stop keep-awake background process
status Show current keep-awake status
restart Restart keep-awake background process
USAGE
}
ensure_caffeinate() {
if ! command -v caffeinate >/dev/null 2>&1; then
echo "caffeinate not found (this helper is for macOS)." >&2
exit 1
fi
}
read_pid() {
if [[ -f "$PID_FILE" ]]; then
tr -d '[:space:]' <"$PID_FILE"
fi
}
is_caffeinate_pid() {
local pid="$1"
[[ -n "$pid" ]] || return 1
kill -0 "$pid" >/dev/null 2>&1 || return 1
local comm
comm="$(ps -p "$pid" -o comm= 2>/dev/null | tr -d '[:space:]')"
[[ "$comm" == "caffeinate" ]]
}
start_awake() {
ensure_caffeinate
mkdir -p "$(dirname "$PID_FILE")"
local pid
pid="$(read_pid || true)"
if is_caffeinate_pid "$pid"; then
echo "keep-awake already on (pid $pid)"
return 0
fi
caffeinate -dimsu >/dev/null 2>&1 &
pid="$!"
echo "$pid" >"$PID_FILE"
echo "keep-awake on (pid $pid)"
}
stop_awake() {
local pid
pid="$(read_pid || true)"
if [[ -z "$pid" ]]; then
echo "keep-awake already off"
return 0
fi
if is_caffeinate_pid "$pid"; then
kill "$pid" >/dev/null 2>&1 || true
echo "keep-awake off (stopped pid $pid)"
else
echo "keep-awake off (stale pid file removed)"
fi
rm -f "$PID_FILE"
}
status_awake() {
local pid
pid="$(read_pid || true)"
if is_caffeinate_pid "$pid"; then
echo "keep-awake is on (pid $pid)"
else
echo "keep-awake is off"
if [[ -n "$pid" ]]; then
rm -f "$PID_FILE"
fi
fi
}
cmd="${1:-}"
case "$cmd" in
on)
start_awake
;;
off)
stop_awake
;;
status)
status_awake
;;
restart)
stop_awake
start_awake
;;
*)
usage
exit 2
;;
esac