ci: publish Docker images to GHCR and Docker Hub (#237)

* feat(ci): add node/python/full runtime variants to Docker publish

Add runtime image variants alongside existing build-tag variants:
- :node (pre-installed Node.js)
- :python (pre-installed Python)
- :full (Node.js + Python + all skill deps)

* feat(ci): add install scripts, release binaries, and Discord notifications

- scripts/install.sh: one-liner binary installer from GitHub Releases
- scripts/setup-docker.sh: interactive Docker setup with variant selection
  (alpine/node/python/full), auto-generates .env + docker-compose.yaml
  with persistent volumes for data, skills, workspace, storage
- release.yaml: build cross-platform binaries (linux/darwin × amd64/arm64)
  and attach to GitHub Release, notify Discord on new releases
- .gitignore: ignore Apple Double (._*) files from external volumes

* docs(docker): add pre-built image references and update docker-compose workflows

- Add `image:` directives to docker-compose.yml, docker-compose.selfservice.yml, and docker-compose.upgrade.yml pointing to ghcr.io/nextlevelbuilder/goclaw pre-built images
- Add Docker Hub mirror references (digitop/goclaw) for public access
- Document available image tags (latest, node, python, full, otel, tsnet, redis) with descriptions
- Update README with pre-built image pull instructions and semver tag examples
- Clarify deployment workflows: use pre-built images by default (no --build), add --build only when building from source
- Update upgrade workflow to pull pre-built images instead of rebuild
- Add note about build args requirement for otel/tsnet/redis overlays
- Update .dockerignore to exclude macOS temp files (._*)

* fix(ci): use claude_code_oauth_token instead of anthropic_api_key
This commit is contained in:
Goon authored and GitHub committed 2026-03-17 12:44:18 +07:00
1 parent 97cacfe68b
commit 9429a7c844
12 files changed
+540 -34

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
.git
.github
._*
.env*
.dockerignore
*.md
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
id: claude-review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
id: claude
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
+55 -19
View File
@@ -19,31 +19,64 @@ jobs:
strategy:
matrix:
include:
# ── Runtime variants ──
- variant: latest
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
suffix: ""
- variant: otel
enable_otel: "true"
enable_tsnet: "false"
enable_redis: "false"
suffix: "-otel"
- variant: tsnet
enable_otel: "false"
enable_tsnet: "true"
enable_redis: "false"
suffix: "-tsnet"
- variant: redis
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "true"
suffix: "-redis"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: node
suffix: "-node"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "true"
enable_python: "false"
enable_full_skills: "false"
- variant: python
suffix: "-python"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_python: "true"
enable_full_skills: "false"
- variant: full
enable_otel: "true"
enable_tsnet: "true"
enable_redis: "true"
suffix: "-full"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "true"
enable_python: "true"
enable_full_skills: "true"
# ── Build-tag variants ──
- variant: otel
suffix: "-otel"
enable_otel: "true"
enable_tsnet: "false"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: tsnet
suffix: "-tsnet"
enable_otel: "false"
enable_tsnet: "true"
enable_redis: "false"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
- variant: redis
suffix: "-redis"
enable_otel: "false"
enable_tsnet: "false"
enable_redis: "true"
enable_node: "false"
enable_python: "false"
enable_full_skills: "false"
steps:
- name: Checkout
@@ -93,6 +126,9 @@ jobs:
ENABLE_OTEL=${{ matrix.enable_otel }}
ENABLE_TSNET=${{ matrix.enable_tsnet }}
ENABLE_REDIS=${{ matrix.enable_redis }}
ENABLE_NODE=${{ matrix.enable_node }}
ENABLE_PYTHON=${{ matrix.enable_python }}
ENABLE_FULL_SKILLS=${{ matrix.enable_full_skills }}
VERSION=${{ github.ref_name }}
cache-from: type=gha,scope=${{ matrix.variant }}
cache-to: type=gha,mode=max,scope=${{ matrix.variant }}
+75 -1
View File
@@ -10,12 +10,86 @@ permissions:
jobs:
release:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.semrel.outputs.version }}
released: ${{ steps.semrel.outputs.version != '' }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: go-semantic-release/action@v1
- id: semrel
uses: go-semantic-release/action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
allow-initial-development-versions: true
# Build cross-platform binaries and attach to release
build-binaries:
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
VERSION: v${{ needs.release.outputs.version }}
run: |
CGO_ENABLED=0 go build \
-ldflags="-s -w -X github.com/nextlevelbuilder/goclaw/cmd.Version=${VERSION}" \
-o goclaw .
tar -czf "goclaw-${{ needs.release.outputs.version }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz" goclaw
- name: Upload to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "v${{ needs.release.outputs.version }}" \
"goclaw-${{ needs.release.outputs.version }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz" \
--clobber
# Notify Discord on new release
notify-discord:
needs: [release, build-binaries]
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- name: Send Discord notification
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
VERSION: v${{ needs.release.outputs.version }}
run: |
curl -fsSL -H "Content-Type: application/json" \
-d "{
\"embeds\": [{
\"title\": \"GoClaw ${VERSION} Released\",
\"url\": \"https://github.com/${{ github.repository }}/releases/tag/${VERSION}\",
\"color\": 5814783,
\"fields\": [
{\"name\": \"Docker\", \"value\": \"\`docker pull digitop/goclaw:latest\`\", \"inline\": false},
{\"name\": \"Install\", \"value\": \"\`curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/main/scripts/install.sh | bash\`\", \"inline\": false}
],
\"footer\": {\"text\": \"${{ github.repository }}\"},
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}]
}" \
"$DISCORD_WEBHOOK_URL"
+3 -1
View File
@@ -10,6 +10,7 @@ openclaw-go
# OS
.DS_Store
._*
# Environment
.env*
@@ -36,6 +37,7 @@ tmp
deploy-*.sh
docs/technical-diaries
docs/zuey
plans
release-manifest.json
k8s-*/*
@@ -43,4 +45,4 @@ k8s-*/*
# Test credentials (never commit)
tests/**/creds.json
ui/simple-saas
*.tar
*.tar
+52 -9
View File
@@ -344,10 +344,14 @@ The script creates `.env` from `.env.example`, auto-generates `GOCLAW_ENCRYPTION
```bash
# Recommended: Gateway + Web Dashboard (http://localhost:3000)
# Pull pre-built images:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d
# Or build from source:
docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d --build
# Without dashboard
docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d --build
docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
# + OpenTelemetry tracing (Jaeger at http://localhost:16686)
docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.otel.yml up -d --build
@@ -440,7 +444,40 @@ CGO_ENABLED=0 go build -ldflags="-s -w" -tags "otel,tsnet" -o goclaw .
> Optional features are gated behind build tags to avoid binary bloat. OTel adds ~11 MB (gRPC + protobuf). Tailscale adds ~20 MB (tsnet + WireGuard). The base build includes in-app tracing backed by PostgreSQL and localhost-only access.
### Docker Build
### Docker Images (Pre-built)
Pre-built multi-arch images (linux/amd64 + linux/arm64) are published to **GHCR** and **Docker Hub** on every release:
```bash
# GHCR (recommended)
docker pull ghcr.io/nextlevelbuilder/goclaw:latest
# Docker Hub
docker pull digitop/goclaw:latest
```
**Available tags:**
| Tag | Description |
| ---------- | -------------------------------------- |
| `latest` | Base image (~50 MB Alpine) |
| `node` | + Node.js runtime for JS tools |
| `python` | + Python runtime for Python tools |
| `full` | + Node.js + Python + all bundled skills |
| `otel` | + OpenTelemetry tracing support |
| `tsnet` | + Tailscale VPN mesh listener |
| `redis` | + Redis cache backend |
Semver tags are also available: `1.0.0`, `1.0`, etc. (e.g. `ghcr.io/nextlevelbuilder/goclaw:1.0.0-python`).
**Web Dashboard:**
```bash
docker pull ghcr.io/nextlevelbuilder/goclaw-web:latest
docker pull digitop/goclaw-web:latest
```
### Docker Build (from source)
```bash
# Standard image (~50MB Alpine)
@@ -682,13 +719,13 @@ Composable files for different deployment scenarios:
# Prepare .env (auto-generates secrets, prompts for API key)
chmod +x prepare-env.sh && ./prepare-env.sh
# Managed (PostgreSQL)
docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d --build
# Using pre-built images (no --build flag):
docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
# Managed + Web Dashboard (http://localhost:3000)
docker compose -f docker-compose.yml \
-f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d --build
-f docker-compose.selfservice.yml up -d
# Managed + Web Dashboard + OpenTelemetry (Jaeger UI at http://localhost:16686)
docker compose -f docker-compose.yml \
@@ -705,15 +742,21 @@ docker compose -f docker-compose.yml \
curl http://localhost:18790/health
```
> **Note:** Omit `--build` to use pre-built images from GHCR. Add `--build` to build from source. Overlays that require build args (otel, tsnet, redis, sandbox) need `--build`.
### Upgrading (Docker Compose)
**Simple upgrade** — pull the latest code, rebuild, and restart. The entrypoint automatically runs `goclaw upgrade` (schema migrations + data hooks) before starting:
**Simple upgrade** — pull the latest images and restart. The entrypoint automatically runs `goclaw upgrade` (schema migrations + data hooks) before starting:
```bash
# Pull latest code
git pull
# Using pre-built images (recommended):
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml pull
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d
# Rebuild and restart (auto-upgrades database on start)
# Or build from source:
git pull
docker compose -f docker-compose.yml -f docker-compose.postgres.yml \
-f docker-compose.selfservice.yml up -d --build
```
+1
View File
@@ -7,6 +7,7 @@
services:
goclaw-ui:
image: ghcr.io/nextlevelbuilder/goclaw-web:latest
build:
context: ./ui/web
dockerfile: Dockerfile
+1
View File
@@ -14,6 +14,7 @@
services:
upgrade:
image: ghcr.io/nextlevelbuilder/goclaw:latest
build:
context: .
dockerfile: Dockerfile
+8 -2
View File
@@ -1,14 +1,20 @@
# Base docker-compose — shared service definition.
# Pre-built images: ghcr.io/nextlevelbuilder/goclaw (also on Docker Hub: digitop/goclaw)
#
# Combine with overlays for your deployment:
#
# Recommended (+ Web Dashboard):
# Recommended (+ Web Dashboard, pre-built images):
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d
#
# Build from source (add --build):
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.selfservice.yml up -d --build
#
# Without dashboard: docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d --build
# Without dashboard: docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
# With OTel tracing: docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.otel.yml up -d --build
services:
goclaw:
image: ghcr.io/nextlevelbuilder/goclaw:latest
build:
context: .
dockerfile: Dockerfile
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# GoClaw installer — downloads the latest binary from GitHub Releases.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/nextlevelbuilder/goclaw/main/scripts/install.sh | bash
# curl -fsSL ... | bash -s -- --version v1.30.0
# curl -fsSL ... | bash -s -- --dir /opt/goclaw
#
# Supported: Linux (amd64/arm64), macOS (amd64/arm64)
set -euo pipefail
REPO="nextlevelbuilder/goclaw"
INSTALL_DIR="${GOCLAW_INSTALL_DIR:-/usr/local/bin}"
VERSION=""
# ── Parse args ──
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2 ;;
--dir) INSTALL_DIR="$2"; shift 2 ;;
--help|-h)
echo "Usage: install.sh [--version v1.x.x] [--dir /path]"
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# ── Detect OS/arch ──
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
case "$OS" in
linux|darwin) ;;
*) echo "Unsupported OS: $OS"; exit 1 ;;
esac
# ── Resolve version ──
if [ -z "$VERSION" ]; then
echo "Fetching latest release..."
VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')"
fi
echo "Installing GoClaw ${VERSION} (${OS}/${ARCH})..."
# ── Download ──
ASSET="goclaw-${VERSION#v}-${OS}-${ARCH}.tar.gz"
URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
echo "Downloading ${URL}..."
curl -fsSL -o "${TMP}/${ASSET}" "$URL"
# ── Extract & install ──
tar -xzf "${TMP}/${ASSET}" -C "$TMP"
# Check write permission, use sudo if needed
if [ -w "$INSTALL_DIR" ]; then
cp "${TMP}/goclaw" "${INSTALL_DIR}/goclaw"
chmod +x "${INSTALL_DIR}/goclaw"
else
echo "Installing to ${INSTALL_DIR} (requires sudo)..."
sudo cp "${TMP}/goclaw" "${INSTALL_DIR}/goclaw"
sudo chmod +x "${INSTALL_DIR}/goclaw"
fi
echo ""
echo "GoClaw ${VERSION} installed to ${INSTALL_DIR}/goclaw"
echo ""
echo "Next steps:"
echo " 1. Set up PostgreSQL (pgvector):"
echo " docker run -d --name goclaw-pg -p 5432:5432 -e POSTGRES_PASSWORD=goclaw pgvector/pgvector:pg18"
echo ""
echo " 2. Run database migrations:"
echo " export GOCLAW_POSTGRES_DSN='postgres://postgres:goclaw@localhost:5432/postgres?sslmode=disable'"
echo " goclaw migrate up"
echo ""
echo " 3. Start the onboard wizard:"
echo " goclaw onboard"
echo ""
echo " 4. Start the gateway:"
echo " goclaw"
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env bash
# GoClaw Docker setup — generates .env and docker-compose command for your chosen variant.
#
# Usage:
# ./scripts/setup-docker.sh # Interactive mode
# ./scripts/setup-docker.sh --variant full --with-ui
# ./scripts/setup-docker.sh --variant alpine --dev
#
# Variants:
# alpine — Base image (~50 MB), no runtimes
# node — Alpine + Node.js pre-installed
# python — Alpine + Python pre-installed
# full — Alpine + Node.js + Python + all skill dependencies
#
# Flags:
# --dev Mount local source for development (live reload)
# --with-ui Include web dashboard
# --port N Gateway port (default: 18790)
# --ui-port N Dashboard port (default: 3000)
# --pg-port N PostgreSQL port (default: 5432)
set -euo pipefail
# ── Defaults ──
VARIANT=""
DEV_MODE=false
WITH_UI=false
PORT="${GOCLAW_PORT:-18790}"
UI_PORT="${GOCLAW_UI_PORT:-3000}"
PG_PORT="${POSTGRES_PORT:-5432}"
DATA_DIR="${GOCLAW_DATA_DIR:-./goclaw-data}"
# ── Parse args ──
while [[ $# -gt 0 ]]; do
case "$1" in
--variant) VARIANT="$2"; shift 2 ;;
--dev) DEV_MODE=true; shift ;;
--with-ui) WITH_UI=true; shift ;;
--port) PORT="$2"; shift 2 ;;
--ui-port) UI_PORT="$2"; shift 2 ;;
--pg-port) PG_PORT="$2"; shift 2 ;;
--data-dir) DATA_DIR="$2"; shift 2 ;;
--help|-h)
head -20 "$0" | grep '^#' | sed 's/^# \?//'
exit 0
;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# ── Interactive variant selection ──
if [ -z "$VARIANT" ]; then
echo "Select GoClaw Docker variant:"
echo ""
echo " 1) alpine — Base image (~50 MB), no runtimes"
echo " 2) node — + Node.js (for JS/TS skills)"
echo " 3) python — + Python (for Python skills)"
echo " 4) full — + Node.js + Python + all skill deps"
echo ""
read -rp "Choice [1-4, default=1]: " choice
case "${choice:-1}" in
1|alpine) VARIANT="alpine" ;;
2|node) VARIANT="node" ;;
3|python) VARIANT="python" ;;
4|full) VARIANT="full" ;;
*) echo "Invalid choice"; exit 1 ;;
esac
fi
# ── Resolve Docker image tag ──
DOCKER_IMAGE="digitop/goclaw"
case "$VARIANT" in
alpine) IMAGE_TAG="latest" ;;
node) IMAGE_TAG="node" ;;
python) IMAGE_TAG="python" ;;
full) IMAGE_TAG="full" ;;
*) echo "Unknown variant: $VARIANT"; exit 1 ;;
esac
echo ""
echo "Setting up GoClaw (${VARIANT})..."
# ── Create data directories ──
mkdir -p "${DATA_DIR}"/{config,data,workspace,skills,storage}
# ── Generate secrets if not present ──
ENV_FILE="${DATA_DIR}/.env"
if [ ! -f "$ENV_FILE" ]; then
GATEWAY_TOKEN="$(openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n')"
ENCRYPTION_KEY="$(openssl rand -hex 16 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
PG_PASSWORD="$(openssl rand -hex 12 2>/dev/null || head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')"
cat > "$ENV_FILE" <<EOF
# GoClaw environment — generated by setup-docker.sh
# Variant: ${VARIANT}
# Gateway
GOCLAW_GATEWAY_TOKEN=${GATEWAY_TOKEN}
GOCLAW_ENCRYPTION_KEY=${ENCRYPTION_KEY}
# Ports
GOCLAW_PORT=${PORT}
GOCLAW_UI_PORT=${UI_PORT}
POSTGRES_PORT=${PG_PORT}
# PostgreSQL
POSTGRES_USER=goclaw
POSTGRES_PASSWORD=${PG_PASSWORD}
POSTGRES_DB=goclaw
EOF
echo "Generated ${ENV_FILE} with random secrets."
else
echo "Using existing ${ENV_FILE}"
fi
# ── Generate docker-compose.yaml ──
COMPOSE_FILE="${DATA_DIR}/docker-compose.yaml"
cat > "$COMPOSE_FILE" <<YAML
# GoClaw Docker Compose — variant: ${VARIANT}
# Generated by setup-docker.sh. Edit as needed.
services:
postgres:
image: pgvector/pgvector:pg18
ports:
- "\${POSTGRES_PORT:-5432}:5432"
environment:
POSTGRES_USER: \${POSTGRES_USER:-goclaw}
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-goclaw}
POSTGRES_DB: \${POSTGRES_DB:-goclaw}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-goclaw}"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
goclaw:
YAML
# Dev mode: build from source; Production: use pre-built image
if [ "$DEV_MODE" = true ]; then
cat >> "$COMPOSE_FILE" <<YAML
build:
context: $(pwd)
dockerfile: Dockerfile
args:
ENABLE_PYTHON: "$([ "$VARIANT" = "python" ] || [ "$VARIANT" = "full" ] && echo true || echo false)"
ENABLE_NODE: "$([ "$VARIANT" = "node" ] || [ "$VARIANT" = "full" ] && echo true || echo false)"
ENABLE_FULL_SKILLS: "$([ "$VARIANT" = "full" ] && echo true || echo false)"
YAML
else
cat >> "$COMPOSE_FILE" <<YAML
image: ${DOCKER_IMAGE}:${IMAGE_TAG}
YAML
fi
cat >> "$COMPOSE_FILE" <<YAML
ports:
- "\${GOCLAW_PORT:-18790}:18790"
env_file:
- .env
environment:
- GOCLAW_HOST=0.0.0.0
- GOCLAW_PORT=18790
- GOCLAW_CONFIG=/app/data/config.json
- GOCLAW_SKILLS_DIR=/app/data/skills
- GOCLAW_WORKSPACE=/app/workspace
- GOCLAW_POSTGRES_DSN=postgres://\${POSTGRES_USER:-goclaw}:\${POSTGRES_PASSWORD:-goclaw}@postgres:5432/\${POSTGRES_DB:-goclaw}?sslmode=disable
volumes:
# Persistent data: config, agent files, runtime packages (pip/npm)
- goclaw-data:/app/data
# Workspace: agent working directory for file operations
- goclaw-workspace:/app/workspace
# Custom skills: user-installed skills persist across restarts
- goclaw-skills:/app/skills
# Storage: uploaded media, documents
- goclaw-storage:/app/storage
depends_on:
postgres:
condition: service_healthy
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=256m
deploy:
resources:
limits:
memory: 1G
cpus: "2.0"
pids: 200
restart: unless-stopped
YAML
# Add web UI service if requested
if [ "$WITH_UI" = true ]; then
cat >> "$COMPOSE_FILE" <<YAML
goclaw-ui:
image: ${DOCKER_IMAGE}-web:latest
ports:
- "\${GOCLAW_UI_PORT:-3000}:80"
depends_on:
- goclaw
restart: unless-stopped
YAML
fi
cat >> "$COMPOSE_FILE" <<YAML
volumes:
postgres-data:
goclaw-data:
goclaw-workspace:
goclaw-skills:
goclaw-storage:
YAML
echo ""
echo "Generated ${COMPOSE_FILE}"
echo ""
echo "── Quick start ──"
echo ""
echo " cd ${DATA_DIR}"
echo " docker compose up -d"
echo ""
if [ "$WITH_UI" = true ]; then
echo " Gateway: http://localhost:${PORT}"
echo " Dashboard: http://localhost:${UI_PORT}"
else
echo " Gateway: http://localhost:${PORT}"
echo " Add --with-ui flag to include the web dashboard."
fi
echo ""
echo "── Volumes ──"
echo ""
echo " goclaw-data → /app/data Config, runtime packages (pip/npm cache)"
echo " goclaw-workspace → /app/workspace Agent file operations"
echo " goclaw-skills → /app/skills Custom installed skills"
echo " goclaw-storage → /app/storage Uploaded media & documents"
echo " postgres-data → PostgreSQL data"
echo ""
echo "── Useful commands ──"
echo ""
echo " docker compose logs -f goclaw # View logs"
echo " docker compose exec goclaw goclaw version # Check version"
echo " docker compose down # Stop"
echo " docker compose down -v # Stop + delete all data"