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) <noreply@anthropic.com>
This commit is contained in:
Ho1yShif
2026-07-18 17:39:59 +08:00
committed by Xubin Ren
co-authored by Claude Opus 4.8
parent afed32b013
commit 770d89b430
5 changed files with 118 additions and 1 deletions
+13 -1
View File
@@ -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
+7
View File
@@ -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?
+23
View File
@@ -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 <<EOF
+33
View File
@@ -0,0 +1,33 @@
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-8",
"provider": "auto"
}
},
"providers": {
"anthropic": {
"apiKey": "${ANTHROPIC_API_KEY}"
}
},
"gateway": {
"host": "127.0.0.1",
"port": 18790
},
"channels": {
"websocket": {
"enabled": true,
"host": "0.0.0.0",
"port": 8765,
"token": "${NANOBOT_WEB_TOKEN}",
"websocketRequiresToken": true
}
},
"tools": {
"restrictToWorkspace": true,
"webuiAllowRemotePackageInstall": false,
"my": {
"allowSet": false
}
}
}
+42
View File
@@ -0,0 +1,42 @@
# Render Blueprint — deploys nanobot's gateway + bundled WebUI as one web
# service. Secrets are provided at deploy time as env vars (sync: false) and
# resolved at runtime via the ${VAR} placeholders in render-config.json.
# Nothing secret is stored in this repo.
services:
- type: web
name: nanobot
runtime: docker
dockerfilePath: ./Dockerfile
dockerContext: .
# Render's Docker Command REPLACES the Dockerfile ENTRYPOINT (it is not
# appended to it), so invoke the entrypoint explicitly. When RENDER=true the
# entrypoint copies render-config.json onto the mounted disk at
# $HOME/.nanobot/config.json (so the runtime data_dir lands on the persistent
# disk), chowns the root-owned mount, drops to the non-root nanobot user, and
# appends the --config flag itself — so pass only `gateway` here.
dockerCommand: /usr/local/bin/entrypoint.sh gateway
plan: starter
healthCheckPath: /
envVars:
# Anthropic API key — powers the agent's LLM calls. Get one at
# https://console.anthropic.com/settings/keys
- key: ANTHROPIC_API_KEY
sync: false
# WebUI access secret — the gate for the public WebUI. Generate a strong
# random value (e.g. `openssl rand -hex 32`) and keep it private.
- key: NANOBOT_WEB_TOKEN
sync: false
# Port Render routes public traffic to; matches channels.websocket.port
# in render-config.json.
- key: PORT
value: 8765
# Persist sessions, memory, and the WebUI display transcripts across deploys.
# The entrypoint copies the config onto this mount, so nanobot's runtime
# data_dir (config_path.parent) resolves here too — keeping webui/ (chat
# history the UI renders), cron, media, and logs durable, not just session
# files. Starter / 1 GB is the lean default; history persistence needs this
# disk, not a bigger plan.
disk:
name: nanobot-data
mountPath: /home/nanobot/.nanobot
sizeGB: 1