← Back to Blog

From Single-Purpose to Platform: Turning an OpenClaw Container into a Multi-Project Agent Factory

In the previous post, we built Hephaestus — a three-agent pipeline that automatically dissects C++ code and produces technical reference documents. It works well, but there's a problem: the entire container stack is tightly coupled to the Hephaestus project.

If tomorrow I wanted to use the same OpenClaw infrastructure to run a completely different agent — say, one that monitors GitHub Trending and writes weekly digests, or one that scans vulnerability databases on a schedule — I'd have to set up a nearly identical Docker environment from scratch.

That's wasteful. The OpenClaw Gateway itself is generic. The project-specific parts are actually quite small. This post documents how to refactor a single-project container into a platform where switching projects takes one line.

Diagnosing the Coupling

Before refactoring, diagnose. The current Hephaestus stack has coupling at three layers:

Layer 1: Image (Dockerfile)

FROM alpine/openclaw:latest
# Hephaestus-specific: Rust + Go toolchains
RUN curl https://sh.rustup.rs | sh -s -- -y
RUN curl -sSL https://go.dev/dl/go1.24.1.linux-amd64.tar.gz | tar -C /usr/local -xz

Rust and Go are needed for Hephaestus's compilation verification step. A different project might need Python + Node, or nothing at all. Baking language toolchains into the base image is wasteful.

Layer 2: Orchestration (docker-compose.yml)

container_name: hephaestus-gateway  # Hardcoded project name
ports: ["18790:18790"]              # Hardcoded port
volumes:
  - ./data/openclaw:/home/node/.openclaw  # Single data directory

Container names, ports, and data paths are all hardcoded. Running a second project would cause conflicts.

Layer 3: Data (data/ directory)

data/
├── openclaw/
│   ├── openclaw.json      ← Hephaestus's three-agent config
│   └── workspace/
│       ├── SOUL.md        ← Hephaestus's writing rules
│       ├── HEARTBEAT.md   ← Hephaestus's pipeline
│       └── TOPIC_INDEX.md ← Hephaestus's topic list
└── ssh/                   ← Hephaestus's Git credentials

All runtime data sits in one directory with no project isolation.

The Fix: Three-Layer Decoupling

Layer 1: Layered Images

Split the Dockerfile into a base image and toolchain extensions:

# Dockerfile — Base image, shared by all projects
FROM alpine/openclaw:latest
USER root
RUN apt-get update -qq && \
    apt-get install -y -qq --no-install-recommends ripgrep && \
    rm -rf /var/lib/apt/lists/*
USER node
# dockerfiles/Dockerfile.dev — Dev image for projects needing compilation
FROM openclaw-base:latest
USER root
# Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
    sh -s -- -y --no-modify-path
# Go
RUN curl -sSL https://go.dev/dl/go1.24.1.linux-amd64.tar.gz | \
    tar -C /usr/local -xz
USER node

Projects that don't need compilation (pure text processing, API calls) use the base image — faster builds, smaller footprint.

Layer 2: Parameterized Orchestration

Drive docker-compose.yml with a .env file. All project-specific values become variables:

# .env — Switch projects by editing this file
PROJECT=hephaestus
CONTAINER_PREFIX=hephaestus
GATEWAY_PORT=18790
OPENCLAW_IMAGE=openclaw-dev:latest
# docker-compose.yml — Parameterized
services:
  tailscale:
    container_name: ${CONTAINER_PREFIX}-ts
    ports:
      - "${GATEWAY_PORT}:${GATEWAY_PORT}"

  openclaw-gateway:
    image: ${OPENCLAW_IMAGE}
    container_name: ${CONTAINER_PREFIX}-gateway
    volumes:
      - ./projects/${PROJECT}/openclaw:/home/node/.openclaw
      - ./projects/${PROJECT}/ssh:/home/node/.ssh
    command: ["node", "dist/index.js", "gateway", "--bind", "lan",
              "--port", "${GATEWAY_PORT}"]

  openclaw-cli:
    image: ${OPENCLAW_IMAGE}
    container_name: ${CONTAINER_PREFIX}-cli
    volumes:
      - ./projects/${PROJECT}/openclaw:/home/node/.openclaw
      - ./projects/${PROJECT}/ssh:/home/node/.ssh

The key change: ./data/openclaw becomes ./projects/${PROJECT}/openclaw. Data follows the project.

Layer 3: Project Directory Isolation

Each project is a self-contained directory with its own runtime state:

projects/
├── hephaestus/
│   ├── openclaw/
│   │   ├── openclaw.json         ← 3 Agents: Scanner/Analyzer/Writer
│   │   ├── agents/               ← Session state
│   │   ├── devices/              ← Device pairing
│   │   └── workspace/
│   │       ├── SOUL.md           ← C++ analysis rules
│   │       ├── HEARTBEAT.md      ← Daily writing pipeline
│   │       └── TOPIC_INDEX.md    ← 35 topics
│   └── ssh/

├── github-digest/                ← Future project example
│   ├── openclaw/
│   │   ├── openclaw.json         ← 1 Agent: Digest Writer
│   │   └── workspace/
│   │       ├── SOUL.md           ← Weekly report rules
│   │       └── HEARTBEAT.md      ← Weekly trigger
│   └── ssh/

└── vuln-scanner/
    ├── openclaw/
    │   ├── openclaw.json         ← 2 Agents: Scanner/Reporter
    │   └── workspace/
    └── ssh/

Each project directory is fully self-contained. Switching projects never affects another project's state, session history, or pairing data.

Day-to-Day Operations

Switching Projects

# Edit .env — change one line
PROJECT=github-digest

# Restart
docker compose down openclaw-gateway
docker compose up -d

Creating a New Project

# Scaffold script
./new-project.sh my-new-project

# What it does:
# - Creates projects/my-new-project/{openclaw/workspace,ssh}
# - Writes a minimal openclaw.json template
# - Creates an empty SOUL.md
# - Optionally generates SSH keys

Running Multiple Projects in Parallel

If you need two projects running simultaneously (different ports), use separate env files:

docker compose --env-file .env.hephaestus up -d
docker compose --env-file .env.digest up -d

For most scenarios though, one project at a time is sufficient. The OpenClaw Gateway is lightweight — starting and stopping takes seconds.

The Scaffold Script

#!/usr/bin/env bash
# new-project.sh — Create a new project skeleton
set -euo pipefail
NAME="${1:?Usage: $0 <project-name>}"
DIR="projects/$NAME"

[ -d "$DIR" ] && { echo "Project '$NAME' already exists"; exit 1; }

mkdir -p "$DIR"/{openclaw/workspace,ssh}

cat > "$DIR/openclaw/openclaw.json" << 'EOF'
{
  "gateway": {
    "mode": "local",
    "bind": "lan",
    "port": 18790,
    "controlUi": { "enabled": true, "allowInsecureAuth": true }
  },
  "agents": {
    "defaults": {
      "model": { "primary": "google/gemini-3-flash", "fallbacks": ["openai/gpt-4o"] },
      "sandbox": { "mode": "off" }
    },
    "list": [
      {
        "id": "main",
        "name": "Main Agent",
        "default": true
      }
    ]
  }
}
EOF

cat > "$DIR/openclaw/workspace/SOUL.md" << 'EOF'
# Soul

Your agent identity and rules go here.
EOF

echo "Created project: $DIR"
echo "Next steps:"
echo "  1. Edit $DIR/openclaw/openclaw.json — configure agents"
echo "  2. Edit $DIR/openclaw/workspace/SOUL.md — write rules"
echo "  3. Set PROJECT=$NAME in .env"
echo "  4. docker compose up -d"

Migration Checklist

Moving from the current single-project structure to the multi-project layout:

Step Action Risk
1 mv data/ projects/hephaestus/ Low — directory rename
2 Split Dockerfile into base + dev Low — requires rebuild
3 Parameterize docker-compose.yml Medium — test variable substitution
4 Create .env file Low
5 Update .gitignore (projects/ replaces data/) Low
6 Update attach.sh Low

The entire migration takes under 30 minutes, with less than a minute of Gateway downtime.

Design Trade-offs

Why not Kubernetes? This runs on a personal Mac at home. A single docker-compose file is plenty. K8s declarative configuration would be better for multi-project orchestration, but the operational overhead far exceeds the benefit at this scale.

Why not run multiple projects' agents in one Gateway? OpenClaw agents share a single workspace. Different projects' SOUL.md files would overwrite each other, and heartbeat scheduling would get confused. The cleanest isolation is separate .openclaw directories per project.

Should SSH keys be shared across projects? Depends. If multiple projects push to the same Gitea, sharing keys makes sense (use symlinks). If projects access different Git services, separate keys are more secure.

Takeaway

The core idea behind this refactoring is simple: find the coupling, parameterize it.

  • Image coupling → layered Dockerfiles
  • Orchestration coupling → .env parameterization
  • Data coupling → project directory isolation

The end result: switching projects means changing one line in .envPROJECT=xxx — then docker compose up -d. Each project's agent config, workspace state, and SSH credentials are fully independent.

The OpenClaw Gateway is already a generic AI Agent runtime. Our job isn't to modify it — it's to not let our deployment patterns limit its generality.