Docs / Guides · updated

How do I deploy a self-hosted AI agent on Debian or Ubuntu as a systemd service?

Put the agent in its own folder with a Python virtualenv, run it through a systemd unit with a Restart= policy, and treat the LLM as an optional plug-in: a coding-agent CLI you already logged into, called as a subprocess, so no API key sits on the server. Run an offline test suite before you enable the service, and give the agent a persistent queue so a failed task is retried instead of lost.

The problem

An agent that works in your terminal usually breaks in predictable ways once it has to run on its own:

  • It stops when the SSH session closes, and nothing brings it back after a crash or a reboot.
  • API keys end up in shell history, .env files or git.
  • A task fails at 3 a.m. and is simply gone: no retry, no record.
  • You cannot test it without spending tokens or calling the network.
  • Under systemd the agent cannot find the LLM CLI, because the service does not see your ~/.local/bin in its PATH.

A practical approach you can apply today

1. One folder, one virtualenv

sudo apt-get install -y python3 python3-venv python3-pip
python3 -m venv /opt/myagent/venv
/opt/myagent/venv/bin/pip install -r /opt/myagent/requirements.txt

Keep code, venv and the agent's data (SQLite file, logs) in that folder, owned by the account that runs the service.

2. A unit file

[Unit]
Description=My agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=alice
WorkingDirectory=/opt/myagent
Environment=PATH=/home/alice/.local/bin:/usr/local/bin:/usr/bin:/bin
EnvironmentFile=-/etc/myagent.env
ExecStart=/opt/myagent/venv/bin/python /opt/myagent/agent.py run
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Save it as /etc/systemd/system/myagent.service, then:

sudo systemctl daemon-reload
sudo systemctl enable --now myagent
journalctl -u myagent -f

Points that matter:

  • User= should be the account where your LLM CLI is logged in, since the login is stored in that user's home.
  • Environment=PATH= must include the folder where the CLI lives, or the agent will not find it.
  • EnvironmentFile=-: the leading dash makes the file optional. Keep any settings there with chmod 600, not in the unit.
  • Type=simple fits a long-running loop. For a job that runs once and exits, use Type=oneshot and trigger it with a timer:
# /etc/systemd/system/myagent.timer
[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

Enable it with sudo systemctl enable --now myagent.timer. See the systemd.service and systemd.timer man pages.

3. Plug in your own LLM CLI, no API key

Coding-agent CLIs have a non-interactive mode: claude -p "prompt" for Claude Code, codex exec "prompt" for Codex. Your agent can call one with subprocess.run(..., timeout=...), read stdout, extract the JSON it asked for and validate it. Three rules:

  • Detect the CLI with shutil.which() and treat "not found" as a normal state, not a crash.
  • Keep a deterministic code path for everything that must always work. The LLM adds reasoning; it should not be the only way a task completes.
  • Always set a timeout. CLIs can be slow or wait for input.

4. A queue with retries

A SQLite table is enough for one machine: task id, type, JSON payload, status, attempt count, next run time and a unique idempotency key. On failure, increase the attempt count and reschedule with a growing delay. After the last attempt, move the task to a dead-letter state you can inspect and replay. The idempotency key stops a producer that retries from creating the same work twice.

5. Offline tests

Write tests that run with no network and no LLM: stub the CLI call and check the deterministic path. Run them before enabling the service and after every update.

How the packs do it

The packs below share one install pattern. Paid packs cost $3 one-time; the sample is free. Their install.sh (run as sudo bash install.sh):

  • installs python3, python3-venv and python3-pip with apt,
  • creates venv/ in the pack folder and installs requirements.txt,
  • runs llm_adapter.py to detect a logged-in Claude Code, Kimi Code or Codex CLI, or a local Ollama server,
  • writes /etc/systemd/system/<pack-id>.service with User= set to the account that ran sudo and a PATH that includes that account's ~/.local/bin.

The installer is idempotent, so it is safe to re-run. bash install.sh --no-service sets up only the venv. PACK_LLM=claude|kimi|codex forces a backend. If no CLI is found, the pack runs its deterministic core. bash smoke_test.sh runs the offline test suite with no keys. The quickstart walks through these steps.

Free Sample Agent (free): try the pattern first

Free Sample Agent is small on purpose: a JSON-file task queue feeding four deterministic transforms (upper, lower, reverse, wordcount), with atomic queue writes (temp file plus os.replace). Its unit is Type=oneshot: the installer enables it but does not start it. It is a structural sample, not a production product, and it is MIT licensed.

sudo bash install.sh
bash smoke_test.sh
python3 agent.py enqueue --text "hello world" --transform upper
python3 agent.py run --once
python3 agent.py status

It lets you read the installer, the unit, the LLM adapter and the tests before paying for anything.

Agent Worker Pack v2.0: the durable queue

Agent Worker Pack v2.0 is the long-running version. Its core is a SQLite queue where each task has a type, a priority (HIGH, NORMAL, LOW), a JSON payload, a maximum number of attempts, dependencies and an optional idempotency key. Failed attempts are rescheduled after 10, 60 and 600 seconds; a task that runs out of attempts goes to the dead-letter queue. Task states are pending, running, completed, blocked and dead. It does not run arbitrary commands found in a payload. The generated unit uses Restart=always and RestartSec=5.

python3 agent.py enqueue --type send_report --payload '{"report_id":"weekly"}'
python3 agent.py run --once --deterministic
python3 agent.py status
python3 agent.py replay --task t-example

Autonomous Stack Starter v2.0: a runnable foundation

Autonomous Stack Starter v2.0 is for building your own agent. It ships 5 named prompts (Bootstrap Architect, Config Reviewer, Running Guard, Scaling Advisor, Deployment Reporter), a 35-step checklist with commands, config templates (starter_config.yaml, agent_rules.yaml), a prompt tester that works with Ollama or a stub, and a minimal OODA orchestrator.

cp templates/starter_config.yaml ./starter.yaml
python3 scripts/test_prompt.py --prompt bootstrap --input "My agent must monitor Reddit and create alerts"
python3 scripts/run_agent.py --config starter.yaml --duration 60

Autonomous Agent Starter: design before code

Autonomous Agent Starter is the design companion: SOUL.md (identity, values, decision loop, anti-patterns), AUTONOMOUS.md (OODA architecture, components, MCP and A2A protocols), PROMPTS.md (5 system prompts) and a 48-step "Zero to Autonomous" checklist. Every file starts with YAML frontmatter, so an LLM can parse it too.

Honest limits

  • Debian/Ubuntu first. The installers use apt and systemd. Elsewhere, use --no-service and write your own unit.
  • Which LLMs. The shared llm_adapter.py probes for Claude Code, Kimi Code and Codex, then for a local Ollama server at http://127.0.0.1:11434 (or PACK_OLLAMA_HOST). Set PACK_LLM=ollama to prefer your local model over any installed CLI.
  • Root step. The installer runs apt and writes a unit file as root. Read install.sh before running it; it is short.
  • Scope. The Worker pack is a single-machine queue, not a distributed scheduler. The free sample only demonstrates structure.

Where to start

  1. Download the free Free Sample Agent, read install.sh, run bash smoke_test.sh, then install it with the quickstart.
  2. If you need a real task queue with retries, move to Agent Worker Pack v2.0.
  3. If you are designing a new agent, read Autonomous Agent Starter, then build on Autonomous Stack Starter v2.0.
  4. Browse all packs for domain-specific agents.

Packs used in this guide

Browse all packs by category →