When your infrastructure grows beyond a handful of servers, logging into each one to check /var/log/syslog during an outage is not a strategy — it is a liability. Centralized logging solves this by funnelling every log line from every server, container, and application into a single searchable interface. Instead of guessing which box threw the error, you query once and get your answer in seconds.

Graylog is one of the strongest self-hosted options available in 2026, offering real-time search, alerting, dashboards, and pipeline processing without per-GB licensing fees. In this tutorial, we walk through a complete production Graylog deployment on Ubuntu 24.04 using Docker Compose — from initial setup through hardening, monitoring integration, and troubleshooting.

If you are running any workload that generates logs (and they all do), this guide will get you from zero to production-grade centralized logging in under an hour.

What You Will Need

Graylog runs three services — Graylog itself, OpenSearch for indexing and search, and MongoDB for configuration metadata. This stack is memory-hungry, so plan accordingly.

Resource Minimum (Dev/Staging) Recommended (Production)
CPU 4 cores 8+ cores
RAM 8 GB 16 GB+
Disk 50 GB NVMe 250+ GB NVMe
OS Ubuntu 24.04 LTS
Software Docker CE + Docker Compose plugin

A Cloud VPS works well for development, staging, or low-volume production environments. For teams ingesting more than a few GB of logs per day, or where retention requirements extend beyond 30 days, a Dedicated Server gives you the sustained I/O and memory headroom that OpenSearch demands under heavy indexing loads.

Architecture Overview

Before we start installing, it helps to understand how the three components interact. If you are familiar with the Grafana stack, this follows a similar pattern — a data store, an indexer, and a UI layer:

  • Graylog — the web UI, alerting engine, stream processor, and API. This is where you search logs, build dashboards, and configure inputs.
  • OpenSearch — the indexing and search backend. Every log message gets stored here. This is the component that needs the most RAM and disk.
  • MongoDB — stores Graylog’s configuration, user accounts, and stream definitions. Lightweight and low-maintenance.

Log sources (your servers, containers, applications) send messages to Graylog inputs over GELF, Syslog, or raw TCP/UDP. Graylog processes and routes them into OpenSearch indices.

Step 1: Prepare Ubuntu and Install Docker

Start with a fresh Ubuntu 24.04 server. Update packages and install Docker from the official repository:

sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw

# Add Docker GPG key and repository
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker

OpenSearch requires a higher virtual memory map limit. Set it now and make it persistent across reboots:

sudo sysctl -w vm.max_map_count=262144
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf

Step 2: Create the Working Directory and Secrets

sudo mkdir -p /opt/graylog
cd /opt/graylog

# Generate the password secret (min 16 chars, we use 96)
openssl rand -hex 48
# Save this output — you will need it in docker-compose.yml

# Generate the admin password hash
echo -n "YourStrongAdminPassword" | sha256sum
# Copy the hex string (without the trailing dash)

Keep both values safe. The password secret is used for encryption and must remain consistent across restarts. If you change it, existing sessions and encrypted values become invalid.

Step 3: Create the Docker Compose Stack

Create /opt/graylog/docker-compose.yml with the following content. Replace REPLACE_SECRET, REPLACE_HASH, and YOUR_SERVER_IP with your actual values:

services:
  mongodb:
    image: mongo:6
    restart: unless-stopped
    volumes:
      - ./mongo_data:/data/db

  opensearch:
    image: opensearchproject/opensearch:2.13.0
    restart: unless-stopped
    environment:
      discovery.type: single-node
      OPENSEARCH_JAVA_OPTS: "-Xms2g -Xmx2g"
      plugins.security.disabled: "true"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - ./os_data:/usr/share/opensearch/data

  graylog:
    image: graylog/graylog:6.0
    restart: unless-stopped
    depends_on:
      - mongodb
      - opensearch
    environment:
      GRAYLOG_PASSWORD_SECRET: "REPLACE_SECRET"
      GRAYLOG_ROOT_PASSWORD_SHA2: "REPLACE_HASH"
      GRAYLOG_HTTP_EXTERNAL_URI: "http://YOUR_SERVER_IP:9000/"
      GRAYLOG_MONGODB_URI: "mongodb://mongodb:27017/graylog"
      GRAYLOG_ELASTICSEARCH_HOSTS: "http://opensearch:9200"
    ports:
      - "9000:9000"       # Graylog web UI and API
      - "1514:1514"       # Syslog TCP
      - "1514:1514/udp"   # Syslog UDP
      - "12201:12201"     # GELF TCP
      - "12201:12201/udp" # GELF UDP

A note on the OpenSearch heap: -Xms2g -Xmx2g allocates 2 GB. On a 16 GB server, you can safely increase this to 4 GB. Never allocate more than 50% of total RAM to the JVM heap.

Step 4: Start the Stack and Verify

cd /opt/graylog
docker compose up -d

# Check all three containers are running
docker compose ps

# Watch Graylog logs for the startup sequence
docker compose logs -f graylog

Wait until you see Graylog server up and running in the logs. Then open http://YOUR_SERVER_IP:9000 in your browser and log in with username admin and the password you hashed in Step 2.

Step 5: Configure Inputs and Retention

Inputs define how Graylog receives log data. Navigate to System ? Inputs in the web UI.

Create a GELF UDP Input

GELF (Graylog Extended Log Format) is the native format and supports structured fields, which makes it the best choice for Docker and application logs:

  1. Select GELF UDP from the dropdown and click Launch new input
  2. Set the title to something descriptive like “GELF UDP — All Sources”
  3. Bind address: 0.0.0.0, Port: 12201
  4. Click Save

Create a Syslog UDP Input

For Linux servers sending via rsyslog or systemd-journal-upload:

  1. Select Syslog UDP, launch a new input
  2. Title: “Syslog UDP — Linux Servers”
  3. Bind address: 0.0.0.0, Port: 1514
  4. Click Save

Set Retention Policies

Under System ? Indices, configure the Default index set:

  • Rotation strategy: Index Time (rotate daily or weekly)
  • Retention strategy: Delete (remove oldest indices when limit is reached)
  • Max indices: 30 for a month of retention, adjust based on disk

Test Log Ingestion

Send a test message from any server that can reach your Graylog instance:

# Test GELF input
echo -e '{"version":"1.1","host":"test-server","short_message":"Graylog test message","level":6}' \
  | nc -u -w1 YOUR_SERVER_IP 12201

# Test Syslog input
logger --server YOUR_SERVER_IP --port 1514 --udp "Test syslog message from $(hostname)"

Check the Search page in Graylog — your test messages should appear within a few seconds.

Port Protocol Service Notes
9000 TCP Graylog Web UI / API Restrict to admin IPs in production
1514 TCP/UDP Syslog Input Restrict to source server IPs
12201 TCP/UDP GELF Input Restrict to source server IPs
9200 TCP OpenSearch API Internal only — do NOT expose
27017 TCP MongoDB Internal only — do NOT expose

Step 6: Production Hardening

The default Docker Compose setup gets you running, but it is not production-ready. Work through each of these before putting real traffic on the system.

Firewall Rules with UFW

Lock down access to only what is needed. Replace YOUR_ADMIN_IP and YOUR_SOURCE_CIDR with actual values:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH

# Admin UI — restrict to your IP
sudo ufw allow from YOUR_ADMIN_IP to any port 9000 proto tcp

# Log ingestion — restrict to source servers
sudo ufw allow from YOUR_SOURCE_CIDR to any port 1514
sudo ufw allow from YOUR_SOURCE_CIDR to any port 12201

sudo ufw enable

Important: Docker modifies iptables directly and can bypass UFW. To prevent this, add {"iptables": false} to /etc/docker/daemon.json and restart Docker — then manage port exposure strictly through UFW.

HTTPS with Nginx Reverse Proxy

Never expose the Graylog UI over plain HTTP in production. Set up nginx as a reverse proxy with Let’s Encrypt:

sudo apt -y install nginx certbot python3-certbot-nginx

# Create nginx config
sudo tee /etc/nginx/sites-available/graylog > /dev/null << 'EOF'
server {
    listen 80;
    server_name logs.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/graylog /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# Get SSL certificate
sudo certbot --nginx -d logs.yourdomain.com

After SSL is active, update GRAYLOG_HTTP_EXTERNAL_URI in your Compose file to https://logs.yourdomain.com/ and restart the Graylog container.

Backups and Log Rotation

MongoDB holds your Graylog configuration — dashboards, streams, alert rules, user accounts. Back it up regularly:

# Daily MongoDB backup via cron
0 3 * * * docker exec graylog-mongodb-1 mongodump --archive=/data/db/backup-$(date +\%F).gz --gzip 2>&1 | logger -t graylog-backup

For OpenSearch data, rely on index retention policies rather than filesystem backups — the indices are large and are better managed through Graylog's built-in rotation and retention settings.

Data Residency and Compliance

Logs frequently contain personally identifiable information — IP addresses, usernames, email addresses, and session tokens. If your organization operates under Canadian privacy regulations (PIPEDA, provincial health privacy acts) or pursues SOC 2 compliance, where your logs are stored matters. Hosting your Graylog instance on Canadian infrastructure ensures log data never crosses the border, simplifying your compliance posture considerably.

Step 7: Troubleshooting

These are the most common issues we see when teams deploy Graylog for the first time.

OpenSearch Fails to Start

Symptom: The opensearch container exits immediately or restart-loops. docker compose logs opensearch shows memory-related errors or max virtual memory areas vm.max_map_count [65530] is too low.

Cause: OpenSearch needs both adequate heap memory and a higher vm.max_map_count kernel setting.

Fix:

# Set vm.max_map_count (required for OpenSearch)
sudo sysctl -w vm.max_map_count=262144
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf

# Check current heap allocation in docker-compose.yml
# OPENSEARCH_JAVA_OPTS: "-Xms2g -Xmx2g"
# On 8 GB servers, use 2g. On 16 GB, use 4g. Never exceed 50% of total RAM.

# Verify data directory permissions
sudo chown -R 1000:1000 /opt/graylog/os_data

# Restart
docker compose up -d opensearch
docker compose logs -f opensearch

Graylog Web UI Not Loading

Symptom: Port 9000 is not responding, or you see a blank page. docker compose logs graylog may show MongoDB connection errors or port binding failures.

Cause: Usually one of three things — MongoDB is not ready yet, another service is using port 9000, or the GRAYLOG_HTTP_EXTERNAL_URI does not match your actual access URL.

Fix:

# Check if MongoDB is healthy
docker compose logs mongodb | tail -20

# Check for port conflicts
sudo ss -tlnp | grep 9000

# Verify external URI matches how you access Graylog
# If behind nginx proxy: GRAYLOG_HTTP_EXTERNAL_URI: "https://logs.yourdomain.com/"
# If direct access: GRAYLOG_HTTP_EXTERNAL_URI: "http://YOUR_IP:9000/"

# Restart in order
docker compose restart mongodb
docker compose restart graylog

Inputs Not Receiving Data

Symptom: Inputs show as running in Graylog, but no messages appear in search. The throughput counter stays at zero.

Cause: Firewall blocking the input ports, source agents sending in the wrong format, or the input is bound to the wrong address.

Fix:

# Test connectivity from a source server
nc -vz YOUR_GRAYLOG_IP 12201
nc -vzu YOUR_GRAYLOG_IP 12201

# Check UFW is allowing the port
sudo ufw status | grep 12201

# Send a test GELF message and watch for it
echo -e '{"version":"1.1","host":"debug","short_message":"test","level":6}' | nc -u -w1 localhost 12201

# Check Graylog's input metrics
curl -s -u admin:PASSWORD http://localhost:9000/api/system/metrics | python3 -m json.tool | grep -i input

# Common mistake: sending plain text to a GELF input. GELF expects JSON.
# Common mistake: sending GELF to the Syslog port or vice versa.

Monitoring Your Graylog Instance

Graylog itself needs monitoring — a self-hosted monitoring stack can track OpenSearch heap usage, index sizes, and ingestion rates. At minimum, set up alerts for:

  • Disk usage above 80% on the OpenSearch data volume
  • OpenSearch heap usage consistently above 75%
  • Journal uncommitted entries growing (indicates Graylog cannot keep up with ingestion)
  • MongoDB replication lag if you run a replica set

Conclusion

A properly deployed Graylog stack turns your scattered log files into a searchable, alertable operations platform. The key decisions that determine long-term success are OpenSearch heap sizing, disciplined retention policies, and locking down network access from day one.

For development and staging environments, or production setups ingesting under a few GB per day, a Cloud VPS provides the flexibility to scale resources as your log volume grows. For heavier workloads — multi-server fleets, compliance-driven retention windows, or high-throughput ingestion — a Dedicated Server ensures you have the sustained I/O and memory that OpenSearch needs.

Either way, hosting in Canada keeps your log data under Canadian jurisdiction and simplifies privacy compliance. And if you would rather not manage the stack yourself, CWH's managed support team can handle the deployment, monitoring, and maintenance so you can focus on what your logs are telling you.