How to Deploy Graylog on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-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.
- 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)
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 statsshowed 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_countof 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
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_SECRETpeppers stored passwords and encrypts secrets in the database. The.env.examplesays 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_SHA2is the SHA-256 hash of the built-inadminuser'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.