← Back to DevBytes

Systemd vs Docker vs PM2 for Agent Processes: What Works

Understanding Agent Processes in Modern Development

Agent processes are long-running background services that perform automated tasks — monitoring file changes, processing queues, managing scheduled jobs, or running AI inference loops. Unlike short-lived scripts, these processes must survive crashes, restart cleanly, and integrate with the operating system's lifecycle. Choosing the right process manager directly impacts reliability, observability, and deployment velocity. Three dominant contenders have emerged: systemd, Docker, and PM2. Each solves the problem differently, and understanding their trade-offs is essential for any developer shipping production agents.

What Is systemd?

Systemd is the init system and service manager for most modern Linux distributions. It runs as PID 1 and manages the entire lifecycle of services — starting them at boot, monitoring their health, restarting them on failure, and handling logging via journald. Systemd unit files define services declaratively, giving you fine-grained control over environment variables, resource limits, working directories, and restart policies. It is deeply integrated into the Linux kernel ecosystem through cgroups, making process isolation and resource accounting native rather than bolted-on.

Why systemd Matters for Agents

If your agent runs on bare metal or a virtual machine and needs to start at boot without any containerization overhead, systemd is the most natural choice. It provides:

Creating a systemd Agent Service

Below is a complete example of a systemd service unit for a Python agent that monitors a directory and processes incoming files. Create this file at /etc/systemd/system/file-agent.service:

[Unit]
Description=File Processing Agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=agent-user
Group=agent-group
WorkingDirectory=/opt/file-agent
Environment="AGENT_CONFIG=/etc/file-agent/config.yaml"
Environment="PYTHONUNBUFFERED=1"
ExecStart=/opt/file-agent/venv/bin/python -m agent.main
ExecStop=/bin/kill -s SIGTERM $MAINPID
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
MemoryHigh=2G
MemoryMax=2.5G
CPUQuota=200%
LimitNOFILE=65536

# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/file-agent /tmp/agent-work
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

After creating the unit file, enable and start it with these commands:

# Reload systemd to recognize the new unit
sudo systemctl daemon-reload

# Enable the service to start at boot
sudo systemctl enable file-agent.service

# Start it immediately
sudo systemctl start file-agent.service

# Check status and recent logs
sudo systemctl status file-agent.service
sudo journalctl -u file-agent.service -f

Systemd Best Practices

What Is Docker?

Docker is a containerization platform that packages applications with their entire dependency tree into portable images. Containers run in isolated namespaces with their own filesystem, network stack, and process space. Docker provides a declarative way to define services through Dockerfiles and Compose files, with built-in restart policies, logging drivers, and volume management. Unlike systemd, Docker abstracts the host OS entirely — an agent runs the same way on a developer laptop, a cloud VM, or a Kubernetes cluster.

Why Docker Matters for Agents

Docker shines when your agent has complex dependencies, needs to run in heterogeneous environments, or is part of a microservice architecture. The image becomes a self-contained artifact that includes the exact Python runtime, native libraries, and configuration files — eliminating the "works on my machine" problem. Docker Compose extends this to multi-agent setups, letting you define entire swarms of cooperating agents in a single YAML file.

Creating a Dockerized Agent

First, write a Dockerfile that bakes the agent into an image:

FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim AS runner
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY agent/ ./agent/
COPY config.yaml ./

# Create non-root user
RUN groupadd -r agent && useradd -r -g agent agent && chown -R agent:agent /app
USER agent

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD python -c "import sys; sys.exit(0)" || exit 1

ENTRYPOINT ["python", "-m", "agent.main"]

Then define the agent service with Docker Compose for production-grade management:

version: "3.8"
services:
  file-agent:
    build:
      context: .
      dockerfile: Dockerfile
    image: file-agent:latest
    container_name: file-agent-prod
    restart: unless-stopped
    environment:
      - AGENT_CONFIG=/app/config.yaml
      - PYTHONUNBUFFERED=1
    volumes:
      - agent-data:/var/lib/file-agent
      - /tmp/agent-work:/tmp/agent-work:rw
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2.5G
        reservations:
          memory: 512M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - agent-net

  watchdog:
    image: alpine:latest
    container_name: agent-watchdog
    restart: unless-stopped
    command: |
      sh -c 'while true; do
        if ! curl -s http://file-agent:8080/health > /dev/null; then
          echo "Agent unhealthy, triggering restart" >> /var/log/watchdog.log
        fi
        sleep 60
      done'
    depends_on:
      - file-agent
    networks:
      - agent-net

volumes:
  agent-data:
    driver: local

networks:
  agent-net:
    driver: bridge

Docker Best Practices for Agents

What Is PM2?

PM2 is a production-grade process manager for Node.js applications, though it can manage any executable through its interpreter configuration. It provides process monitoring, automatic restarts, log management, and clustering — all controlled through a straightforward CLI. PM2 maintains an in-memory process registry and can save it to disk for resurrection across reboots. Unlike systemd (kernel-level) and Docker (container-level), PM2 operates purely in userspace as a Node.js daemon managing child processes.

Why PM2 Matters for Agents

PM2 is the sweet spot for JavaScript/TypeScript agents that don't need full containerization. It's dramatically simpler than systemd unit files for developers who want a quick, reliable way to keep a Node.js agent running. The built-in keymetrics integration (optional) provides web-based monitoring, and PM2's cluster mode can fork multiple instances across CPU cores without a load balancer. For AI agents built with Node.js or TypeScript — increasingly common in the LangChain and ModelContextProtocol ecosystems — PM2 offers the fastest path from development to production.

Setting Up a PM2 Agent

First, create an ecosystem configuration file ecosystem.config.js that defines how PM2 should manage your agent:

module.exports = {
  apps: [
    {
      name: 'inference-agent',
      script: 'dist/agent.js',
      interpreter: 'node',
      node_args: '--max-old-space-size=2048',
      instances: 2,
      exec_mode: 'cluster',
      watch: false,
      max_memory_restart: '2G',
      env: {
        NODE_ENV: 'production',
        AGENT_CONFIG: '/etc/agent/config.yaml',
        MODEL_PATH: '/data/models/llama-3',
      },
      error_file: '/var/log/inference-agent/err.log',
      out_file: '/var/log/inference-agent/out.log',
      log_file: '/var/log/inference-agent/combined.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      max_restarts: 10,
      restart_delay: 4000,
      kill_timeout: 10000,
      listen_timeout: 8000,
    },
    {
      name: 'cleanup-worker',
      script: 'dist/cleanup.js',
      instances: 1,
      exec_mode: 'fork',
      cron_restart: '0 3 * * *', // Daily restart at 3 AM for cleanup
      env: {
        NODE_ENV: 'production',
      },
    },
  ],
};

Start and manage the agent with these PM2 commands:

# Start all apps defined in the ecosystem file
pm2 start ecosystem.config.js

# View real-time dashboard with CPU/memory per process
pm2 monit

# List all managed processes with their status
pm2 list

# View logs in real time (streams from all instances)
pm2 logs inference-agent --lines 100

# Save the current process list so PM2 resurrects them on reboot
pm2 save

# Generate a startup script that hooks into the OS boot sequence
pm2 startup systemd
# Follow the printed instructions to complete setup

For a Node.js agent that requires no configuration file, a minimal CLI invocation works too:

# Start a single-instance agent with a one-liner
pm2 start dist/agent.js \
  --name "simple-agent" \
  --max-memory-restart 2G \
  --env production \
  --restart-delay 3000

# Stop and remove from the registry
pm2 delete simple-agent

PM2 Best Practices

Comparative Analysis: When to Use What

The decision hinges on three factors: stack complexity, deployment target, and team expertise. Here's a practical decision framework:

Choose systemd when:

Choose Docker when:

Choose PM2 when:

Combined Approaches: The Hybrid Strategy

In practice, production systems often combine these tools. A common pattern is to wrap a PM2-managed agent inside a Docker container, then orchestrate that container with systemd on the host. This gives you Docker's dependency isolation, PM2's process-level monitoring and clustering, and systemd's boot-time startup guarantees:

[Unit]
Description=Containerized PM2 Agent
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/docker compose -f /opt/agent/compose.yml up -d
ExecStop=/usr/bin/docker compose -f /opt/agent/compose.yml down
ExecStopPost=/usr/bin/docker compose -f /opt/agent/compose.yml rm -f
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Inside the Docker Compose file, the container runs PM2 as its entrypoint:

services:
  node-agent:
    image: node-agent:1.2.0
    entrypoint: ["pm2", "start", "ecosystem.config.js", "--no-daemon"]
    restart: unless-stopped
    # ...

This hybrid approach gives you systemd's host integration, Docker's dependency packaging, and PM2's fine-grained process management — each layer solving the problem it solves best.

Common Pitfalls Across All Three

Conclusion

Systemd, Docker, and PM2 are not competitors — they are complementary tools at different layers of the stack. Systemd excels at host-level service lifecycle management with minimal overhead and deep OS integration. Docker provides dependency isolation and environment parity, turning an agent into a portable, immutable artifact. PM2 offers the gentlest learning curve for Node.js agents with clustering and real-time monitoring built in. The right choice depends on your runtime stack, deployment topology, and operational maturity. For many production agents, a layered strategy — PM2 inside Docker, orchestrated by systemd — combines the strengths of all three while insulating your team from the weaknesses of any single tool. Start simple, then add layers as your reliability requirements grow.

— Ad —

Google AdSense will appear here after approval

← Back to all articles