Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .env.combined.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Combined Hermes Container Configuration
# Copy this file to .env and modify as needed

# Docker image settings
HERMES_AGENT_IMAGE=nousresearch/hermes-agent:latest
COMBINED_IMAGE=hermes-combined:latest
CONTAINER_NAME=hermes

# Data directory (host path)
HERMES_DATA_DIR=./hermes_data

# Web UI settings
PORT=6060
AUTH_DISABLED=false

# Notes:
# - UPSTREAM is automatically set to http://127.0.0.1:8642 (localhost within container)
# - HERMES_BIN is set to /opt/hermes/.venv/bin/hermes
# - HERMES_HOME is set to /opt/data/.hermes (matches hermes user home)
# - Ports 8642-8670 are exposed for multi-profile gateway support
# - Container user is 'hermes' with home /opt/data
58 changes: 58 additions & 0 deletions Dockerfile.combined
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
ARG BASE_IMAGE=nousresearch/hermes-agent:latest
FROM ${BASE_IMAGE}

USER root

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*

# Install Node.js
RUN ARCH=$(dpkg --print-architecture) \
&& if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; else NODE_ARCH="$ARCH"; fi \
&& echo "Downloading Node.js v23.11.0 for ${NODE_ARCH}" \
&& curl -fsSL "https://nodejs.org/dist/v23.11.0/node-v23.11.0-linux-${NODE_ARCH}.tar.gz" \
-o /tmp/node.tar.gz \
&& tar -xzf /tmp/node.tar.gz -C /usr/local --strip-components=1 \
&& rm -f /tmp/node.tar.gz \
&& node --version

WORKDIR /app

# Copy and build web-ui
COPY package*.json ./
# Increase Node.js memory limit to prevent OOM during build
ENV NODE_OPTIONS=--max-old-space-size=4096
RUN npm install --ignore-scripts && npm rebuild node-pty

COPY . .
RUN npm run build && npm prune --omit=dev

# Ensure proper permissions for hermes user
RUN chown -R hermes:hermes /app

# Copy startup script
COPY docker/start-combined.sh /start-combined.sh
RUN chmod +x /start-combined.sh

# Environment variables
ENV NODE_ENV=production
ENV HOME=/opt/data
ENV HERMES_HOME=/opt/data/.hermes
ENV PORT=6060
ENV UPSTREAM=http://127.0.0.1:8642
ENV HERMES_BIN=/opt/hermes/.venv/bin/hermes
ENV PATH=/opt/hermes/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Expose ports
# 8642-8670: Hermes Agent gateway ports
# 6060: Web UI port
EXPOSE 8642-8670 6060

# Use combined startup script
ENTRYPOINT ["/start-combined.sh"]
CMD []
28 changes: 28 additions & 0 deletions docker-compose.combined.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
services:
hermes:
build:
context: .
dockerfile: Dockerfile.combined
args:
BASE_IMAGE: ${HERMES_AGENT_IMAGE:-nousresearch/hermes-agent:latest}
image: ${COMBINED_IMAGE:-hermes-combined:latest}
container_name: ${CONTAINER_NAME:-hermes}
user: root
volumes:
- ${HERMES_DATA_DIR:-./hermes_data}:/opt/data/.hermes:z
- ${HERMES_DATA_DIR:-./hermes_data}/hermes-web-ui:/opt/data/.hermes-web-ui:z
- ${HERMES_DATA_DIR:-./hermes_data}/workspace:/opt/data/workspace:z
environment:
- PORT=${PORT:-6060}
- HERMES_HOME=/opt/data/.hermes
- HERMES_BIN=/opt/hermes/.venv/bin/hermes
- AUTH_DISABLED=${AUTH_DISABLED:-false}
- HERMES_ALLOW_ROOT_GATEWAY=1
- GATEWAY_DEFAULT_HOST=127.0.0.1
- PATH=/opt/hermes/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ports:
- "${PORT:-6060}:${PORT:-6060}"
- "8642-8670:8642-8670"
stdin_open: true
tty: true
restart: unless-stopped
37 changes: 37 additions & 0 deletions docker/start-combined.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
set -e

# Combined startup script for hermes-agent + hermes-web-ui
# Runs both services in a single container (as root)
# The web-ui's GatewayManager handles hermes-agent lifecycle

# Graceful shutdown handler (before exec replaces this process)
cleanup() {
echo "[start-combined] Shutting down..."
exit 0
}

trap cleanup SIGTERM SIGINT SIGQUIT

# Mark container environment so GatewayManager uses "gateway run" mode
# (Podman doesn't create /.dockerenv like Docker does)
touch /.dockerenv 2>/dev/null || true

# Ensure .hermes directory exists
echo "[start-combined] Ensuring .hermes directory..."
mkdir -p /opt/data/.hermes/logs
mkdir -p /opt/data/.hermes-web-ui

# Ensure workspace directory exists
mkdir -p /opt/data/workspace

# Set hermes default working directory to /opt/data/workspace
echo "[start-combined] Setting hermes cwd to /opt/data/workspace..."
/opt/hermes/.venv/bin/hermes config set messaging.cwd /opt/data/workspace 2>/dev/null || true
/opt/hermes/.venv/bin/hermes config set terminal.cwd /opt/data/workspace 2>/dev/null || true

# Start hermes-webui in foreground (as root, no su)
# The web-ui's GatewayManager will start hermes-agent automatically
echo "[start-combined] Starting hermes-webui on port ${PORT:-6060}..."
cd /app
exec node dist/server/index.js
79 changes: 79 additions & 0 deletions docs/docker-combined.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Combined Hermes Container

将webui和agent容器合二为一,对于容器文件权限做处理,兼容SELinux和podman

## Quick Start

### Build and Run

```bash
# Copy environment configuration
cp .env.combined.example .env

# Edit .env if needed (optional)
# vim .env

# Build and start (using podman-compose)
podman-compose -f docker-compose.combined.yml up -d --build

# View logs
podman-compose -f docker-compose.combined.yml logs -f

# Stop
podman-compose -f docker-compose.combined.yml down
```

### Using Docker

```bash
# Build and start (using docker compose)
docker compose -f docker-compose.combined.yml up -d --build

# View logs
docker compose -f docker-compose.combined.yml logs -f

# Stop
docker compose -f docker-compose.combined.yml down
```

## Access

- **Web UI:** http://localhost:6060
- **Auth Token:** Check logs or `./hermes_data/hermes-web-ui/.token`

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `HERMES_AGENT_IMAGE` | `nousresearch/hermes-agent:latest` | Base hermes-agent image |
| `COMBINED_IMAGE` | `hermes-combined:latest` | Combined image name |
| `CONTAINER_NAME` | `hermes` | Container name |
| `HERMES_DATA_DIR` | `./hermes_data` | Host path for data persistence |
| `PORT` | `6060` | Web UI port |
| `AUTH_DISABLED` | `false` | Set to `true` to disable authentication |

### Restart Services

```bash
podman-compose -f docker-compose.combined.yml restart
```

### Rebuild from Scratch

```bash
podman-compose -f docker-compose.combined.yml down
podman-compose -f docker-compose.combined.yml up -d --build
```

### Check Container Health

```bash
# Check if both services are running
podman-compose -f docker-compose.combined.yml ps

# Check hermes-agent default gateway health
curl http://localhost:8642/health

# Check hermes-webui health
curl http://localhost:6060/health
```
4 changes: 3 additions & 1 deletion packages/server/src/services/hermes/gateway-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ export class GatewayManager {
*/
private readProfilePort(name: string): { port: number; host: string } {
const configPath = join(this.profileDir(name), 'config.yaml')
const defaultHost = initSystem === 'container' ? 'hermes-agent' : '127.0.0.1'
const defaultHost = initSystem === 'container'
? (process.env.GATEWAY_DEFAULT_HOST || 'hermes-agent')
: '127.0.0.1'

if (!existsSync(configPath)) return { port: 8642, host: defaultHost }

Expand Down