Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

Run CrewAI on a VPS: An Offline-Tested Two-Agent Demo

Published

AI

CrewAI can run a sequence of AI-assisted tasks on a VPS. This guide uses two agents to draft and review a short support handoff from a synthetic incident note. It starts with an offline wiring check, then shows how to configure an optional OpenRouter connection. It does not connect a customer mailbox, publish content, run server commands or change a hosting account.

A VPS hosts the orchestration process, not necessarily the model. In the optional provider mode, the note, prompts and intermediate draft are sent to OpenRouter and its selected model provider. Hosting charges and model usage are separate. A laptop or development machine is sufficient for the initial test; you do not need a public web service or dashboard.

What this example verifies

The example was checked on October 14, 2026 with CrewAI 1.15.28 and Python 3.12.15 in a non-root Linux container with external networking disabled. Two real CrewAI tasks ran using canned responses, and a mocked HTTP transport checked the provider request format, model ID and task handoff. No live model, paid API call, MCP integration or production VPS workload was tested. The fixture output below is not an AI-generated diagnosis or a benchmark.

1. Prepare an isolated Python environment

Use a supported Linux installation with Python 3.12 and its virtual-environment package available. Have the administrator maintain the operating system; do not give the agent account sudo or Docker-daemon access. Run these Bash commands in a new development directory, outside a website’s document root:

umask 077
mkdir crewai-handoff-demo
cd crewai-handoff-demo
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install 'crewai==1.15.28'
python -m pip check
python -c "import importlib.metadata; print(importlib.metadata.version('crewai'))"

The version check should print 1.15.28; stop if installation or dependency checks fail. The pinned package is the tested version, not a promise of continuing security support. Review releases and re-run tests before upgrades. Pinning the top-level package alone does not lock every dependency: keep a reviewed dependency lock or tested image for a repeatable deployment. Do not overwrite the system Python or run pip as root to resolve conflicts.

This small program defines its crew directly in Python. It does not rely on a generated YAML or JSON project layout. The current CrewAI installation guide covers the separate CLI scaffolding workflow. No optional search, file-access, Anthropic or LiteLLM extra is needed for the native OpenAI-compatible client used here.

2. Create the two-task program

Put the following in crewai_demo.py. The first task summarizes the supplied note; the second receives both that draft and the original note. Neither agent has tools, delegation, memory or planning enabled. The reviewer is another model call, not an independent source of truth.

import argparse
import os

os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"

from crewai import Agent, BaseLLM, Crew, LLM, Process, Task

NOTE = (
    "Synthetic ticket: the test website returned HTTP 503 at 10:15 UTC. "
    "The operator has not checked application logs. No cause is confirmed."
)


def build_crew(llm):
    common = dict(llm=llm, tools=[], allow_delegation=False,
                  max_iter=2, max_retry_limit=0, verbose=False)
    writer = Agent(role="Handoff writer", goal="Summarize supplied facts only",
                   backstory="You draft short internal support handoffs.", **common)
    reviewer = Agent(role="Handoff reviewer", goal="Separate facts from unknowns",
                     backstory="You check drafts against the supplied note.", **common)
    draft = Task(
        description="Draft a short handoff from this note. Do not diagnose: {note}",
        expected_output="Observed symptom, time and what remains unknown.",
        agent=writer,
    )
    review = Task(
        description=("Review the draft against the original note: {note}. "
                     "Remove unsupported causes. Return a handoff for human review."),
        expected_output="A short factual handoff with unverified details labeled.",
        agent=reviewer, context=[draft],
    )
    return Crew(agents=[writer, reviewer], tasks=[draft, review],
                process=Process.sequential, memory=False, planning=False,
                cache=False, tracing=False, share_crew=False, verbose=False)


class OfflineFixture(BaseLLM):
    """Canned responses test wiring, not model reasoning."""
    def __init__(self):
        super().__init__(model="offline-fixture")
        self.messages_seen = []

    def supports_function_calling(self):
        return False

    def call(self, messages, tools=None, callbacks=None,
             available_functions=None, **kwargs):
        self.messages_seen.append(messages)
        replies = [
            "Final Answer: DRAFT: HTTP 503 at 10:15 UTC; cause unknown.",
            "Final Answer: REVIEW: HTTP 503 at 10:15 UTC. Logs not checked; "
            "cause unconfirmed. Human review required.",
        ]
        if len(self.messages_seen) > len(replies):
            raise RuntimeError("Unexpected extra fixture call")
        return replies[len(self.messages_seen) - 1]


def provider_llm():
    key = os.environ.get("OPENROUTER_API_KEY", "").strip()
    model = os.environ.get("OPENROUTER_MODEL", "").strip()
    if not key or not model:
        raise ValueError("Set OPENROUTER_API_KEY and OPENROUTER_MODEL first.")
    return LLM(model=model, custom_openai=True,
               base_url="https://openrouter.ai/api/v1", api_key=key,
               max_tokens=500, timeout=30, max_retries=0)


def main():
    parser = argparse.ArgumentParser(description="Synthetic support-handoff demo")
    mode = parser.add_mutually_exclusive_group(required=True)
    mode.add_argument("--offline-test", action="store_true")
    mode.add_argument("--run", action="store_true", help="Make billable provider calls")
    args = parser.parse_args()
    try:
        llm = OfflineFixture() if args.offline_test else provider_llm()
    except ValueError as error:
        parser.error(str(error))
    result = build_crew(llm).kickoff(inputs={"note": NOTE})
    if args.offline_test:
        if len(llm.messages_seen) != 2 or not result.raw.startswith("REVIEW:"):
            raise RuntimeError("Offline wiring check failed")
        print("OFFLINE FIXTURE ONLY: no model quality or provider test")
    print(result.raw)


if __name__ == "__main__":
    main()

The explicit telemetry and tracing settings apply to this demonstration. They do not block network traffic or override the policies of a model provider, package dependency or tool you add later. Review CrewAI telemetry and your logging configuration before using sensitive information. Even with memory disabled, framework task records or application logs can persist locally; keep the working account private and define retention deliberately.

3. Run the offline wiring check first

python crewai_demo.py --offline-test

The fixture returns fixed strings through CrewAI’s custom LLM interface. It should finish once and print:

OFFLINE FIXTURE ONLY: no model quality or provider test
REVIEW: HTTP 503 at 10:15 UTC. Logs not checked; cause unconfirmed. Human review required.

This checks imports, construction and sequential execution. It does not prove that a real model will summarize accurately. Running the script without a mode stops with a usage error; it does not silently start a billable job. Our additional isolated tests checked that the first task’s draft reached the second task and that missing or blank provider configuration was rejected.

4. Optionally connect to OpenRouter

Before making a live request, choose an available chat model from OpenRouter’s model catalog, check its supported parameters and pricing, and create a separate API key with a small credit limit. Do not assume a model is available, free or compatible because it appeared in an older example. Review both OpenRouter’s policies and the selected provider’s data retention and training policies.

In the same Bash session, enter the model ID and key interactively. The hidden key prompt avoids placing the literal secret in a command in shell history; environment variables are still accessible to sufficiently privileged processes and are not a secret vault.

read -r -p 'OpenRouter model ID (from its catalog): ' OPENROUTER_MODEL
read -r -s -p 'OpenRouter test API key: ' OPENROUTER_API_KEY
printf '\n'
export OPENROUTER_MODEL OPENROUTER_API_KEY
timeout 180s python crewai_demo.py --run
unset OPENROUTER_API_KEY OPENROUTER_MODEL

This uses CrewAI’s native custom OpenAI-compatible endpoint configuration, with custom_openai=True and OpenRouter’s HTTPS API base. Enter the full catalog model ID, including its publisher segment. Do not add the extra openrouter/ routing prefix used by CrewAI’s separate LiteLLM integration. The Anthropic extra is not required merely because the selected model is made by Anthropic; the transport here remains OpenRouter’s compatible API.

Retry limits are not a spending guarantee. Our simulated HTTP 429 failure produced three framework-level attempts before raising an error, despite zero SDK and agent retry settings. The second task did not start. This is an observed result for the tested version, not a fixed limit promised for every failure. Use the provider’s key-level credit limit, inspect usage, and do not wrap the command in an automatic retry loop. The timeout command limits the local process; it cannot cancel charges for requests already accepted remotely.

Successful model output will vary. Check that it retains the observed symptom and time, says logs have not been checked, and does not invent a root cause. Test incorrect credentials, unavailable models and ambiguous input before considering any real data. Authentication, model availability, provider billing and actual output quality were not validated by our offline tests.

5. Add tools only after defining their permissions

A role called researcher does not give an agent web access. The example intentionally performs no research and has no file reader, database connection or MCP server. Add one integration at a time with a synthetic test and a clear reason for the permission.

  • Use read-only, narrowly scoped credentials first. A directory path in a tool’s configuration is not automatically an operating-system sandbox.
  • Do not expose SSH keys, payment credentials, production database passwords or the Docker socket to an agent.
  • Treat retrieved documents, tool descriptions and outputs as untrusted input. Keep account-changing actions behind an authorization check and human confirmation outside the prompt.
  • Check the installed version’s supported transport, authentication and tool-selection controls before connecting an MCP endpoint. A URL alone does not establish a trusted or authorized connection.

See CrewAI’s MCP security guidance and our prompt-injection precautions. For the hosting-specific endpoint and its verification status, use the InterServer MCP server guide; this example does not demonstrate a working connection to that service.

Before running unattended on a VPS

The script is a one-shot command, not a web server. Do not open port 8000 or any other inbound port for it. Do not configure restart: unless-stopped around a completed job: repeated launches can repeat paid requests. If you later schedule a validated workflow, add an overlap lock, input identifiers, bounded execution, a failure alert and a record of already-processed jobs.

Keep source, credentials and logs outside the public web root. Exclude secrets from both version control and container build contexts, supply only required secrets at runtime, and use a non-root process. Test backups and recovery, update dependencies in staging, and measure actual memory, disk and concurrent-job use before choosing capacity. This review did not establish a universal RAM requirement or a production hosting guarantee.

Start with a human-reviewed draft, not automated publishing or service changes. The useful result of this exercise is a small workflow whose inputs, permissions and failure behavior you can inspect.

Related guides

Account services are unavailable until the production backend is configured.