How to host an AI agent on a server in 6 steps
Learning how to host an AI agent on a server is mostly learning five boring skills and one interesting one. The interesting part is the agent. The boring parts are picking a box, building an image, keeping the process alive, handling secrets, reading logs, and shipping updates. Skip the boring parts and your agent runs fine for a week, then dies at 3am and stays dead until you notice. This walkthrough covers all of it with real commands you can paste.
Here is the whole path, top to bottom:
Pick and size a VPS.
Dockerize the agent so it runs the same everywhere.
Keep it alive with systemd so a crash means a restart, not an outage.
Wire up secrets so keys are not sitting in your shell history.
Ship logs somewhere you will actually read them.
Automate updates so a pinned image does not rot.
If you are not yet sure a single server is the right home, where to run your agents weighs a laptop, a VPS, serverless, and managed Kubernetes side by side. This guide assumes you have decided on a server and want to do it well.
Step 1: pick and size a VPS
The first real decision in how to host an AI agent on a server is the box itself, and it is easy to overspend here. Most agents are memory-hungry and CPU-spiky. They idle at almost nothing, then spike hard when they run a tool, spawn a subprocess, or check out a repo. That shape means you size for RAM first and burst CPU second. A chat-style agent that mostly proxies model calls is happy on 1 to 2 GB. A coding agent that clones repos, runs tests, and holds a workspace wants 4 GB and 2 vCPU before it stops swapping.
Real tiers, with real prices:
| Server | vCPU / RAM / Disk | ~Price/mo | Workload it fits |
|---|---|---|---|
| Hetzner CX22 | 2 vCPU / 4 GB / 40 GB | $4 to $6 | A single coding agent with a real /workspace |
| DigitalOcean basic | 1 vCPU / 1 GB / 25 GB | $6 | A tiny always-on bot, no heavy subprocesses |
| DigitalOcean basic | 1 vCPU / 2 GB / 50 GB | $12 | One light agent plus a small database |
| Hetzner CX32 | 4 vCPU / 8 GB / 80 GB | $10 to $13 | Two or three agents, parallel tool calls |
Hetzner Cloud (https://www.hetzner.com/cloud) gives you the most RAM per dollar in this range, which is why the CX22 is the default recommendation for a first agent. DigitalOcean (https://www.digitalocean.com/pricing/droplets) costs a bit more but has data centers in more regions and a friendlier console if you have never touched a server before. Either is fine. Start smaller than you think, because resizing up later takes about two minutes and resizing down is rare.
One rule regardless of provider: create a non-root user, put your SSH key on it, and disable password login before you do anything else. An exposed agent on a root-login box is a bad day waiting to happen.
Step 2: Dockerize the agent
Running the agent in a container is what makes "works on my machine" turn into "works on the server." You pin a base image, install dependencies once, and get an artifact that behaves the same in testing and production. The Docker docs (https://docs.docker.com/) cover install per distro; on a fresh Ubuntu box it is a two-line setup.
Here is a Dockerfile for a typical Python agent. Adjust the language and entrypoint to whatever you actually run.
# Dockerfile
FROM python:3.12-slim
# Never run the agent as root inside the container.
RUN adduser --disabled-password --gecos "" agent
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER agent
# The agent reads its API key from the environment at runtime,
# so nothing secret is baked into the image layers.
EXPOSE 8080
CMD ["python", "-m", "myagent", "--serve", "--port", "8080"]# Dockerfile
FROM python:3.12-slim
# Never run the agent as root inside the container.
RUN adduser --disabled-password --gecos "" agent
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER agent
# The agent reads its API key from the environment at runtime,
# so nothing secret is baked into the image layers.
EXPOSE 8080
CMD ["python", "-m", "myagent", "--serve", "--port", "8080"]Build it, tag it with a real version (not latest), and run it bound to localhost so it is not exposed to the whole internet before you have put TLS in front:
docker build -t myagent:0.1.0 .
docker run -d --name myagent \
--restart unless-stopped \
-v /srv/myagent/workspace:/workspace \
--env-file /etc/myagent/agent.env \
-p 127.0.0.1:8080:8080 \
myagent:0.1.0docker build -t myagent:0.1.0 .
docker run -d --name myagent \
--restart unless-stopped \
-v /srv/myagent/workspace:/workspace \
--env-file /etc/myagent/agent.env \
-p 127.0.0.1:8080:8080 \
myagent:0.1.0The -v flag mounts a persistent volume so the agent's workspace survives a container rebuild. That mount is the difference between an agent that remembers its work and one that starts from zero every deploy.
Step 3: keep it alive with systemd
--restart unless-stopped handles a plain crash, but it does not survive a host reboot cleanly on every setup, and it gives you no clean start/stop/status controls. Handing the container's lifecycle to systemd fixes both. systemd is already running on your VPS, it restarts the service on failure, and it starts it on boot without any extra tooling.
Create a unit file:
# /etc/systemd/system/myagent.service
[Unit]
Description=My AI agent
After=docker.service
Requires=docker.service
[Service]
Restart=always
RestartSec=5
# Clean up a stale container from a previous run, ignore errors if none.
ExecStartPre=-/usr/bin/docker rm -f myagent
ExecStart=/usr/bin/docker run --rm --name myagent \
--env-file /etc/myagent/agent.env \
-v /srv/myagent/workspace:/workspace \
-p 127.0.0.1:8080:8080 \
myagent:0.1.0
ExecStop=/usr/bin/docker stop myagent
[Install]
WantedBy=multi-user.target# /etc/systemd/system/myagent.service
[Unit]
Description=My AI agent
After=docker.service
Requires=docker.service
[Service]
Restart=always
RestartSec=5
# Clean up a stale container from a previous run, ignore errors if none.
ExecStartPre=-/usr/bin/docker rm -f myagent
ExecStart=/usr/bin/docker run --rm --name myagent \
--env-file /etc/myagent/agent.env \
-v /srv/myagent/workspace:/workspace \
-p 127.0.0.1:8080:8080 \
myagent:0.1.0
ExecStop=/usr/bin/docker stop myagent
[Install]
WantedBy=multi-user.targetThen enable and start it in one command, and tail the logs to confirm it came up:
sudo systemctl daemon-reload
sudo systemctl enable --now myagent
journalctl -u myagent -fsudo systemctl daemon-reload
sudo systemctl enable --now myagent
journalctl -u myagent -fenable --now does two jobs at once: it starts the service immediately and marks it to start on every boot. Restart=always with RestartSec=5 means that if the agent process dies, systemd brings it back five seconds later, logs the restart, and moves on. To prove it works, kill the container by hand and watch journalctl restart it. That test is worth the thirty seconds, because it is the whole reason you are using systemd instead of a shell script.
Step 4: secrets, logs, and updates
The container is alive. Now make it safe and maintainable, which is steps 4 through 6 of the plan.
Secrets
Keep keys out of your image and out of your shell history. The --env-file above points at a file you lock down:
# /etc/myagent/agent.env (chmod 600, owned by root)
ANTHROPIC_API_KEY=sk-ant-...
GITHUB_TOKEN=ghp_...# /etc/myagent/agent.env (chmod 600, owned by root)
ANTHROPIC_API_KEY=sk-ant-...
GITHUB_TOKEN=ghp_...sudo chmod 600 /etc/myagent/agent.env
sudo chown root:root /etc/myagent/agent.envsudo chmod 600 /etc/myagent/agent.env
sudo chown root:root /etc/myagent/agent.envThat is the floor, not the ceiling. A plaintext file readable by root is fine for one box and one person. The moment two people share the server, you want a real secrets store and an audit trail of who read what, and a flat file gives you neither.
Logs
journalctl -u myagent is enough while you are the only operator. Set a size cap so logs do not eat the disk, since a chatty agent can fill a 40 GB volume faster than you expect:
# /etc/systemd/journald.conf
[Journal]
SystemMaxUse=500M# /etc/systemd/journald.conf
[Journal]
SystemMaxUse=500MIf you run more than one agent, forward logs off the box to something searchable so you are not SSHing into three servers to trace one bug.
Updates
A pinned image is correct, but a pinned image is also a frozen one. You have to move it forward on purpose:
Watch the upstream project for new releases.
Build or pull the new tag and test it on a staging box.
Bump the tag in the systemd unit.
sudo systemctl daemon-reload && sudo systemctl restart myagent.Confirm health in
journalctl, and keep the previous tag around so a rollback is one edit away.
Do this monthly at least. Skipping it is how a six-month-old agent image quietly accumulates unpatched dependencies.
When to stop self-hosting
Everything above is what it truly takes to know how to host an AI agent on a server: a few hours of setup and then a steady drip of maintenance. Patch the OS, renew the certificate, rotate the keys, watch the disk, test the update, repeat. For one agent on one box, that drip is small and completely reasonable. Self-hosting is the honest right answer when you want full control over the data, when the network is air-gapped, or when the whole point is to learn how the pieces fit.
That is worth saying plainly, because the DIY path gets a bad reputation it does not fully deserve. A single agent on a well-configured Hetzner CX22, with the systemd unit above and a locked-down env file, will run for months with almost no attention. The setup is a good afternoon of work and it teaches you exactly what every layer does. If you are running one agent and you value control, stop reading and go build it.
The math changes when the count grows. Three agents across three servers means three sets of the same chores, three restart stories, three TLS certs, and three places a secret can leak. At that point the per-agent overhead you shrugged off starts to dominate your week. That is the moment a managed runtime earns its price: it takes the pinned image, the persistent workspace, a private terminal only you can reach, and the automatic restart-on-crash you just built by hand and makes them the default. With Sokko you launch one from the dashboard in a couple of clicks and skip every chore in this guide. If you live in the terminal, the same launch is one command:
sokko instances create --runtime openclaw --name nightly-refactor \
--byok --provider anthropic --api-key sk-ant-...
sokko instances lssokko instances create --runtime openclaw --name nightly-refactor \
--byok --provider anthropic --api-key sk-ant-...
sokko instances lsIf you want to see that tradeoff priced out in dollars and hours, managed vs self-hosting OpenClaw compares the two head to head, and deploying your first agent walks the managed path from zero. Self-host first if you want to understand the machinery. Move to managed when the machinery stops being the interesting part of your job.