How to Deploy Hermes Agent on Cloud Run instances

1. Introduction

Overview

In this lab, you will deploy a fully persistent, secure instance of the Hermes Agent (by Nous Research) to Cloud Run Instances. You will interact with your AI agent using the Hermes Web Dashboard, and back its persistent workspace with Google Cloud Storage.

While Hermes supports a Gateway mode that could run as an auto-scaling Cloud Run Service, it also acts as a stateful agent that scans skills on boot and handles background execution. Cloud Run Instances provide a long-lived, individually addressable environment that is a perfect fit for this workload.

What you'll do

  • Prepare a Cloud Storage bucket to persist container state and configurations.
  • Create a custom Python supervisor (run_hermes.py) and a startup script (start_hermes.sh) to handle boot initialization.
  • Deploy the Hermes Agent using gcloud beta run instances deploy.
  • Access and authenticate to the Hermes Dashboard.

What you'll learn

  • How to deploy the Hermes Agent to Cloud Run Instances.
  • How to mount Cloud Storage buckets to Cloud Run Instances using GCSFuse.
  • How to safely configure SQLite and ephemeral caches to bypass GCSFuse file-locking limitations.

2. Setup and Requirements

GCP Project Setup

  1. Sign in to the Google Cloud Console.
  2. Create or select a Google Cloud Project.
  3. Ensure billing is enabled for your Google Cloud project.

Open Cloud Shell

Activate Google Cloud Shell from the top toolbar of the Cloud Console.

Set Project & Install gcloud beta

First, set your project and region as environment variables.

export PROJECT_ID=<YOUR_PROJECT_ID>
export REGION="us-west2"
export BUCKET_NAME="hermes-state-${PROJECT_ID}"

And configure your project for gcloud.

gcloud config set project $PROJECT_ID

Ensure the beta component is installed for gcloud beta run instances:

gcloud components install beta --quiet

And your gcloud version is up-to-date.

gcloud components updates

Enable Required Google Cloud APIs

In Cloud Shell, enable the Cloud Run, Cloud Storage, and Secret Manager APIs:

gcloud services enable \
  run.googleapis.com \
  secretmanager.googleapis.com \
  storage.googleapis.com \
  compute.googleapis.com \
  aiplatform.googleapis.com

3. Create Dedicated Service Account

To adhere to the principle of least privilege, create a dedicated IAM service account for the Hermes agent and grant it the necessary permissions to invoke Vertex AI models:

export SERVICE_ACCOUNT_NAME="hermes-sa"
gcloud iam service-accounts create ${SERVICE_ACCOUNT_NAME} \
  --display-name="Hermes Service Account"

export SERVICE_ACCOUNT="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role="roles/aiplatform.user"

4. Store Credentials in Secret Manager

We will store sensitive credentials like the dashboard password in Google Cloud Secret Manager so Cloud Run can securely inject them into the container at boot time.

Generate a secure random password for your dashboard and store it in Secret Manager:

export DASHBOARD_PASSWORD=$(openssl rand -hex 16)
echo "Generated Hermes Dashboard Password: ${DASHBOARD_PASSWORD}"

echo -n "${DASHBOARD_PASSWORD}" | gcloud secrets create hermes-dashboard-password \
  --data-file=- \
  --replication-policy="automatic"

gcloud secrets add-iam-policy-binding hermes-dashboard-password \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role="roles/secretmanager.secretAccessor"

5. Prepare Cloud Storage Bucket & Configuration Files

Hermes needs persistent storage mounted as /opt/data. We will use a Google Cloud Storage (GCS) bucket and mount it using Cloud Storage volume mounts.

1. Create a Cloud Storage Bucket

gcloud storage buckets create gs://${BUCKET_NAME} --location=${REGION}

# Grant the service account permissions to mount the bucket
gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role="roles/storage.objectAdmin"

2. Create config.yaml

Create a config.yaml file. Be sure to include _config_version: 12 to ensure the configuration is loaded correctly:

_config_version: 12

model:
  default: "google/gemini-3.8-flash"
  provider: "vertex"

dashboard:
  enabled: true

database:
  journal_mode: delete

3. Create the Supervisor Script (run_hermes.py)

Cloud Storage doesn't support the specific file locking mechanisms that SQLite databases need to run safely. To prevent database corruption, we need a custom "supervisor" script (run_hermes.py). This script configures Hermes to store its temporary database locks in the container's local memory instead of on Cloud Storage before starting the agent.

Create run_hermes.py locally:

import os
import shutil
import subprocess
import sys
import threading
import time

print(
    "=== INITIALIZING HERMES SUPERVISOR ===", flush=True
)

# 1. Local Directory Setup
# Creates temporary, local folders (in /tmp) for the agent's caches and working directories.
# See more below in comment section NOTE ON CLOUD STORAGE FUSE
home_dir = "/tmp/hermes_home"
hermes_dir = os.path.join(home_dir, ".hermes")
os.makedirs(hermes_dir, exist_ok=True)
os.makedirs("/tmp/logs", exist_ok=True)
os.makedirs("/tmp/skills", exist_ok=True)
os.makedirs("/tmp/uv_cache", exist_ok=True)
os.makedirs("/tmp/cache", exist_ok=True)
os.makedirs("/opt/data/workspace", exist_ok=True)
os.makedirs("/opt/data/.hermes", exist_ok=True)

# 2. State Restoration & Database Config
# Copies your existing configurations and chat history (state.db) from Cloud Storage into the local folders. 
# It also forces the SQLite database into TRUNCATE mode, a crucial step to prevent database corruption 
# when eventually saving back to Cloud Storage. See more in section 3. Note on Cloud Storage Fuse below
if os.path.exists("/opt/data/config.yaml"):
  shutil.copy("/opt/data/config.yaml", os.path.join(hermes_dir, "config.yaml"))
  print(f"Synced config.yaml -> {hermes_dir}/config.yaml", flush=True)
elif os.path.exists("/opt/data/.hermes/config.yaml"):
  shutil.copy("/opt/data/.hermes/config.yaml", os.path.join(hermes_dir, "config.yaml"))
  print(f"Synced config.yaml from .hermes -> {hermes_dir}/config.yaml", flush=True)

if os.path.exists("/opt/data/.env"):
  shutil.copy("/opt/data/.env", os.path.join(hermes_dir, ".env"))
  print(f"Synced .env -> {hermes_dir}/.env", flush=True)
elif os.path.exists("/opt/data/.hermes/.env"):
  shutil.copy("/opt/data/.hermes/.env", os.path.join(hermes_dir, ".env"))
  print(f"Synced .env from .hermes -> {hermes_dir}/.env", flush=True)

if os.path.exists("/opt/data/.hermes/state.db"):
  shutil.copy("/opt/data/.hermes/state.db", os.path.join(hermes_dir, "state.db"))
  print(f"Synced state.db -> {hermes_dir}/state.db (restored previous chats!)", flush=True)

# 3. Note on Cloud Storage Fuse
# Cloud Storage FUSE is optimized for object storage, but is not fully POSIX compliant.
# This means GCS lacks the byte-range file locking required by active caches and default SQLite (WAL mode) 
# which SQLite depends on to prevent data collisions.
# Without these locks, SQLite experiences database corruption and blocked I/O operations.
# To ensure stability, we route these active I/O processes to local container memory (/tmp).
# See section 5 Enable Autosave below on how /tmp is uploaded to Cloud Storage.
# Read more: https://cloud.google.com/storage/docs/cloud-storage-fuse/overview#differences-and-limitations
db_path = os.path.join(hermes_dir, "state.db")
try:
  import sqlite3
  conn = sqlite3.connect(db_path)
  conn.execute("PRAGMA journal_mode=TRUNCATE;")
  conn.close()
  print("Configured SQLite database to TRUNCATE mode for direct single-file persistence", flush=True)
except Exception as e:
  print(f"Warning: Failed to configure TRUNCATE mode: {e}", flush=True)

subprocess.run(["chmod", "-R", "777", "/tmp"], check=False)

# 4. Update system environment variables
# Hermes needs to know to look at the new local /tmp folders rather than defaulting to the mounted bucket.
env = dict(os.environ)
env["HOME"] = home_dir
env["HERMES_HOME"] = hermes_dir
env["PATH"] = "/opt/hermes/.venv/bin:/opt/hermes/bin:" + env.get("PATH", "")
env["PYTHONUNBUFFERED"] = "1"
env["HERMES_STATE_PATH"] = hermes_dir
env["HERMES_SKILLS_PATH"] = "/tmp/skills"
env["UV_CACHE_DIR"] = "/tmp/uv_cache"
env["XDG_CACHE_HOME"] = "/tmp/cache"
env["SQLITE_BUSY_TIMEOUT"] = "30000"
env["HERMES_ALLOW_ROOT_GATEWAY"] = "1"
env["HERMES_WORKSPACE"] = "/opt/data/workspace"
env["HERMES_WRITE_SAFE_ROOT"] = "/opt/data"

python_bin = "/opt/hermes/.venv/bin/python3"

# 5. Enable Autosave 
# Spawn a background worker thread to watch your local database and config files every 5 seconds.
# As you chat with your agent, this worker thread automatically copies the updated database content
# back to Cloud Storage to persist it.
def sync_to_gcs_loop():
  files_to_sync = ["state.db", "config.yaml", ".env"]
  last_mtimes = {}
  
  # Initialize last_mtimes
  for f in files_to_sync:
    path = os.path.join(hermes_dir, f)
    if os.path.exists(path):
      last_mtimes[f] = os.path.getmtime(path)
    else:
      last_mtimes[f] = 0
      
  while True:
    time.sleep(5)
    for f in files_to_sync:
      src_path = os.path.join(hermes_dir, f)
      if os.path.exists(src_path):
        try:
          mtime = os.path.getmtime(src_path)
          if mtime > last_mtimes.get(f, 0):
            dst_path = os.path.join("/opt/data/.hermes", f)
            shutil.copy2(src_path, dst_path)
            last_mtimes[f] = mtime
            print(f"Auto-saved {f} to GCS volume mount", flush=True)
        except Exception as e:
          print(f"Error auto-saving {f} to GCS: {e}", flush=True)

threading.Thread(target=sync_to_gcs_loop, daemon=True).start()


# 6. Launch the Hermes Gateway (the AI backend) and the Web Dashboard (the UI)
# These are launched as parallel processes, sending logs to Cloud Run via stdout & stderr
print("=== STARTING GATEWAY IN BACKGROUND ===", flush=True)
gw = subprocess.Popen(
    [python_bin, "-m", "hermes_cli.main", "gateway", "run"],
    env=env,
    cwd="/opt/data/workspace",
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1,
)

def stream_gw():
  for line in iter(gw.stdout.readline, ""):
    if line:
      print(f"[GATEWAY] {line.rstrip()}", flush=True)

threading.Thread(target=stream_gw, daemon=True).start()

print("=== STARTING DASHBOARD ON 0.0.0.0:8080 ===", flush=True)
sys.stdout.flush()

dash = subprocess.Popen(
    [
        python_bin,
        "-m",
        "hermes_cli.main",
        "dashboard",
        "--host",
        "0.0.0.0",
        "--port",
        "8080",
        "--skip-build",
    ],
    env=env,
    cwd="/opt/data/workspace",
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1,
)

for line in iter(dash.stdout.readline, ""):
  if line:
    print(f"[DASHBOARD] {line.rstrip()}", flush=True)

rc = dash.wait()
print(f"DASHBOARD EXITED WITH RETURN CODE: {rc}", flush=True)

while True:
  time.sleep(10)

4. Create the Startup Script (start_hermes.sh)

Create start_hermes.sh locally.

#!/bin/sh
set -e
export PYTHONUNBUFFERED=1
exec python3 /opt/data/run_hermes.py

5. Upload Files to Cloud Storage

Copy the configuration files to the root of your GCS bucket:

gcloud storage cp config.yaml run_hermes.py start_hermes.sh gs://${BUCKET_NAME}/

6. Deploy Hermes on Cloud Run Instances

We use gcloud beta run instances deploy to deploy the container. This command includes specific configurations to address known issues with GCSFuse and container limits.

Ensure your environment variables (PROJECT_ID, REGION, BUCKET_NAME, SERVICE_ACCOUNT) are exported in your active terminal session.

Deploy the instance:

gcloud beta run instances deploy hermes-instance \
  --image nousresearch/hermes-agent:latest \
  --service-account ${SERVICE_ACCOUNT} \
  --command "/bin/sh" \
  --args "/opt/data/start_hermes.sh" \
  --port 8080 \
  --cpu 2 \
  --memory 4Gi \
  --ingress all \
  --no-invoker-iam-check \
  --add-volume name=hermes-storage,mount-path=/opt/data,type=cloud-storage,mount-options="uid=2000;gid=2000;file-mode=0777;dir-mode=0777;implicit-dirs",bucket=$BUCKET_NAME \
  --set-secrets "HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=hermes-dashboard-password:latest" \
  --set-env-vars "PYTHONUNBUFFERED=1,VERTEX_PROJECT_ID=$PROJECT_ID,VERTEX_LOCATION=global,HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin,HERMES_ALLOW_ROOT_GATEWAY=1,HERMES_WORKSPACE=/opt/data/workspace,HERMES_WRITE_SAFE_ROOT=/opt/data" \
  --region $REGION \
  --project $PROJECT_ID

Important Configurations Included Above:

  • --service-account: Attaches the dedicated hermes-sa service account.
  • Supervisor Scripts: start_hermes.sh calls the custom Python supervisor run_hermes.py that routes SQLite locking limits and caching issues away from GCS FUSE into local tmpfs.
  • --set-secrets: Injects credentials directly from Secret Manager into environment variables.

7. Interact Directly via the Hermes Web UI

Once deployed, you can access your dashboard at the generated .run.app URL. When prompted for authentication, enter admin as the Username and your ${DASHBOARD_PASSWORD} as the Password.

Chat with your agent

You can try things like echo "hello" to confirm the agent is working.

Test persistent storage

You can test persistent storage in your Google Cloud Bucket by asking the agent

Write "hello world" to a file named hello.txt in your workspace.

Then in your shell, you can verify the file was written by running

gcloud storage cat gs://$BUCKET_NAME/workspace/hello.txt

Lastly, to verify that your chats and your files are persisted across new Cloud Run instances (as a Cloud Run instance has up to 7-day continuous runtime, with automatic restart policy configured by default), you can rerun the gcloud beta run instances deploy command exactly as before. Then you'll see your chat sessions. And you can ask your agent

Read the contents of the file hello.txt in your workspace.

and you'll see "hello world".

8. Clean Up

To avoid incurring charges to your Google Cloud account for the resources used in this codelab:

  1. Delete the Cloud Run Instance:
    gcloud beta run instances delete hermes-instance --region ${REGION} --quiet
    
  2. Delete Secret Manager Secrets:
    gcloud secrets delete hermes-dashboard-password --quiet
    
  3. Delete Cloud Storage Bucket:
    gcloud storage rm -r gs://${BUCKET_NAME}
    
  4. Delete Dedicated Service Account:
    gcloud iam service-accounts delete ${SERVICE_ACCOUNT} --quiet
    

9. Conclusion

Congratulations! You have successfully deployed a secure, fully persistent instance of the Hermes Agent on Cloud Run Instances backed by Cloud Storage!

What you learned

  • How to deploy the Hermes Agent to Cloud Run Instances.
  • How to mount Cloud Storage buckets to Cloud Run Instances using GCSFuse.
  • How to safely configure SQLite and ephemeral caches to bypass GCSFuse file-locking limitations.