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:
- Select GELF UDP from the dropdown and click Launch new input
- Set the title to something descriptive like “GELF UDP — All Sources”
- Bind address:
0.0.0.0, Port:12201 - Click Save
Create a Syslog UDP Input
For Linux servers sending via rsyslog or systemd-journal-upload:
- Select Syslog UDP, launch a new input
- Title: “Syslog UDP — Linux Servers”
- Bind address:
0.0.0.0, Port:1514 - 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.
Be First to Comment