dom4in.net
← All resources
Guide · Elastic Stack

Stand up an ELK stack with Docker Compose

Elasticsearch, Kibana, and Filebeat in one compose file — the two things that decide success are one kernel setting and not skipping security, and both take a minute.

The process

The full path from nothing to logs searchable in Kibana. The kernel setting in step one is the thing everyone hits; do it first and the rest is plumbing.

Directions

1Set vm.max_map_count on the host

Elasticsearch refuses to start (or crash-loops with a bootstrap check error) unless the host kernel allows enough memory-mapped areas. Set vm.max_map_count to at least 262144 on the Docker host — inside WSL2 or a Linux VM this is the VM's kernel, not Windows or macOS.

Make it permanent in /etc/sysctl.conf or a sysctl.d file, otherwise the stack breaks on the next reboot and looks like a mystery regression.

sudo sysctl -w vm.max_map_count=262144
# persist it:
echo 'vm.max_map_count=262144' | sudo tee /etc/sysctl.d/99-elasticsearch.conf

2Write the compose file

Three services: elasticsearch as a single node with a named volume for its data, kibana pointed at it, and filebeat shipping logs in. Pin an exact version and use the same one for all three images — mixed versions in the Elastic stack fail in confusing ways.

Cap the JVM heap with ES_JAVA_OPTS; without it Elasticsearch happily takes half the machine. 1–2 GB heap is plenty for a dev or small ops node.

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0
    environment:
      - discovery.type=single-node
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    volumes: ["esdata:/usr/share/elasticsearch/data"]
    ports: ["9200:9200"]
  kibana:
    image: docker.elastic.co/kibana/kibana:8.15.0
    ports: ["5601:5601"]
volumes:
  esdata:

3Keep security enabled and wire Kibana properly

The tempting shortcut — xpack.security.enabled=false — is how internet-exposed Elasticsearch instances end up in breach write-ups. Leave security on and set ELASTIC_PASSWORD for the built-in superuser.

Kibana can't log in as the elastic superuser; it needs its own service account. Easiest path: exec into the Elasticsearch container once and create a token, then hand it to Kibana as ELASTICSEARCH_SERVICEACCOUNTTOKEN. That's a one-time setup step that survives restarts.

docker compose exec elasticsearch \
  bin/elasticsearch-service-tokens create elastic/kibana kibana-token
# put the printed token in Kibana's environment

4Point Filebeat at your logs

Add a filebeat service with a config that reads what you care about — container logs via the Docker input (mount /var/lib/docker/containers read-only) or plain files via filestream. Filebeat authenticates to Elasticsearch with the credentials from step three and creates its data stream automatically.

Start small: one input, confirm events arrive, then add more. A misconfigured multiline pattern or a wrong path fails silently, and debugging one input is much easier than debugging six.

5Confirm data end to end

Open localhost:5601, log in as elastic, and go to Discover. Create a data view matching filebeat-* (or the data stream name) and you should see events within a minute of them being written. If Discover is empty, check the pipeline in order: is Filebeat running, does its log show a successful connection, does the index exist (GET _cat/indices against port 9200)?

Last piece of hygiene: set an index lifecycle policy so logs roll over and delete after a retention window. A dev ELK node that keeps everything forever fills its disk on schedule, and Elasticsearch goes read-only at the flood-stage watermark.

Common issues & fixes

Elasticsearch exits with 'max virtual memory areas too low'.

The vm.max_map_count setting from step one is missing on the host (or the WSL2/VM kernel). Set it, persist it, restart the container.

Kibana says 'Kibana server is not ready yet' forever.

It can't authenticate. Confirm the service-account token is set and that versions match exactly between Kibana and Elasticsearch.

Everything ran fine, then indices went read-only.

Disk passed the 95% flood-stage watermark. Free space or extend the volume, then clear the read-only block on the index, and add an ILM delete policy so it doesn't recur.

Filebeat runs but nothing arrives.

Check Filebeat's own log first — auth failures and wrong hosts are loud there. Then confirm the input paths exist inside the Filebeat container, not just on the host.

Container gets OOM-killed under light load.

No heap cap means the JVM sizes for the whole machine. Set ES_JAVA_OPTS=-Xms1g -Xmx1g (heap should be roughly half the container's memory limit).

Honesty note: articles are drafted with AI assistance, then a human checks each one against the vendor's current setup flow before it publishes — nothing goes live unreviewed. Vendor interfaces still change; if a step looks different, the underlying record is what matters.