Skip to content

How to Deploy Graylog on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Graylog 7 with MongoDB and the Graylog Data Node on a VPS using the official Docker Compose files. Covers generated secrets, the first-start preflight setup, a GELF input, a test message and HTTPS through Caddy.

Before you start
  • A VPS with 8 GB RAM (4 GB is the practical floor for a test install)
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server
  • Docker Engine + Compose and git installed (see the base guide below)
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Graylog is

Graylog is a centralized log management server. Logs arrive through inputs: syslog, GELF (Graylog's structured JSON format), Beats, or raw TCP/UDP. Graylog parses them with extractors and pipeline rules, routes them into streams, and lets you search, build dashboards and alert on what it finds. It is a Java application with three moving parts:

  • Graylog server: the web UI, the REST API, and message processing.
  • MongoDB: configuration only (users, inputs, streams, dashboards).
  • Graylog Data Node: Graylog's managed OpenSearch, which stores and indexes the log messages themselves.

Graylog Open is licensed under the SSPL, not an OSI-approved open-source license. That matters if you plan to offer Graylog as a service to others, and not for running it for yourself. It is also the heaviest tool in this family. If you want logs, metrics and traces in one lighter binary, compare OpenObserve.

A caveat from Graylog itself: the README of the official Graylog2/docker-compose repository says the files are "meant for testing and demo purposes", "might be outdated, can change in incompatible ways", and "are not tested for production use cases". This guide uses them because they are the maintained, single-host starting point. Treat the result as a solid base you will harden, not a turnkey production cluster.

Server sizing

  • RAM: our catalog lists 8 GB as the minimum for a real deployment. The Data Node runs OpenSearch, which is where the memory goes. In our run for this guide (GCP e2-standard-2 with 8 GB, Ubuntu 26.04, September 2026), once initial configuration was complete and memory had levelled off (a 10-minute plateau), docker stats showed about 2.2 GB for the Data Node, 610 MB for Graylog and 100 MB for MongoDB: ~3.0 GB in total before any real log volume. An earlier install check recorded only about 706 MB, taken before setup while OpenSearch was not yet running, so don't size from it.
  • Disk: log data is the growth driver. The images alone took about 5.5 GB in our install check. Start with 60 GB+ of SSD and set index retention early.
  • Kernel setting: OpenSearch needs vm.max_map_count of at least 262144. The prepare step below checks it.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed. If not, work through Docker & Compose on Ubuntu first. Open SSH and the reverse-proxy ports:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose

Check vm.max_map_count, and raise it persistently only if it's below what OpenSearch needs. Recent Ubuntu releases already ship a higher default:

current=$(sysctl -n vm.max_map_count)
echo "vm.max_map_count is $current"
if [ "$current" -lt 262144 ]; then
  echo "vm.max_map_count=262144" | sudo tee /etc/sysctl.d/99-graylog.conf
  sudo sysctl --system >/dev/null
fi
sysctl vm.max_map_count
Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Get the official compose files

Clone the repository. The single-node, open-license stack lives in open-core/:

cd ~
[ -d ~/graylog ] || git clone https://github.com/Graylog2/docker-compose ~/graylog
cd ~/graylog/open-core
grep -n 'image:' docker-compose.yml

It pins mongo:7.0, graylog/graylog-datanode:7.1 and graylog/graylog:7.1. Every port is already published on 127.0.0.1 only: the web UI and API on 9000, plus Beats, syslog, raw and GELF inputs. Nothing is reachable from outside until you choose to open it.

Generate the secrets

The stack refuses to start without two values in .env:

  • GRAYLOG_PASSWORD_SECRET peppers stored passwords and encrypts secrets in the database. The .env.example says to use at least 64 characters, and that changing it later invalidates sessions and encrypted values. Generate it once and back it up.
  • GRAYLOG_ROOT_PASSWORD_SHA2 is the SHA-256 hash of the built-in admin user's password. Per the example file, this password can't be changed from the UI or the API, only here.
cd ~/graylog/open-core
if [ ! -f .env ]; then
  openssl rand -base64 18 > admin-password
  chmod 600 admin-password
  printf 'GRAYLOG_PASSWORD_SECRET=%s\nGRAYLOG_ROOT_PASSWORD_SHA2=%s\n' \
    "$(openssl rand -hex 48)" \
    "$(tr -d '\n' < admin-password | sha256sum | cut -c1-64)" > .env
fi
chmod 600 .env
cut -d= -f1 .env

The admin password is in ~/graylog/open-core/admin-password. The secret is 96 hex characters.

Also tell Graylog its public URL, since the compose file hard-codes http://localhost:9000/. Use your real domain:

cd ~/graylog/open-core
sed -i 's|GRAYLOG_HTTP_EXTERNAL_URI: "http://localhost:9000/"|GRAYLOG_HTTP_EXTERNAL_URI: "https://logs.example.com/"|' docker-compose.yml
grep -n GRAYLOG_HTTP_EXTERNAL_URI docker-compose.yml

Start the stack

cd ~/graylog/open-core
docker compose up -d
for i in $(seq 1 60); do
  docker compose logs graylog 2>&1 | grep -q "Initial configuration is accessible" && break
  sleep 5
done
docker compose ps

First start: the preflight setup

With the Data Node, a fresh Graylog does not start fully on first boot. It starts a setup interface ("preflight") on port 9000, protected by a one-time password it prints to its log. In that interface you create a certificate authority and provision a TLS certificate for the Data Node. After that, Graylog restarts in normal mode. Until then, there is no search UI and no way to use the admin password you generated.

Find the one-time credentials in the log:

cd ~/graylog/open-core
docker compose logs graylog 2>&1 | grep "Initial configuration is accessible" | sed "s/password '.*'/password '<shown in your log>'/"

Run it without the sed to see the actual password. Then tunnel to the loopback port from your own machine and open http://localhost:9000:

ssh -L 9000:127.0.0.1:9000 deploy@SERVER_IP

Log in as admin with the one-time password, create a new CA, accept or adjust the certificate renewal policy, provision certificates for the detected Data Node, and resume startup.

If you'd rather stay in the terminal, the same four steps can be driven with the HTTP calls the setup page makes. These are what Graylog 7.1's preflight page sends. They aren't a documented public API, so prefer the UI if a later version behaves differently:

cd ~/graylog/open-core
PRE=$(docker compose logs graylog 2>&1 | grep -o "password '[^']*'" | head -1 | cut -d"'" -f2)
api() { curl -s -u "admin:$PRE" -H 'X-Requested-By: cli' -H 'Content-Type: application/json' "$@"; }
api -X POST http://127.0.0.1:9000/api/ca/create -d '{"organization":"Graylog CA"}'; echo
api -X POST http://127.0.0.1:9000/api/renewal_policy -d '{"mode":"AUTOMATIC","certificate_lifetime":"P30D"}'
api -X POST http://127.0.0.1:9000/api/generate
for i in $(seq 1 30); do
  api http://127.0.0.1:9000/api/data_nodes | grep -q '"status":"CONNECTED"' && break
  sleep 5
done
api http://127.0.0.1:9000/api/data_nodes; echo
api -X POST http://127.0.0.1:9000/api/status/finish-config; echo

{"result":"FINISHED"} ends preflight. Graylog restarts into normal mode, and from then on admin uses the password you generated. Wait for it to report alive:

cd ~/graylog/open-core
for i in $(seq 1 60); do
  [ "$(curl -s -o /dev/null -w '%{http_code}' -u "admin:$(tr -d '\n' < admin-password)" http://127.0.0.1:9000/api/system/lbstatus)" = 200 ] && break
  sleep 5
done
curl -s -u "admin:$(tr -d '\n' < admin-password)" http://127.0.0.1:9000/api/system | grep -o '"version":"[^"]*"\|"lifecycle":"[^"]*"'

Send a test message

Graylog does nothing until an input exists. Create a global GELF UDP input on port 12201, which the compose file already publishes on 127.0.0.1. In the UI this is System → Inputs. From the shell, it's one call to the REST API:

cd ~/graylog/open-core
AUTH="admin:$(tr -d '\n' < admin-password)"
curl -s -u "$AUTH" -H 'X-Requested-By: cli' -H 'Content-Type: application/json' \
  -X POST http://127.0.0.1:9000/api/system/inputs \
  -d '{"title":"GELF UDP","type":"org.graylog2.inputs.gelf.udp.GELFUDPInput","global":true,"configuration":{"bind_address":"0.0.0.0","port":12201}}'
echo
sleep 5
echo -n '{"version":"1.1","host":"vps","short_message":"hello from the VPS"}' | nc -u -w1 127.0.0.1 12201
sleep 15
curl -s -u "$AUTH" -H 'Accept: application/json' \
  "http://127.0.0.1:9000/api/search/universal/relative?query=hello&range=600&fields=message,source" \
  | grep -o '"total_results":[0-9]*'

"total_results":1 means the message went in through the input, was indexed by the Data Node, and came back from search.

HTTPS + domain

Point an A record for logs.example.com at the server and put Caddy in front of the loopback port:

logs.example.com {
    reverse_proxy 127.0.0.1:9000
}

The GRAYLOG_HTTP_EXTERNAL_URI you set earlier must match this address, or the web interface will build its links for the wrong host.

Getting logs in from other machines. The inputs are bound to loopback, so only senders on this box can reach them. For remote senders, change the relevant 127.0.0.1: port mapping in docker-compose.yml (for example 12201:12201/udp), recreate the container, and then restrict that port with your provider's cloud firewall. Docker-published ports bypass ufw. Neither GELF UDP nor syslog UDP has authentication, so never leave them open to the world.

Backups

MongoDB holds the configuration: users, inputs, streams, pipelines and dashboards. Dump it with the stack running:

cd ~/graylog/open-core
docker compose exec -T mongodb mongodump --db graylog --archive --gzip > graylog-mongo-$(date +%F).archive.gz
ls -lh graylog-mongo-*.archive.gz

Keep .env (without the password secret, restored encrypted values become unreadable) and admin-password with it, off the box. The log messages themselves live in the graylog-datanode volume. For those, index retention and rotation (System → Indices) matter more than backups on a single VPS. If you need copies, archive the stopped volume or use OpenSearch snapshots to external storage.

Upgrades

Change the Graylog and Data Node tags together, since their versions must match. Take a MongoDB dump, then:

cd ~/graylog/open-core
docker compose pull
docker compose up -d

Read Graylog's upgrade notes before moving between major versions. Because upstream says the compose files "can change in incompatible ways", review git diff after a git pull in ~/graylog rather than applying it blind.

Troubleshooting

Port 9000 asks for a password and the generated one fails. You are still in preflight. Use the one-time password from the log, not your own.

Preflight never sees the Data Node. Check docker compose logs datanode. The usual causes are a vm.max_map_count below 262144, or GRAYLOG_PASSWORD_SECRET differing between the two services. The compose file passes the same .env value to both, and the comments say it must match.

Containers restart or the host swaps. OpenSearch needs its memory. See the sizing section and check docker stats --no-stream.

Search shows nothing. Confirm the input is running (System → Inputs), the sender uses the right protocol (GELF UDP vs TCP), and the time range covers the message's timestamp.

Verification + next steps

You're done when https://logs.example.com loads over a valid certificate, you can sign in as admin with your generated password, the test message is searchable, and a MongoDB dump sits off the box.

Next: set index rotation and retention under System → Indices, add a syslog or GELF input for your real hosts, and create a non-admin user for daily use. For error tracking rather than raw logs, see GlitchTip. For metrics, see Prometheus.

Next steps

How to self-host Graylog →More self-hosted observability tools →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.