From 770d89b430fd75548f5f15d4b6e4d7cce42d4c28 Mon Sep 17 00:00:00 2001 From: Ho1yShif Date: Tue, 14 Jul 2026 09:00:15 -0700 Subject: [PATCH] feat: add one-click Deploy to Render support Adds a Render Blueprint (render.yaml) and supporting pieces so nanobot can be deployed to Render in one click, with persistent memory across deploys. - render.yaml: web service + 1GB persistent disk mounted at /home/nanobot/.nanobot. Prompts for ANTHROPIC_API_KEY and NANOBOT_WEB_TOKEN at deploy time (sync: false). - render-config.json: committed gateway config that wires secrets via ${VAR} placeholders (resolved at runtime). Nothing secret is committed. - entrypoint.sh: adds a branch gated on RENDER=true that copies the config onto the mounted disk, chowns the root-owned mount, and drops to the non-root nanobot user via setpriv. Local (non-Render) path is unchanged. - Dockerfile: COPY render-config.json; USER nanobot -> USER root so the entrypoint can chown the freshly-mounted disk before dropping privileges; add PYTHONUNBUFFERED/PYTHONFAULTHANDLER for diagnosable crash output. - README.md: Deploy to Render button + section. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 14 +++++++++++++- README.md | 7 +++++++ entrypoint.sh | 23 +++++++++++++++++++++++ render-config.json | 33 +++++++++++++++++++++++++++++++++ render.yaml | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 render-config.json create mode 100644 render.yaml diff --git a/Dockerfile b/Dockerfile index 6fa9feb9..c0690f9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,12 @@ COPY nanobot/ nanobot/ COPY --from=webui-builder /app/nanobot/web/dist/ nanobot/web/dist/ RUN NANOBOT_SKIP_WEBUI_BUILD=1 uv pip install --system --no-cache ".[$NANOBOT_EXTRAS]" +# Render deploy template (see render.yaml): committed gateway config that wires +# secrets through ${ANTHROPIC_API_KEY} / ${NANOBOT_WEB_TOKEN} env vars (resolved +# at startup). Lives in the code dir (/app), not the data dir, so a mounted disk +# won't shadow it. Only used when RENDER=true; ignored by local runs. +COPY render-config.json ./ + # Create non-root user and config directory RUN useradd -m -u 1000 -s /bin/bash nanobot && \ mkdir -p /home/nanobot/.nanobot && \ @@ -36,8 +42,14 @@ RUN useradd -m -u 1000 -s /bin/bash nanobot && \ COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN sed -i 's/\r$//' /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh -USER nanobot +# Start as root so the Render entrypoint can chown the freshly-mounted +# (root-owned) persistent disk, then drop to the non-root nanobot user via +# setpriv (see entrypoint.sh). Local runs without a disk are unaffected. +USER root ENV HOME=/home/nanobot +# Ensure crash output reaches Render logs (app output is otherwise swallowed on +# non-graceful exit). +ENV PYTHONUNBUFFERED=1 PYTHONFAULTHANDLER=1 # Gateway health endpoint and optional WebUI/WebSocket channel ports EXPOSE 18790 8765 diff --git a/README.md b/README.md index 8308925a..0c2b94db 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,13 @@ | Connect Telegram, Discord, WeChat, Slack, Email, Mattermost, or another chat app | [Chat Apps](./docs/chat-apps.md) | | Configure providers, fallback models, Langfuse, MCP, web tools, or security | [Docs](./docs/README.md) and [Configuration](./docs/configuration.md) | | Understand or extend the internals | [Architecture](./docs/architecture.md) and [Development](./docs/development.md) | +| Deploy to the cloud in one click | [Deploy to Render](#deploy-to-render) | + +## Deploy to Render + +Deploy nanobot's gateway and bundled WebUI as a single web service with persistent memory. Render reads [`render.yaml`](./render.yaml) and prompts for two secrets on deploy: `ANTHROPIC_API_KEY` and `NANOBOT_WEB_TOKEN` (the password that gates the public WebUI — generate a strong random value, e.g. `openssl rand -hex 32`). + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/HKUDS/nanobot) ## What can nanobot do? diff --git a/entrypoint.sh b/entrypoint.sh index ab780dc9..2de8ce73 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,5 +1,28 @@ #!/bin/sh dir="$HOME/.nanobot" + +# Render deploy path (see render.yaml + render-config.json). Gated on Render's +# automatic RENDER=true env var so local Docker/podman usage is unaffected. +# Copies the committed config template onto the mounted disk (wiring secrets via +# ${VAR} env vars and keeping runtime data on the persistent disk), chowns the +# root-owned mount, then drops to the non-root nanobot user. Logs each decision +# so a failed start is diagnosable in Render's logs. +if [ "$RENDER" = "true" ]; then + echo "[entrypoint] Render deploy — starting as $(id)" + mkdir -p "$dir" || echo "[entrypoint] warning: mkdir $dir failed" + config="$dir/config.json" + cp /app/render-config.json "$config" || echo "[entrypoint] warning: cp config failed" + if [ "$(id -u)" = "0" ]; then + chown -R nanobot:nanobot "$dir" || echo "[entrypoint] warning: chown $dir failed" + if setpriv --reuid=nanobot --regid=nanobot --init-groups true 2>/dev/null; then + echo "[entrypoint] dropping privileges to nanobot via setpriv" + exec setpriv --reuid=nanobot --regid=nanobot --init-groups nanobot "$@" --config "$config" + fi + echo "[entrypoint] setpriv privilege-drop not permitted — running as root" + fi + exec nanobot "$@" --config "$config" +fi + if [ -d "$dir" ] && [ ! -w "$dir" ]; then owner_uid=$(stat -c %u "$dir" 2>/dev/null || stat -f %u "$dir" 2>/dev/null) cat >&2 <