Docker Compose ELK Stack 8.x: Elasticsearch, Logstash, Kibana

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

Service Configuration Summary

ServiceImagePortsVolumesKey Env
Elasticsearchdocker.elastic.co/elasticsearch/elasticsearch:8.17.09200:9200 (API), 9300:9300 (transport)es-data:/usr/share/elasticsearch/datadiscovery.type=single-node, ES_JAVA_OPTS=-Xms1g -Xmx1g, ELASTIC_PASSWORD, xpack.security.enabled=true
Logstashdocker.elastic.co/logstash/logstash:8.17.05044:5044 (Beats), 5000:5000/tcp (TCP input), 9600:9600 (monitoring)./logstash/pipeline:/usr/share/logstash/pipeline:ro, ./logstash/config/logstash.ymlLS_JAVA_OPTS=-Xms256m -Xmx256m, LOGSTASH_INTERNAL_PASSWORD
Kibanadocker.elastic.co/kibana/kibana:8.17.05601:5601./kibana/config/kibana.ymlKIBANA_SYSTEM_PASSWORD, ELASTICSEARCH_HOSTS=https://elasticsearch:9200
Setup (init)docker.elastic.co/elasticsearch/elasticsearch:8.17.0nonees-certs:/usr/share/elasticsearch/config/certsELASTIC_PASSWORD, KIBANA_PASSWORD, LOGSTASH_PASSWORD

Environment File (.env)

VariableDefaultPurpose
ELASTIC_VERSION8.17.0Stack version for all images
ELASTIC_PASSWORDchangemeSuperuser password
KIBANA_SYSTEM_PASSWORDchangemeKibana service account password
LOGSTASH_INTERNAL_PASSWORDchangemeLogstash pipeline output password
ES_MEM_LIMIT1073741824 (1 GB)Elasticsearch container memory limit
KB_MEM_LIMIT1073741824 (1 GB)Kibana container memory limit
LS_MEM_LIMIT1073741824 (1 GB)Logstash container memory limit

Decision Tree

START: What is your deployment scenario?
├── Development/testing on a single machine?
│   ├── YES → Use single-node mode (discovery.type=single-node)
│   │   ├── Need quick prototyping?
│   │   │   ├── YES → Disable security (xpack.security.enabled=false) -- DEV ONLY
│   │   │   └── NO → Keep security on, set ELASTIC_PASSWORD
│   └── NO (production) →
│       ├── Data volume < 100 GB/day?
│       │   ├── YES → Single-node ES is sufficient, enable security + TLS
│       │   └── NO → Multi-node cluster (3+ ES nodes)
│       │       ├── Need high availability?
│       │       │   ├── YES → 3 master-eligible + 2 data nodes minimum
│       │       │   └── NO → 3 combined master+data nodes
│       └── Need TLS between all components?
│           ├── YES → Use setup container with elasticsearch-certutil (see Step 2)
│           └── NO → Basic authentication only (passwords, no TLS)

Step-by-Step Guide

1. Set host system requirements

Elasticsearch requires elevated mmap limits. This MUST be done on the Docker host, not inside the container. [src2]

# Linux: set vm.max_map_count (required for Elasticsearch)
sudo sysctl -w vm.max_map_count=262144

# Make persistent across reboots
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf

# Windows (WSL2): run in PowerShell as admin
wsl -d docker-desktop sh -c "sysctl -w vm.max_map_count=262144"

Verify: sysctl vm.max_map_count → expected: vm.max_map_count = 262144

2. Create project directory and environment file

Set up the directory structure with configuration files for each service. [src4]

mkdir -p docker-elk/{elasticsearch,logstash/pipeline,logstash/config,kibana/config}

cat > docker-elk/.env << 'EOF'
ELASTIC_VERSION=8.17.0
ELASTIC_PASSWORD=changeme
KIBANA_SYSTEM_PASSWORD=changeme
LOGSTASH_INTERNAL_PASSWORD=changeme
ES_MEM_LIMIT=1073741824
KB_MEM_LIMIT=1073741824
LS_MEM_LIMIT=1073741824
CLUSTER_NAME=docker-elk
LICENSE=basic
EOF

Verify: cat docker-elk/.env → all variables should be set

3. Create the docker-compose.yml

This is the core configuration file that defines all ELK services. [src1]

Full script: docker-compose.yml (89 lines)

Verify: docker compose config → validates the Compose file without starting services

4. Create Logstash pipeline configuration

The Logstash pipeline defines input sources, processing filters, and the Elasticsearch output. [src3]

Full script: logstash.conf (44 lines)

Verify: docker compose exec logstash logstash --config.test_and_exit → validates pipeline syntax

5. Create Kibana configuration

Configure Kibana to connect to Elasticsearch with authentication. [src5]

# kibana/config/kibana.yml
server.name: kibana
server.host: "0.0.0.0"
elasticsearch.hosts: ["http://elasticsearch:9200"]
elasticsearch.username: "kibana_system"
elasticsearch.password: "${KIBANA_SYSTEM_PASSWORD}"
monitoring.ui.container.elasticsearch.enabled: true

Verify: curl -s http://localhost:5601/api/status | jq .status.overall.level"available"

6. Start the stack and initialize users

Bootstrap the stack by starting Elasticsearch first, then initializing service account passwords. [src4]

cd docker-elk
docker compose up -d

# Wait for Elasticsearch to be healthy
until curl -s -u elastic:changeme http://localhost:9200/_cluster/health | grep -q '"status"'; do
  echo "Waiting for Elasticsearch..."; sleep 5
done

# Set kibana_system password
curl -s -X POST -u elastic:changeme \
  http://localhost:9200/_security/user/kibana_system/_password \
  -H "Content-Type: application/json" \
  -d '{"password":"changeme"}'

Verify: docker compose ps → all containers should show "healthy" or "running"

Code Examples

Docker Compose: Complete Single-Node ELK Stack

Full script: docker-compose.yml (89 lines)

# docker-compose.yml -- ELK Stack 8.x single-node development
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION}
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - xpack.security.enabled=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    ports:
      - "127.0.0.1:9200:9200"
      - "127.0.0.1:9300:9300"

Logstash Pipeline: JSON + Syslog + Beats Input

Full script: logstash.conf (44 lines)

# logstash/pipeline/logstash.conf
input {
  beats { port => 5044 }
  tcp { port => 5000; codec => json_lines }
  syslog { port => 5140 }
}

Elasticsearch Index Template: Log Data

curl -s -X PUT -u elastic:changeme \
  http://localhost:9200/_index_template/logs-template \
  -H "Content-Type: application/json" \
  -d '{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 0,
      "index.lifecycle.name": "logs-policy"
    },
    "mappings": {
      "properties": {
        "@timestamp": { "type": "date" },
        "message": { "type": "text" },
        "level": { "type": "keyword" },
        "service": { "type": "keyword" }
      }
    }
  }
}'

ILM Policy: Log Retention

curl -s -X PUT -u elastic:changeme \
  http://localhost:9200/_ilm/policy/logs-policy \
  -H "Content-Type: application/json" \
  -d '{
  "policy": {
    "phases": {
      "hot": { "actions": { "rollover": { "max_primary_shard_size": "50gb", "max_age": "30d" } } },
      "warm": { "min_age": "30d", "actions": { "shrink": { "number_of_shards": 1 } } },
      "delete": { "min_age": "90d", "actions": { "delete": {} } }
    }
  }
}'

Anti-Patterns

Wrong: Running without persistent volumes

# BAD -- data lost when container restarts
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
    # No volumes defined -- all indices lost on restart

Correct: Named volumes for data persistence

# GOOD -- data survives container restarts and upgrades
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
    volumes:
      - es-data:/usr/share/elasticsearch/data
volumes:
  es-data:
    driver: local

Wrong: Using logstash_system for pipeline output

# BAD -- logstash_system is monitoring-only, cannot write indices
output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    user => "logstash_system"
    password => "${LOGSTASH_PASSWORD}"
  }
}

Correct: Dedicated logstash_internal user with write role

# GOOD -- logstash_internal has logstash_writer role with index write
output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    user => "logstash_internal"
    password => "${LOGSTASH_INTERNAL_PASSWORD}"
    index => "logstash-%{+YYYY.MM.dd}"
  }
}

Wrong: Mismatched image versions

# BAD -- mixing 8.17.0 and 8.15.0 causes version incompatibility
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
  logstash:
    image: docker.elastic.co/logstash/logstash:8.15.0
  kibana:
    image: docker.elastic.co/kibana/kibana:8.16.0

Correct: Single version variable for all images

# GOOD -- all services pinned via .env variable
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION}
  logstash:
    image: docker.elastic.co/logstash/logstash:${ELASTIC_VERSION}
  kibana:
    image: docker.elastic.co/kibana/kibana:${ELASTIC_VERSION}
# .env: ELASTIC_VERSION=8.17.0

Wrong: Unequal JVM heap sizes

# BAD -- Xms != Xmx causes heap resizing overhead and GC pauses
environment:
  - "ES_JAVA_OPTS=-Xms256m -Xmx2g"

Correct: Equal min and max heap

# GOOD -- equal Xms/Xmx eliminates heap resizing, 50% of container memory
environment:
  - "ES_JAVA_OPTS=-Xms1g -Xmx1g"

Common Pitfalls

Diagnostic Commands

# Check Elasticsearch cluster health
curl -s -u elastic:changeme http://localhost:9200/_cluster/health?pretty

# Check Elasticsearch node stats (memory, disk, CPU)
curl -s -u elastic:changeme http://localhost:9200/_nodes/stats?pretty | jq '.nodes[].os'

# Check Logstash pipeline status
curl -s http://localhost:9600/_node/stats/pipelines?pretty

# Verify Logstash can reach Elasticsearch
docker compose exec logstash curl -s -u logstash_internal:changeme http://elasticsearch:9200

# Check Kibana status
curl -s http://localhost:5601/api/status | jq '.status.overall'

# View Elasticsearch container logs
docker compose logs elasticsearch --tail=50

# View Logstash pipeline errors
docker compose logs logstash --tail=50 | grep -i error

# Check vm.max_map_count on the host
sysctl vm.max_map_count

# List all Elasticsearch indices
curl -s -u elastic:changeme http://localhost:9200/_cat/indices?v

# Test Logstash TCP input
echo '{"message":"test log","level":"info"}' | nc localhost 5000

# Check Elasticsearch disk usage
curl -s -u elastic:changeme http://localhost:9200/_cat/allocation?v

Version History & Compatibility

VersionStatusBreaking ChangesMigration Notes
8.17.xCurrent (Jan 2026)NoneRecommended for new deployments
8.15.xSupportedNoneStandard upgrade path
8.0.xSupported (baseline)Security enabled by default, TLS auto-configuredRequires password bootstrap on first start
7.17.xMaintenanceN/A (last 7.x)Upgrade to 8.x: enable security, update env vars, re-index if needed
7.x → 8.xMigrationxpack.security.enabled=true by default, enrollment tokensRun upgrade assistant in Kibana 7.17 first

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Centralized log aggregation for <50 GB/dayIngesting >1 TB/day requiring dedicated hardwareBare-metal Elastic cluster with Ansible/Terraform
Development and testing of log pipelinesNeed a managed service with SLAElastic Cloud, AWS OpenSearch Service
Self-hosted observability on a single serverOnly need metrics (no logs)Prometheus + Grafana stack
Air-gapped or on-premises deploymentNeed real-time streaming analyticsApache Kafka + Flink + Elasticsearch
Prototyping dashboards before productionNeed APM and distributed tracing onlyJaeger or Zipkin with Elasticsearch backend

Important Caveats

Related Units