Use case · Log management

Graylog server on a dedicated machine: sizing, Compose, inputs and backups

A self-hosted Graylog server is three services: Graylog, MongoDB for configuration and metadata, and a search backend for the messages, which on Graylog 7.2 is Graylog Data Node or your own OpenSearch. Graylog's smallest production layout gives Graylog plus MongoDB 16 GB of RAM on 8 cores, and the Data Node 24 GB on 8 more.

See the machines

The three parts and the versions they must match

The Graylog server receives and processes messages and serves the web interface and REST API on port 9000. MongoDB holds users, streams, dashboards and alert definitions, never the messages. Those live in the search backend: Graylog Data Node, which runs OpenSearch for Graylog and issues the certificates between nodes, or an OpenSearch cluster you manage. Graylog recommends Data Node.

ComponentGraylog 7.2 requirementSource
MongoDB7.x minimum, 8.2.x maximumCompatibility matrix
Self-managed OpenSearch1.1.x to 3.7.x; 3.x only without indices made before OpenSearch 2.0Compatibility matrix
OpenSearch 1.x and ElasticsearchDeprecated since 7.0, to be removed in 8.0Upgrade to Graylog 7.0
Java21, shipped inside the packages and imagesUpgrade to Graylog 7.0
Packaged operating systemsUbuntu 22.04 and 24.04, Debian 12, RHEL/Alma/Rocky 8 and 9, SLES 15Compatibility matrix
Kernel settingvm.max_map_count at least 262144Run Graylog in Docker

Sizing CPU, memory and disk

Graylog's Core layout puts Graylog and MongoDB on one server and the Data Node on a second. The documentation pitches it at small production estates and labs, and warns that it has no fault tolerance: if either node goes offline, ingestion and search stop.

ItemGraylog figureWhere it comes from
Graylog + MongoDB node8 CPU cores, 16 GB RAMCore architecture diagram
Data Node8 CPU cores, 24 GB RAMCore architecture diagram
Data Node heapHalf the RAM, at most 31 GBopensearch_heap, Ubuntu guide
Graylog heapHalf the RAM, at most 16 GB-Xms/-Xmx, Ubuntu guide
Message journalDefault 5 GB or 12 hours; plan 3 to 5 times daily ingest, on SSDArchitecture page
Index storageDaily ingest x retention days x 120%Architecture page

Our arithmetic, not Graylog's: 20 GB a day kept 30 days is 20 x 30 x 1.2 = 720 GB of index, plus 60 to 100 GB of journal. Both Core roles on one box sum to 16 cores and 40 GB, and the two heaps then share that RAM with MongoDB, so neither can take half. The Ubuntu guide strongly recommends XFS.

Which of our machines fit

Every message is written to the journal and then indexed, so local SSDs matter more than clock speed. On our dedicated server range the dual-socket lines match the Core figures:

  • 2 x Xeon E5-2630L v3, 16 cores, memory options to 1 TB: the summed Core figures on one box with 48 GB or more (our margin over 40 GB).
  • 2 x Xeon Gold 6130, 32 cores: room for heavy pipeline rules, or two machines of one line for the two Core roles.
  • Bigger index sets: a build to order with up to 24 drive bays in RAID 10. A build to order takes 4 to 24 hours.

Every racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC) for the evening a bad ufw rule leaves sshd unreachable. Racked machines are handed over in about 30 minutes once payment is approved, and the port is unmetered, so inbound log volume carries no transfer bill. Stay on the Intel and AMD lines: the X-Gene ARM cartridge boots only Ubuntu 16.04 and 18.04.

Installing with Docker Compose

Begin on Ubuntu 24.04; install Docker Engine and the Compose plugin from Docker's apt repository. Graylog's Docker guide uses the open-core example from Graylog2/docker-compose, whose README calls the files testing and demo material not tested for production. As root, the block enables ufw with SSH allowed, sets vm.max_map_count, halts on Docker Engine below 28 (before which a 127.0.0.1 mapping is not reliably local-only), on an existing .env, or on any port not bound to 127.0.0.1, then writes both secrets to a mode-600 .env and starts the stack. The example's restart: "on-failure" leaves containers down after a reboot; the inputs block fixes that.

(
set -euo pipefail
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH && ufw --force enable
echo 'vm.max_map_count=262144' > /etc/sysctl.d/99-graylog.conf
sysctl --system >/dev/null
v=$(docker version -f '{{.Server.Version}}')
[ "${v%%.*}" -ge 28 ] || { echo "STOP - Docker Engine 28+ needed"; exit 1; }
[ ! -e /opt/graylog/.env ] || { echo "STOP - .env exists, keeping it"; exit 1; }
mkdir -p /opt/graylog && cd /opt/graylog
curl -fsSo docker-compose.yml https://raw.githubusercontent.com/Graylog2/docker-compose/main/open-core/docker-compose.yml
if grep -E '^ *- "([0-9]+:|0\.0\.0\.0|\[)' docker-compose.yml; then
  echo "STOP - a port above is not on 127.0.0.1"; exit 1; fi
s=$(openssl rand -hex 48); [ ${#s} -eq 96 ]
read -rsp 'Graylog admin password (12+ chars): ' p; echo
[ ${#p} -ge 12 ] || { echo "STOP - too short"; exit 1; }
h=$(printf %s "$p" | sha256sum | cut -d' ' -f1)
install -m 600 /dev/null .env
printf 'GRAYLOG_PASSWORD_SECRET="%s"\nGRAYLOG_ROOT_PASSWORD_SHA2="%s"\n' "$s" "$h" > .env
docker compose -f /opt/graylog/docker-compose.yml up -d
)

Every change from the official steps: we write .env instead of copying .env.example; the secret comes from openssl rand -hex 48 (96 characters; the example file asks for at least 64) instead of the guide's /dev/urandom pipeline; and read -s takes the admin password, which the guide's one-liner echoes to the screen. Never change the secret later: the example file warns it invalidates sessions and encrypted values in the database.

First login over an SSH tunnel

On first start Graylog 7 runs a preflight interface with a one-time password in its log, still on loopback only. Finish it from your workstation:

  1. On the server, read the credentials: docker compose -f /opt/graylog/docker-compose.yml logs graylog | grep "Initial configuration".
  2. On your workstation, tunnel with your own login: ssh -N -L 9000:127.0.0.1:9000 you@your-server.
  3. Browse to http://localhost:9000 (the address the Compose file gives Graylog as its external URI), log in to admin using the preflight password, create the certificate authority and provision the Data Node.
  4. Once Graylog is up, the admin login takes the password typed during setup. It can only be changed in .env: write a new hash, then run docker compose -f /opt/graylog/docker-compose.yml up -d --force-recreate.
  5. Create named accounts for the team and keep admin for recovery.

We leave port 9000 on loopback permanently. Graylog's Ubuntu guide says it configures no security and that internal services must not face the internet; the FAQ covers an HTTPS proxy if the team needs a URL.

Opening syslog and GELF inputs to named senders

Inputs are listeners created in the web interface under System, then Inputs: syslog on 5140, GELF on 12201, Beats on 5044. The Compose file publishes them on 127.0.0.1 only, and a port Docker publishes on IPv4 bypasses ufw. The block loads a filter into Docker's DOCKER-USER chain that passes only addresses listed in /etc/graylog-senders, re-applies it whenever Docker starts, and only then publishes 5140 and 12201 on all interfaces. Its graylog-stack.service runs docker compose up -d at boot and after each Docker restart, once the filter is in place; restart: unless-stopped was rejected because Docker would start the containers ahead of the filter.

(
set -euo pipefail
touch /etc/graylog-senders
cat > /usr/local/sbin/graylog-inputs.sh <<'EOF'
set -eu
EXT=$(ip -o -4 route get 1.1.1.1 | sed -n 's/.* dev \([^ ]*\).*/\1/p')
[ -n "$EXT" ]
iptables -N GRAYLOG-IN 2>/dev/null || true
iptables -F GRAYLOG-IN && iptables -A GRAYLOG-IN -j DROP
for s in $(grep -Ev '^ *(#|$)' /etc/graylog-senders); do
  iptables -I GRAYLOG-IN -s "$s" -j RETURN; done
for p in 5140 12201; do for t in tcp udp; do
  r="-i $EXT -p $t -m conntrack --ctorigdstport $p -j GRAYLOG-IN"
  iptables -C DOCKER-USER $r 2>/dev/null || iptables -I DOCKER-USER $r
done; done
EOF
S=/etc/systemd/system; O='[Service]\nType=oneshot\nRemainAfterExit=yes\n'
printf "[Unit]\nAfter=docker.service\nRequires=docker.service\nPartOf=docker.service\n$O%s\n[Install]\nWantedBy=docker.service\n" \
  'ExecStart=/bin/bash /usr/local/sbin/graylog-inputs.sh' > $S/graylog-inputs.service
printf "[Unit]\nAfter=graylog-inputs.service\nRequires=graylog-inputs.service\n$O%s\n[Install]\nWantedBy=multi-user.target\n" \
  'ExecStart=/usr/bin/docker compose -f /opt/graylog/docker-compose.yml up -d' > $S/graylog-stack.service
systemctl daemon-reload && systemctl enable --now graylog-inputs && systemctl enable graylog-stack
grep -q GRAYLOG-IN <<<"$(iptables -S DOCKER-USER)" || { echo "STOP - filter not loaded"; exit 1; }
sed -i -E 's/"127\.0\.0\.1:(5140|12201):/"\1:/' /opt/graylog/docker-compose.yml
systemctl restart graylog-stack
)

List one IPv4 address or CIDR per line in /etc/graylog-senders, then systemctl restart graylog-inputs; an empty file drops everything. The filter matches packets arriving on the interface that holds the default route; if logs reach the server through a second interface, add rules naming it. No ufw rule is added, so on IPv6, where Docker's default setup hands published ports to a proxy process that ufw filters, these ports stay shut; with Docker's own IPv6 networking on, add matching ip6tables rules. Graylog notes that TCP inputs are acknowledged at the network level and UDP has no delivery guarantee, and recommends TLS on inputs.

Backups that restore

Graylog's backup guide wants configuration, the MongoDB database and the indexed messages kept together, restored to one point in time on the same versions. On one machine a cold copy does that. The backup script stops the stack, archives /opt/graylog with its .env, every file this page creates and all four volumes, verifies the archive and restarts the stack even after a failure; given prune, it then deletes archives over six days old. It lists the inputs block's files, so run that block first or cut them from its tar line. The restore script checks that the archive holds all four volumes, backs up the current state, empties the volumes, extracts, resets the Graylog volume to uid 1100 (its image's user), reloads the filter and starts the stack.

cat > /usr/local/sbin/graylog-backup.sh <<'EOF'
set -euo pipefail; umask 077; cd /
C=/opt/graylog/docker-compose.yml; B=/var/backups/graylog; f=$B/graylog-$(date +%F-%H%M%S).tar.gz
mkdir -p -m 700 "$B"
trap 'docker compose -f "$C" start' EXIT
docker compose -f "$C" stop
tar --numeric-owner -czf "$f.part" opt/graylog etc/graylog-senders etc/sysctl.d/99-graylog.conf \
  etc/cron.d/graylog-backup etc/systemd/system/graylog-{inputs,stack}.service usr/local/sbin/graylog-*.sh \
  var/lib/docker/volumes/graylog_{mongodb_data,mongodb_config,graylog-datanode,graylog_data}
tar -tzf "$f.part" >/dev/null; mv "$f.part" "$f"; echo "ok $f"
[ "${1:-}" != prune ] || find "$B" -name 'graylog-*.tar.gz' -mtime +6 -delete
EOF
cat > /usr/local/sbin/graylog-restore.sh <<'EOF'
set -euo pipefail
F=${1:?}; D=/var/lib/docker/volumes; L=$(tar -tzf "$F")
V="mongodb_data mongodb_config graylog-datanode graylog_data"
for v in $V; do [ -d "$D/graylog_$v/_data" ]
  grep -q "^var/lib/docker/volumes/graylog_$v/_data/" <<<"$L"; done
bash /usr/local/sbin/graylog-backup.sh
docker compose -f /opt/graylog/docker-compose.yml down
for v in $V; do find "$D/graylog_$v/_data" -mindepth 1 -delete; done
tar -C / --numeric-owner -xzf "$F"
chown -R 1100:1100 "$D/graylog_graylog_data/_data"
systemctl daemon-reload && sysctl --system >/dev/null
systemctl enable graylog-inputs graylog-stack && systemctl restart graylog-inputs
grep -q GRAYLOG-IN <<<"$(iptables -S DOCKER-USER)"
systemctl restart graylog-stack
EOF

Schedule it with echo '30 3 * * * root bash /usr/local/sbin/graylog-backup.sh prune >/var/log/graylog-backup.log 2>&1' > /etc/cron.d/graylog-backup, then test with bash /usr/local/sbin/graylog-backup.sh and look for ok. To restore, give the archive to the restore script, as root: bash /usr/local/sbin/graylog-restore.sh /var/backups/graylog/graylog-2026-10-31-033000.tar.gz, substituting your archive's name; a failed check stops it before any volume is emptied. To undo a restore, run it again on the archive it made first. On a rebuilt server, run the install, inputs and backup blocks first, then restore. UDP sent during the stop is lost. Ship each archive to another host using rsync -a, leaving out --delete. Once the index is too big to copy nightly, move to the snapshot method in Graylog's backup guide.

Updates, the SSPL licence and the limits

Patch releases come through the 7.2 tags: back up, run docker compose -f /opt/graylog/docker-compose.yml pull, and after it up -d against the same file. A new minor or major version means new image tags, after reading its upgrade notes; 7.0 raised the MongoDB floor to 7.0.

Graylog is published under the Server Side Public License, version 1 (LICENSE in Graylog2/graylog2-server; the docker-compose README agrees), while the Compose files and image sources are Apache 2.0. Our general reading, not legal advice: the SSPL is copyleft, so handing copies to others, modified or not, means handing over the source; its section 13 adds that offering the program's functionality to third parties as a service obliges you to publish the source of the whole service stack, management, monitoring, backup and hosting software included. Collecting your own organisation's logs is ordinary use; selling hosted Graylog is one for a lawyer.

One box is a single point of failure; for high availability Graylog describes multi-server layouts. For metrics and alerting beside the logs, see Prometheus and Grafana and Zabbix; for a search cluster of your own, self-hosted OpenSearch.

Questions

Can Graylog, MongoDB and the Data Node share one server?

Yes. The example Compose file runs all three on one host, and Graylog's documentation describes it as a single-node deployment. Graylog's smallest production layout uses two machines, so add both sets of figures together and accept that nothing is redundant.

Does Graylog 7 still work with Elasticsearch?

Only as a deprecated option. Elasticsearch and OpenSearch 1.x are deprecated from Graylog 7.0 and due to be removed in 8.0. Use Graylog Data Node, or OpenSearch within the 7.2 matrix range of 1.1.x to 3.7.x.

How do I put the web interface on a domain with HTTPS?

Run a reverse proxy on the host on 443, passing to 127.0.0.1:9000 and sending an X-Graylog-Server-URL header with the public URL, as Graylog's proxy examples do. Allow 80 and 443 in ufw before requesting a certificate, and only after preflight is done and the admin password set.

Can network devices that only send to port 514 reach it?

Yes, with a different mapping. Keep the input on 5140 inside the container, map host port 514 to it (514:5140/udp) in the Compose file and put 514 in place of 5140 in the filter script, because the filter matches the port the sender addressed. Then run systemctl restart graylog-inputs followed by docker compose -f /opt/graylog/docker-compose.yml up -d.

Is Graylog Open free to run?

Graylog Open, the graylog/graylog image and graylog-server package, is published under the SSPL, which charges nothing to run it on hardware you control. The other two editions, Graylog Enterprise and Graylog Security, share one image, graylog/graylog-enterprise, and the graylog-enterprise package.

Size it, then pick the machine

Work out index and journal space from your daily volume, then configure a dual-socket server with the memory and SSDs that figure calls for.

See the machines

Related on Server Room

Other use cases