DEV Community

Amaresh Pelleti
Amaresh Pelleti

Posted on Originally published at devtoolhub.com

Running Ollama in Production: systemd, TLS, and API Auth

Originally published on DevToolHub.

Ollama production deployment means three things the official docs don't cover: the default systemd unit runs Ollama unsandboxed, the reverse-proxy example in Ollama's own FAQ has no TLS and no auth, and there's no built-in authentication at all — anyone who reaches the port can call the API. None of that is a bug; it's just out of scope for a quickstart. This closes the gap with the actual hardening steps.

This is provider-agnostic — VPS, bare metal, home server, any Linux box.

Hardening the systemd Service Beyond the Default Unit

Ollama's installer creates a dedicated ollama system user instead of running as root, and the default unit looks like this:

[Unit]
Description=Ollama Service
After=network-online.target

[Service]
ExecStart=/usr/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="PATH=$PATH"

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Reasonable baseline, not hardened. Add these via sudo systemctl edit ollama:

[Service]
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ReadWritePaths=/usr/share/ollama/.ollama
Enter fullscreen mode Exit fullscreen mode

ProtectSystem=strict makes the filesystem read-only except API filesystems — ReadWritePaths carves out the one directory Ollama needs to write models into, or pulls fail silently. Apply incrementally and check systemctl status ollama after each addition.

Running Ollama in Production Behind Nginx

Ollama's FAQ gives the minimal baseline:

server {
    listen 80;
    server_name example.com;
    location / {
        proxy_pass http://localhost:11434;
        proxy_set_header Host localhost:11434;
    }
}
Enter fullscreen mode Exit fullscreen mode

HTTP-only, no access control. Leave OLLAMA_HOST at its loopback default rather than binding to 0.0.0.0 — nginx should be the only thing facing the network.

Adding TLS with Let's Encrypt

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
Enter fullscreen mode Exit fullscreen mode

certbot rewrites the server block for HTTPS and installs a renewal timer automatically.

The Real Blocker: No Built-In Auth

Every Ollama endpoint answers anyone who reaches the port. Auth has to live in nginx:

location / {
    if ($http_x_api_key != "your-long-random-key-here") {
        return 401;
    }
    proxy_pass http://127.0.0.1:11434;
    proxy_set_header Host localhost:11434;
}
Enter fullscreen mode Exit fullscreen mode

nginx's if is unreliable for anything beyond one simple comparison — fine here, but don't stack more if blocks expecting them to compose. For multiple keys, use auth_request instead.

Logs: journald, Not logrotate

Ollama logs to journald by default — logrotate doesn't apply. journalctl -e -u ollama tails logs; retention is /etc/systemd/journald.conf:

[Journal]
SystemMaxUse=500M
MaxRetentionSec=1month
Enter fullscreen mode Exit fullscreen mode

Set both — space caps alone let old quiet-period entries linger, age caps alone don't stop a sudden volume spike from filling disk.

Quick Summary:

  • Default systemd unit isn't sandboxed — add ProtectSystem=strict, PrivateTmp, NoNewPrivileges, ReadWritePaths
  • Keep OLLAMA_HOST on loopback; nginx faces the network
  • certbot --nginx handles TLS in two commands
  • Zero built-in auth — an X-API-Key check in nginx is the minimum fix
  • Logs go to journald, not a file — set SystemMaxUse and MaxRetentionSec instead of logrotate

Full breakdown at DevToolHub.

Top comments (2)

Collapse
 
sikamikanikobg profile image
Arsen Apostolov

the x-api-key check in nginx is the minimum, and i think that is the right call. i ran into the same thing on my box, the api was open on the port and nothing stopped anyone who found it.

the journald part is the one i would not skip. people set the space cap and forget the age cap, or the other way around, and one quiet week leaves you with a disk that looks fine but is full of old entries. i keep both, 500m and one month.

the readwritepaths thing is real too. i had model pulls fail silently once the filesystem went read-only.

i built a small dashboard that watches ollama and the gpu it runs on, github.com/sikamikanikobg/homelab-monitor, mostly because i wanted to see what the box does when i am not in front of it.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.