Graylog is a strong fit when operations teams need one place to search application logs, security events, firewall messages, and platform audit trails without turning every troubleshooting session into a manual server-by-server hunt. In this guide we will deploy Graylog on a single Ubuntu host using Docker Compose, Traefik for HTTPS routing, OpenSearch for searchable log storage, and MongoDB for Graylog metadata. The goal is not a toy install: the stack uses persistent volumes, explicit secrets, health checks, network isolation, backup points, and verification commands that make the service supportable after the first successful login.
The deployment pattern works well for small and mid-sized internal platforms, lab environments that need production habits, and teams centralizing logs from Docker, Linux syslog, firewalls, or application shippers. For larger ingestion rates, the same architecture can be split across multiple OpenSearch nodes and dedicated Graylog servers, but starting with a clean single-node baseline makes later scaling much safer.
Architecture and flow overview
Traffic enters through Traefik on ports 80 and 443. Traefik terminates TLS, redirects HTTP to HTTPS, and forwards authenticated browser sessions and API calls to the Graylog web interface on the internal Docker network. Graylog stores stream definitions, dashboards, user accounts, and configuration state in MongoDB. Searchable event data lands in OpenSearch, where Graylog manages indices, retention, and query access. Log producers can send messages to Graylog inputs such as Beats, GELF, or Syslog after you expose only the input ports you actually use.
This separation matters operationally. If Traefik has a certificate issue, Graylog and its data stores can still be healthy. If OpenSearch is under disk pressure, MongoDB may still preserve configuration. If a future migration is needed, each persistent volume has a clear purpose and backup strategy.
Prerequisites
- Ubuntu 22.04 or 24.04 server with a static public IP or private DNS route.
- Docker Engine and the Docker Compose plugin installed.
- A DNS record such as
graylog.example.compointing at the host. - Ports 80 and 443 open to Traefik; log input ports should stay closed until needed.
- At least 4 CPU cores, 8 GB RAM, and fast SSD storage for a modest production node.
- A backup destination for Compose files, MongoDB data, OpenSearch snapshots, and Graylog journal data.
Step-by-step deployment
1) Create the project layout
Keep the deployment under one directory so configuration, secrets, and operational scripts are easy to audit. Restrict permissions before writing environment files.
sudo mkdir -p /opt/graylog/{data/opensearch,data/mongo,data/graylog-journal,traefik/acme,backups}
sudo chown -R $USER:$USER /opt/graylog
cd /opt/graylog
umask 077
touch .env docker-compose.yml traefik/traefik.yml
If the copy button is unavailable in your browser, manually select and copy the command block above.
2) Generate secrets and write environment values
Graylog requires a password secret and a SHA-256 hash of the initial admin password. Store the plaintext admin password in your password manager, not in Git or documentation.
GL_SECRET=$(openssl rand -base64 48 | tr -d '
')
ADMIN_PASSWORD='replace-with-a-long-password-from-your-vault'
ADMIN_SHA=$(printf "%s" "$ADMIN_PASSWORD" | sha256sum | awk '{print $1}')
cat > .env <<EOF
GRAYLOG_HOSTNAME=graylog.example.com
[email protected]
GRAYLOG_PASSWORD_SECRET=$GL_SECRET
GRAYLOG_ROOT_PASSWORD_SHA2=$ADMIN_SHA
OPENSEARCH_INITIAL_ADMIN_PASSWORD=replace-with-another-long-password
EOF
chmod 600 .env
If the copy button is unavailable in your browser, manually select and copy the command block above.
3) Add Traefik static configuration
Traefik handles ACME certificates and keeps the Graylog container off the public edge. The dashboard is intentionally disabled; expose it later only behind SSO or a VPN.
cat > traefik/traefik.yml <<'EOF'
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: ${TRAEFIK_EMAIL}
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false
EOF
chmod 600 traefik/traefik.yml
chmod 600 traefik/acme || true
If the copy button is unavailable in your browser, manually select and copy the command block above.
4) Create the Docker Compose stack
The stack pins service roles, adds basic health checks, and keeps all back-end services on an internal network. Only Traefik publishes web ports. OpenSearch disables demo security for a private single-node deployment; if you expose OpenSearch outside this network, replace that setting with a hardened security configuration.
cat > docker-compose.yml <<'EOF'
services:
traefik:
image: traefik:v3.1
restart: unless-stopped
command: --configFile=/etc/traefik/traefik.yml
env_file: .env
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/acme:/letsencrypt
networks: [edge]
mongo:
image: mongo:6.0
restart: unless-stopped
volumes:
- ./data/mongo:/data/db
networks: [graylog]
opensearch:
image: opensearchproject/opensearch:2.15.0
restart: unless-stopped
environment:
discovery.type: single-node
plugins.security.disabled: "true"
OPENSEARCH_JAVA_OPTS: "-Xms2g -Xmx2g"
bootstrap.memory_lock: "true"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- ./data/opensearch:/usr/share/opensearch/data
networks: [graylog]
graylog:
image: graylog/graylog:6.0
restart: unless-stopped
depends_on: [mongo, opensearch]
env_file: .env
environment:
GRAYLOG_PASSWORD_SECRET: ${GRAYLOG_PASSWORD_SECRET}
GRAYLOG_ROOT_PASSWORD_SHA2: ${GRAYLOG_ROOT_PASSWORD_SHA2}
GRAYLOG_HTTP_EXTERNAL_URI: https://${GRAYLOG_HOSTNAME}/
GRAYLOG_MONGODB_URI: mongodb://mongo:27017/graylog
GRAYLOG_ELASTICSEARCH_HOSTS: http://opensearch:9200
volumes:
- ./data/graylog-journal:/usr/share/graylog/data/journal
labels:
- traefik.enable=true
- traefik.http.routers.graylog.rule=Host(`${GRAYLOG_HOSTNAME}`)
- traefik.http.routers.graylog.entrypoints=websecure
- traefik.http.routers.graylog.tls.certresolver=letsencrypt
- traefik.http.services.graylog.loadbalancer.server.port=9000
networks: [edge, graylog]
networks:
edge:
graylog:
internal: true
EOF
If the copy button is unavailable in your browser, manually select and copy the command block above.
5) Tune the host and start services
OpenSearch needs virtual memory settings and enough file descriptors. Apply them before bootstrapping the stack, then watch the logs until Graylog reports that the web interface is available.
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-graylog-opensearch.conf
sudo sysctl --system
docker compose pull
docker compose up -d
docker compose logs -f --tail=80 graylog opensearch traefik
If the copy button is unavailable in your browser, manually select and copy the command block above.
Configuration and secrets handling best practices
Do not commit .env, ACME storage, MongoDB files, OpenSearch data, or Graylog journals. Keep the initial admin password in a vault and rotate it after the first login. For team access, create named users or connect an identity provider rather than sharing the root account. When you begin accepting logs, expose only required input ports and document which firewall rule maps to which Graylog input.
For retention, start with conservative index settings. A common pattern is daily indices with a size cap and a retention window that matches compliance needs. Alert before disk reaches 75 percent, because OpenSearch can become read-only under pressure. If logs are business-critical, configure OpenSearch snapshots to S3-compatible storage and separately back up MongoDB so dashboards, streams, extractors, and users are recoverable.
Verification checklist
Run checks from both the host and an external workstation. Verify TLS, container health, application login, and storage availability before handing the service to users.
docker compose ps
curl -I https://graylog.example.com/
docker compose exec opensearch curl -s http://localhost:9200/_cluster/health?pretty
docker compose exec mongo mongosh --quiet --eval 'db.runCommand({ ping: 1 })' graylog
docker compose logs --since=10m graylog | egrep -i 'started|listening|error|exception' || true
If the copy button is unavailable in your browser, manually select and copy the command block above.
After login, create a test GELF UDP input or Syslog input on a non-public network, send one message, and confirm it appears in search. Then create a stream for a real source, such as firewall events or Docker host logs, so you can prove parsing and retention before onboarding every server.
Common issues and fixes
1) Graylog shows 502 through Traefik
Check whether Graylog is still starting or whether GRAYLOG_HTTP_EXTERNAL_URI has the wrong hostname. Also confirm the Traefik service label points at port 9000 and both containers share the edge network.
2) OpenSearch exits immediately
The most common causes are missing vm.max_map_count, wrong volume ownership, or insufficient memory. Reapply the sysctl, inspect docker compose logs opensearch, and avoid setting the Java heap larger than half of available RAM.
3) Search works slowly after onboarding logs
Reduce noisy sources, add stream routing, review extractors, and shorten retention before adding hardware. If ingestion is legitimate and sustained, split OpenSearch onto dedicated nodes and keep Graylog stateless behind the same Traefik entry point.
4) Admin password hash does not work
Regenerate the SHA-256 hash without a trailing newline, update .env, and recreate only the Graylog container. Do not delete MongoDB or OpenSearch volumes just to fix an authentication mistake.
FAQ
Can this run on one small VPS?
Yes for a lab or low-volume internal deployment, but production log search benefits from predictable CPU, memory, and SSD I/O. Start with at least 8 GB RAM and monitor heap, disk, and ingestion latency.
Should OpenSearch security be disabled?
Only when OpenSearch is reachable exclusively on the private Docker network. If you publish OpenSearch, move it to a dedicated secured configuration with authentication, TLS, and strict firewall rules.
Which Graylog inputs should I expose first?
Start with one private input, such as GELF UDP from application hosts or Syslog TCP from network devices. Expose additional ports only after you define source ownership and retention.
How do I back up this stack?
Back up Compose files, .env through a secret vault process, MongoDB data, Graylog journal policy, and OpenSearch snapshots. Test restore into a staging host before trusting the backup plan.
Can I use Caddy or NGINX instead of Traefik?
Yes. The important design is TLS termination at the edge and Graylog kept on an internal network. Traefik is convenient here because Docker labels keep routing close to the service definition.
When should I move beyond a single node?
Scale when ingestion spikes cause journal backlog, searches time out, disk pressure rises quickly, or retention requirements exceed one host. Separate OpenSearch first, then add Graylog nodes behind the edge proxy.
Internal links
- OpenSearch on Kubernetes with Helm: Secure Cluster Setup, Snapshots, and Operations
- Production Guide: Deploy VictoriaMetrics with Docker Compose + NGINX + systemd + UFW on Ubuntu
- Production Guide: Deploy Grafana Loki + Promtail with Docker Compose + Traefik + Let's Encrypt on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.