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:
- Zero dependency footprint — systemd ships with the OS itself
- True process supervision — it tracks the actual process tree via cgroups, not just PID files
- Automatic log capture — stdout and stderr are piped to journald with timestamps and metadata
- Resource limits — memory, CPU, and I/O constraints enforced at the kernel level
- Socket activation — services can be started lazily when traffic arrives on a socket
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
- Use
Type=notifyfor sophisticated agents — your application sends a readiness signal via sd_notify, letting systemd know exactly when startup is complete - Always set
RestartSec— a backoff delay prevents thrashing when the agent fails immediately on restart - Leverage
ProtectSystem=strict— this mounts the root filesystem read-only, dramatically reducing the blast radius of a compromised agent - Direct logs to journald — avoid writing to separate log files; use stdout/stderr so logs are unified with system metadata
- Set explicit resource limits — an agent with a memory leak shouldn't bring down the entire host
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
- Use multi-stage builds — keep the final image slim by copying only runtime artifacts from a builder stage
- Always include a HEALTHCHECK — Docker uses this to determine if the container is actually functioning, not just running
- Never run as root inside the container — create a dedicated user and switch to it with
USER - Use
restart: unless-stopped— this survives Docker daemon restarts but allows manual intervention - Mount sensitive paths explicitly with
:ro— reduce the attack surface by making volumes read-only where possible - Tag images with semantic versions — avoid
:latestin production; pin to specific version tags for reproducible deployments
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
- Always use an ecosystem file — CLI flags are convenient for prototyping but not reproducible; ecosystem files are declarative and version-controlled
- Set
max_memory_restart— this acts as a safety valve for memory leaks, restarting the process gracefully before it exhausts system memory - Use
pm2 startupfor production — this integrates PM2 with systemd or init scripts so your agents survive server reboots - Separate error and output logs — this simplifies debugging and allows different retention policies for errors versus normal output
- Beware of cluster mode for stateful agents — if your agent maintains in-memory state (conversations, pending tasks), cluster mode without a shared store will cause inconsistencies
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:
- The agent runs on a dedicated Linux VM or bare-metal server
- Dependencies are managed at the OS level (apt, pip system-wide, or vendored binaries)
- You need tight kernel integration — cgroups, socket activation, or systemd timers for scheduling
- The team is comfortable with Linux administration
- Overhead matters — systemd adds zero runtime resource consumption beyond the agent itself
Choose Docker when:
- The agent has complex, version-specific dependencies (Python 3.12 exactly, specific CUDA libraries)
- You deploy across heterogeneous environments — developer laptops, CI runners, cloud VMs, Kubernetes
- Multiple agents must run in concert with isolated networking and volumes
- You need immutable deployment artifacts that can be rolled back atomically
- The agent is part of a larger containerized microservice ecosystem
Choose PM2 when:
- The agent is written in Node.js or TypeScript
- You want the simplest possible setup — a single config file and three CLI commands to production
- Built-in clustering across CPU cores matters without external load balancing
- The team is primarily JavaScript developers who find systemd unit files intimidating
- You need rapid iteration with live reload capabilities in development
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
- PID file mismatch — all three tools track the main process; if your agent forks daemon children (common with legacy Python apps), the supervisor sees the wrong PID and restart logic fails. Always run agents in the foreground with
Type=simple(systemd),--no-daemon(PM2), or a foreground process (Docker). - Log rotation neglect — agents can produce gigabytes of logs daily. Configure
journaldrotation (systemd), Docker logging driver max-size, or PM2 log rotation viapm2-logrotateexplicitly. - Health check gaps — a process that hasn't crashed isn't necessarily healthy. Implement application-level health checks (HTTP endpoints, file timestamps, queue depths) in addition to process-level supervision.
- Graceful shutdown ignorance — agents processing work must drain queues before exiting. All three tools support graceful shutdown signals (SIGTERM); your agent must handle them with a configurable drain timeout.
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.