DEV Community

Cover image for How to Run A2A-Compatible Agents on a Single VPS (No Docker, No Kubernetes)
Maretto
Maretto

Posted on

How to Run A2A-Compatible Agents on a Single VPS (No Docker, No Kubernetes)

The Agent-to-Agent (A2A) protocol is becoming the standard for AI agent interoperability. But most guides assume you're running Kubernetes, Docker Compose, or a cloud platform.

What if you just want to run a few agents on a single VPS? No containers. No orchestration. Just agents talking to each other on localhost.

That's what ag2ag does.

What is ag2ag?

ag2ag is an open-source operational layer for running A2A-compatible agents on a single host. It provides:

  • Local registry — JSON file tracking all your agents, their ports, and systemd units
  • Lifecycle management — start, stop, restart agents via systemd
  • Discovery — each agent exposes an AgentCard at GET /card
  • Messaging — agents send messages to each other on localhost
  • Task persistence — JSONL files that survive restarts
  • CLI — manage everything from the terminal

One external dependency: @a2a-js/sdk. Everything else is Node.js built-ins.

Not affiliated with, endorsed by, or connected to the A2A Protocol project, Google, or the Linux Foundation.

Prerequisites

  • A Linux server (VPS, homelab, dev VM)
  • Node.js 18+
  • systemd (comes with any modern Linux distro)
node --version  # v22.x recommended
Enter fullscreen mode Exit fullscreen mode

Step 1: Install ag2ag

npm install -g ag2ag
Enter fullscreen mode Exit fullscreen mode

Or clone and link:

git clone https://github.com/Maretto/ag2ag.git
cd ag2ag && npm install && npm link
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize

ag2ag init
Enter fullscreen mode Exit fullscreen mode

This creates the registry file and data directories.

Step 3: Build Your First Agent

Create a file called my-agent.js:

const { AgentServer } = require('ag2ag');

const card = {
  schemaVersion: '1.0',
  name: 'greeter',
  description: 'A simple greeting agent',
  url: 'http://127.0.0.1:5001',
  capabilities: { streaming: false, pushNotifications: false },
  skills: [
    { name: 'greet', description: 'Returns a friendly greeting' }
  ],
};

async function handleMessage(message, task) {
  // Extract text from the incoming message
  const text = message.parts?.[0]?.text || 'unknown';

  return {
    parts: [{ type: 'text', text: `Hello! You said: "${text}"` }],
    source: 'greeter',
    timestamp: new Date().toISOString(),
  };
}

const server = new AgentServer({
  agentCard: card,
  agentName: 'greeter',
  port: 5001,
  handler: handleMessage,
});

server.start().then(({ port }) => {
  console.log(`Greeter agent running on port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Test It

node my-agent.js &
Enter fullscreen mode Exit fullscreen mode

Check its AgentCard:

curl http://127.0.0.1:5001/card | jq .
Enter fullscreen mode Exit fullscreen mode

Send a message:

ag2ag register greeter --port 5001 --description "A simple greeting agent"
ag2ag call greeter "Hey there"
Enter fullscreen mode Exit fullscreen mode

Step 5: Build a Second Agent (That Calls the First)

Now the interesting part — an agent that discovers and calls another agent:

const { AgentServer, AgentClient } = require('ag2ag');

const card = {
  schemaVersion: '1.0',
  name: 'orchestrator',
  description: 'Calls the greeter agent and returns the response',
  url: 'http://127.0.0.1:5002',
  capabilities: { streaming: false, pushNotifications: false },
  skills: [
    { name: 'forward-greeting', description: 'Forwards a message to the greeter' }
  ],
};

async function handleMessage(message, task) {
  const text = message.parts?.[0]?.text || 'hello from orchestrator';

  // Call the greeter agent
  const client = new AgentClient();
  const result = await client.sendMessage(5001, {
    role: 'user',
    parts: [{ type: 'text', text }],
  });

  // Wait for completion
  const completed = await client.waitForTask(5001, result.data.id, {
    interval: 500,
    timeout: 10000,
  });

  const response = completed.artifacts?.[0]?.parts?.[0]?.text || 'no response';

  return {
    parts: [{ type: 'text', text: `Greeter responded: "${response}"` }],
    source: 'orchestrator',
    timestamp: new Date().toISOString(),
  };
}

const server = new AgentServer({
  agentCard: card,
  agentName: 'orchestrator',
  port: 5002,
  handler: handleMessage,
});

server.start().then(({ port }) => {
  console.log(`Orchestrator running on port ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Run it:

node orchestrator.js &
ag2ag register orchestrator --port 5002 --description "Calls greeter agent"
ag2ag call orchestrator "A2A is cool"
Enter fullscreen mode Exit fullscreen mode

You just built agent composition — one agent discovering and calling another via the A2A protocol.

Step 6: Deploy with systemd

For production use, you want agents running as systemd services. ag2ag handles this:

# Generate a systemd unit
ag2ag register greeter --port 5001 --unit greeter.service

# Start as a service
ag2ag start greeter

# Check status
ag2ag status --health

# View logs
ag2ag logs greeter
Enter fullscreen mode Exit fullscreen mode

Agents now survive reboots, restart on failure, and integrate with your server's logging.

Real-World Example: Health Proxy

I use this in production with a Health Proxy agent that queries multiple services and returns an aggregated health report:

// Simplified — the real version queries API Gateway + Mesh Ping
async function handleMessage(message, task) {
  const client = new AgentClient();

  const [gateway, mesh] = await Promise.all([
    client.getCard(3099),
    client.getCard(3101),
  ]);

  return {
    parts: [{
      type: 'text',
      text: `API Gateway: ${gateway.data.name}\nMesh Ping: ${mesh.data.name}`
    }],
    source: 'health-proxy',
    timestamp: new Date().toISOString(),
  };
}
Enter fullscreen mode Exit fullscreen mode

This agent doesn't know about the other agents at build time — it discovers them at runtime via their AgentCards.

Security Considerations

ag2ag is designed for localhost-only environments. Important limitations:

  • No authentication — all communication is unencrypted HTTP on loopback
  • No inter-agent isolation — agents run as systemd services, typically under the same user
  • No concurrency testing — JSONL persistence hasn't been stress-tested under parallel writes
  • Body limit — 1MB max payload per request

For more details, see SECURITY.md.

When to Use This vs Alternatives

Situation Use
Single VPS, 2-20 agents ✅ ag2ag
Multi-host, distributed Docker Compose, Kubernetes
Need auth/encryption Build your own auth layer or use a service mesh
Just managing Node.js processes PM2
Building A2A agents from scratch @a2a-js/sdk directly

What's Next

ag2ag is experimental (v0.2.0) but functional. It's been validated with 6 agents on a single VPS, including real composition patterns.

If you're experimenting with the A2A protocol and want a lightweight way to run agents without container overhead, give it a try:

PRs, issues, and feedback welcome.

Top comments (0)