Wazuh on Docker is the fastest way to get the full stack running, and the official wazuh-docker repository ships a single-node compose file with the manager, the indexer and the dashboard as three containers. Clone it, generate certificates, set one kernel parameter on the host, and bring it up. Four commands. The part worth reading is what the defaults leave you with, because a cloned compose stack is a lab until you change several things in it.
What you need
- A Docker host with at least 4 cores, 8 GB of RAM and 50 GB of free storage for the single-node stack. Double the memory and storage for the multi-node stack.
- Docker Engine 20.10.0 or newer, a current Docker Compose, and Git.
- Root or a user in the
dockergroup. - The sizing lesson if this is going to hold real data, because container storage sizing catches people out.
Set vm.max_map_count on the host
Do this first, because skipping it is the single most common reason a Wazuh Docker deployment fails.
sysctl -w vm.max_map_count=262144
The indexer creates a large number of virtual memory-mapped areas, and the Linux default limit of 65530 is far below what it needs. The documentation is blunt about it: without at least 262144 the indexer will not work correctly.
sysctl -w does not survive a reboot. Persist it the usual way, by writing vm.max_map_count=262144 into a file under /etc/sysctl.d/ and reloading. This is standard Linux housekeeping rather than a Wazuh setting, but a stack that comes back broken after a host reboot is a bad Monday.
Clone the repository
git clone https://github.com/wazuh/wazuh-docker.git -b v5.0.0-beta5
cd wazuh-docker/single-node/
The repository also contains a multi-node directory with three indexer containers, two manager containers and an Nginx proxy, and a wazuh-agent directory covered further down.
For beta5 specifically, the documentation instructs you to update the image tags in docker-compose.yml to use the -latest suffix, for example image: wazuh/wazuh-manager:5.0.0-beta5-latest. The file as cloned carries the plain 5.0.0-beta5 tags. If docker compose up fails to pull an image, this is why.
Generate the certificates
Communication between the three containers is TLS, so the stack needs certificates before it will start.
Download the certificate tool:
curl -o wazuh-certs-tool.sh https://packages-staging.xdrsiem.wazuh.info/pre-release/5.x/installation-assistant/wazuh-certs-tool-5.0.0-beta5.sh
Create config.yml in the single-node directory with the node names the compose file uses:
nodes:
# Wazuh indexer server nodes
indexer:
- name: wazuh.indexer
dns: "wazuh.indexer"
# Wazuh manager nodes
# Use node_type only with more than one Wazuh manager
manager:
- name: wazuh.manager
dns: "wazuh.manager"
# Wazuh dashboard node
dashboard:
- name: wazuh.dashboard
dns: "wazuh.dashboard"
Those DNS names are not decorative. They match the hostname values in the compose file, and the indexer validates the certificate common names against them. Rename a service and you regenerate certificates.
Run the certificate step:
bash ../tools/utils/deployment/certificates-conf.sh --cert --copy --priv
This populates the config/ directory that the compose file mounts into each container. On the beta5 lab stack a good run ends like this:
Detected indexer nodes: wazuh.indexer
Detected manager nodes: wazuh.manager
Detected dashboard nodes: wazuh.dashboard
Generating certificates
...
Copying certificates for indexer: wazuh.indexer -> config/wazuh_indexer/certs/
Copying certificates for manager: wazuh.manager -> config/wazuh_manager/certs/
Copying certificates for dashboard: wazuh.dashboard -> config/wazuh_dashboard/certs/
Copying root-ca certificates -> config/root-ca/certs/
Setting permissions for indexer wazuh.indexer (101:101)
Setting permissions for manager wazuh.manager (0:101)
Setting permissions for dashboard wazuh.dashboard (101:101)
Setting permissions for root-ca certificates (0:101)
Process completed.
Run the certificate tool on Linux, with GNU awk
This step needs a real GNU userland: bash 4 or newer for mapfile and declare -n, and GNU awk. Run it anywhere else and it fails loudly while still printing Process completed. at the end, which is how people end up with an empty config/ tree and a stack that will not start.
Verified on the beta5 lab, running it under the bash 3.2 and BSD tools that ship with macOS produces:
23/09/2026 08:02:18 ERROR: Invalid config file path.
./wazuh-certs-tool.sh: line 894: mapfile: command not found
./wazuh-certs-tool.sh: line 777: declare: -n: invalid option
declare: usage: declare [-afFirtx] [-p] [name[=value] ...]
awk: syntax error at source line 7
context is
>>> indent+) <<< " ";
awk: illegal statement at source line 7
...
23/09/2026 08:02:18 ERROR: Invalid certificate temporary path.
BusyBox awk, which is what a plain Alpine container gives you, fails on the same parser. The fix is one line: run the tool in a Linux container that has bash and gawk installed, with the single-node directory bind-mounted, then bring the stack up from the host as usual.
Bring the stack up
docker compose up -d
Give it a minute or two. The documentation notes that the dashboard container polls the indexer on port 9200 until it answers, so during startup you will see repeated Failed to connect to Wazuh indexer port 9200 messages or Wazuh dashboard server is not ready yet. That is expected while the indexer initialises, and only a problem if it never stops. On the beta5 lab all three containers reported healthy about a minute after up.
Check what is running:
docker compose ps
On the beta5 lab stack this returns:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
single-node-wazuh.dashboard wazuh/wazuh-dashboard:5.0.0-beta5 "/entrypoint.sh" wazuh.dashboard 15 minutes ago Up 15 minutes (healthy) 443/tcp, 0.0.0.0:8444->5601/tcp, [::]:8444->5601/tcp
single-node-wazuh.indexer wazuh/wazuh-indexer:5.0.0-beta5 "/entrypoint.sh open…" wazuh.indexer 15 minutes ago Up 15 minutes (healthy) 0.0.0.0:9200->9200/tcp, [::]:9200->9200/tcp
single-node-wazuh.manager wazuh/wazuh-manager:5.0.0-beta5 "/usr/local/bin/tini…" wazuh.manager 15 minutes ago Up 15 minutes (healthy) 0.0.0.0:1517->1517/tcp, [::]:1517->1517/tcp, 514/udp, 1514-1516/tcp, 0.0.0.0:55000->55000/tcp, [::]:55000->55000/tcp
That output is from a stack with two deliberate edits to the cloned compose file, and both are worth copying. The dashboard reads 8444->5601 instead of 443->5601, which keeps the stack off a privileged port. The manager publishes only 1517 and 55000: the three legacy mappings 1514:1514, 1515:1515 and 514:514/udp were deleted, so those listeners stay inside the container where a 4.x agent on the network cannot reach them. Put them back only when you have 4.x agents to keep alive.
Then open https://<DOCKER_HOST_IP> in a browser. The default login is admin with the password admin, and the self-signed certificate will produce a browser warning.
What the compose file actually gives you
Reading it is five minutes well spent. The parts that matter:
The manager publishes 1517. The single-node compose maps 1517:1517 alongside the legacy 1514, 1515 and 514/udp, plus 55000 for the API. Worth knowing because the exposed-ports table in the Docker documentation lists only the legacy set and omits 1517; the shipped file is the one to trust. Wazuh 5.x agents connect on 1517 and nothing else.
The manager’s healthcheck counts daemons. The compose file runs wazuh-manager-control status and fails the container if any line says not running. On beta5 that means eight daemons, including wazuh-manager-monitord, which the migration guide says is going and which beta5 still starts.
The dashboard listens on 5601 internally and is published on 443. The mapping is 443:5601. If you want a different external port, change the left side only.
The indexer heap is set to 1 GB. OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g is fine for a lab and much too small for real ingest. Raise it deliberately, keeping the minimum and maximum equal, and remember the container needs host memory to back it.
Credentials are environment variables in plain text. INDEXER_USERNAME, INDEXER_PASSWORD, DASHBOARD_USERNAME and DASHBOARD_PASSWORD all ship with the value equal to the username. They are visible to anyone who can read the file or run docker inspect.
Manager paths are the 5.0 layout. Volumes mount to /var/wazuh-manager/etc, /var/wazuh-manager/logs, /var/wazuh-manager/queue and so on. The Docker utilities page still shows an example volume mounted at /var/ossec/api/configuration, which is a leftover from 4.x. Follow the compose file, not that example.
Persist the data
The compose file declares named volumes and the containers are useless without them. On a single-node stack these are the ones that hold state:
| Volume | Holds |
|---|---|
wazuh-indexer-data |
All indexed events, findings and state indices |
wazuh_etc |
Manager configuration, client.keys, authd.pass, certificates |
wazuh_api_configuration |
Manager API configuration |
wazuh_queue |
Manager queues |
wazuh_logs |
Manager logs |
wazuh_var_multigroups |
Merged shared group configuration |
wazuh-dashboard-config |
Dashboard configuration |
wazuh-dashboard-custom |
Dashboard custom assets |
List them with docker volume ls. Two practical consequences. First, docker compose down -v destroys your agent keys along with everything else, so an agent estate would have to re-enrol. Second, named volumes live on the Docker host’s storage driver, which is often the root filesystem, so the indexer will happily fill / unless you move the volume or mount a dedicated device.
Change the credentials
The Docker pages in the beta documentation cover the default credentials and where they live, but do not publish a rotation procedure for the containerised stack. So be honest about where that leaves you: treat the stack as lab-grade until the passwords are changed, and do not publish port 443 anywhere it can be reached before then.
What is documented is the tooling. Indexer users are defined in internal_users.yml and hold bcrypt hashes, generated with the security plugin’s hash tool at /usr/share/wazuh-indexer/plugins/opensearch-security/tools/hash.sh. The manager API and indexer passwords are managed by the Wazuh passwords tool, described in the password management documentation. Applying that to containers means generating hashes, mounting a modified internal_users.yml, and updating the matching environment variables in the compose file for the manager and dashboard service accounts.
Verify the exact procedure against the documentation at general availability rather than trusting a recipe from a 4.x blog post; the service account names and the indexer user the manager connects as have both changed in 5.0.
Connect an agent
Agents outside the compose network connect to the Docker host’s address on 1517, not to wazuh.manager. Remember that in 5.0 a group must exist on the manager before an agent can enrol into it, and that enrolment uses a password by default. The Linux agent lesson has the full sequence.
The agent container image
Wazuh 5.0 ships a wazuh/wazuh-agent image, in the wazuh-agent directory of the same repository. Its compose file in the beta5 tag configures the agent through two variables:
services:
wazuh.agent:
image: wazuh/wazuh-agent:5.0.0-beta5
restart: always
environment:
# The whole manager connection as one host[:port][/prefix] value. Required.
# The port and the prefix may be left out, defaulting to 1517 and
# /wazuh-manager/.
- WAZUH_MANAGER_ENDPOINT=<YOUR_WAZUH_MANAGER_IP_OR_HOSTNAME>:1517/wazuh-manager/
#- WAZUH_AGENT_NAME=<WAZUH_AGENT_NAME>
- WAZUH_REGISTRATION_PASSWORD=<authd.pass-PASSWORD>
The Docker documentation page for the agent shows an older variable, WAZUH_MANAGER_SERVER, and mounts a config file at an ossec.conf path. The repository at the beta5 tag uses WAZUH_MANAGER_ENDPOINT and the 1517 prefix. Where the two disagree, the file in the tag you actually cloned wins. This is the kind of drift you should expect throughout a beta.
Verified on the beta5 lab with WAZUH_MANAGER_ENDPOINT=wazuh.manager:1517/wazuh-manager/, the container’s first log lines are:
No Wazuh configuration files to mount...
ossec.conf configuration
Manager endpoint: wazuh.manager:1517/wazuh-manager/
Starting Wazuh v5.0.0...
Started wazuh-execd...
Started wazuh-agentd...
Started wazuh-syscheckd...
Started wazuh-logcollector...
Started wazuh-modulesd...
Completed.
2026/09/23 02:35:42 wazuh-agentd: INFO: Started (pid: 53).
2026/09/23 02:35:42 wazuh-agentd: INFO: Using password specified on file: etc/authd.pass
2026/09/23 02:35:42 wazuh-agentd:https-client: INFO: TLS verification is DISABLED (verify_mode=none).
2026/09/23 02:35:42 wazuh-agentd: INFO: Valid key received
Valid key received is the line to grep for. It means enrolment succeeded and the agent has written client.keys. Note also that when the agent container runs on the compose network it can use the service name wazuh.manager, which an agent outside that network cannot.
Be clear about what the agent container is for. The documentation states plainly that a containerised agent cannot directly access or monitor the host system. It is useful for receiving forwarded logs and for integrations. It is not a way to monitor the Docker host, and it gives you no file integrity monitoring, inventory or vulnerability detection for that machine. To monitor the host, install the agent on the host. To monitor containers themselves, see monitoring Docker and Kubernetes with Wazuh.
Upgrade and teardown
Upgrading the stack is a tag change. Stop it with docker compose down, edit the image field of every service to the target version, and bring it back up. Named volumes survive down without the -v flag, so the data persists across the version change.
To remove everything including the data, docker compose down -v. There is no undo.
Common errors
The indexer container restarts in a loop shortly after start
Cause: vm.max_map_count below 262144 on the Docker host. The indexer cannot map enough memory regions and dies. Fix: set it on the host with sysctl -w vm.max_map_count=262144 and make it persistent. Setting it inside the container does nothing, because the parameter is a host kernel setting shared by all containers.
Failed to connect to Wazuh indexer port 9200
Cause: during startup this is normal. The dashboard container polls the indexer until it answers, and the documentation says to expect several of these messages. Fix: wait about a minute. If it continues, the indexer is not healthy, and the cause is usually vm.max_map_count, a certificate name mismatch, or insufficient memory on the host.
Wazuh dashboard server is not ready yet
Cause: the same startup race, seen from the browser instead of the logs. Fix: wait, then check docker compose ps for the indexer’s health status before investigating the dashboard.
The image cannot be pulled
Cause: on beta5 the documentation instructs you to append -latest to the image tags, and the tags in the cloned file do not have it. Fix: edit docker-compose.yml so each image reads, for example, wazuh/wazuh-manager:5.0.0-beta5-latest, then docker compose up -d again.
./wazuh-certs-tool.sh: line 777: declare: -n: invalid option
Cause: the certificate tool is running under bash 3.2 rather than bash 4 or newer, which on a developer machine usually means macOS. The companion symptoms are mapfile: command not found, ERROR: Invalid config file path. and a closing ERROR: Invalid certificate temporary path.. Fix: run the tool inside a Linux container with bash and gawk, bind-mounting the single-node directory. Note that the script still prints Process completed. after failing, so check that config/*/certs/ actually contains .pem files before you bring the stack up.
awk: syntax error at source line 7
Cause: the tool’s YAML parser needs GNU awk. BSD awk on macOS and BusyBox awk in a plain Alpine container both reject it. Fix: install gawk in whatever environment you run the tool in.
chown: ./config/wazuh_indexer/certs: Operation not permitted
Cause: the --priv step is trying to set container-side ownership on a bind mount that the Docker VM does not let it change. This is normal on Docker Desktop and OrbStack. Fix: after generating, run chmod -R go+rX config in the single-node directory so the container users can read the certificates. Left unfixed, the indexer and dashboard start and then fail their TLS setup.
Containers start but the manager cannot reach the indexer
Cause: usually certificates. The common names in the generated certificates have to match the hostname values in the compose file, which is why config.yml uses wazuh.indexer, wazuh.manager and wazuh.dashboard. Fix: regenerate the certificates with the correct names and recreate the containers. Changing a service name in the compose file without regenerating certificates produces exactly this symptom.
Port conflicts on start
Cause: another service on the host is bound to 443, 1517, 1514, 1515, 514/udp, 9200 or 55000, or the multi-node stack is already running. Fix: stop the conflicting service or change the left side of the port mapping. Never run both stacks on one host.
Agents cannot enrol against the container
Cause: the agent is pointed at the container name or at 1514 out of 4.x habit. Fix: use the Docker host’s address on 1517. Then check the two things that stop enrolment in 5.0 regardless of transport: the enrolment password, and whether the group named in the agent’s enrolment configuration exists on the manager. The disconnected agent lesson walks the rest of the tree.
Companion files
The repository folder for this lesson holds an annotated copy of the official single-node compose file, the config.yml used for certificate generation, and the agent compose file, each marked with what came from the official source and what is commentary.
The primary source is the Wazuh Docker deployment documentation and the wazuh-docker repository at the v5.0.0-beta5 tag. Where the two disagree during the beta, the tag is authoritative for what you just cloned.
Next: Install the Wazuh agent on Linux.
Files for this lesson
Every file this lesson refers to, exactly as used. Open a file to read it in the browser, or download the set as a zip. Download wazuh-docker.zip. The full set for all lessons is at wazuh-tutorial-files.zip.
README.md3.2 KBconfig.yml913 Bdocker-compose.agent.yml1.5 KBdocker-compose.single-node.yml7.5 KB
Frequently asked questions
Can I run Wazuh in Docker in production?
The official images are supported and the compose stacks are maintained by the Wazuh team, so the software is production-capable. Whether your deployment is depends on you: persistent volumes on real storage, rotated credentials, replaced certificates and a backup of the indexer data. The default stack as cloned is a lab.
Why does the Wazuh indexer container keep restarting?
Almost always vm.max_map_count on the Docker host. The indexer needs at least 262144 and the Linux default is 65530. Set it with sysctl on the host, not inside the container, and make it persistent so it survives a reboot.
What are the default Docker credentials for Wazuh?
The dashboard login is admin with the password admin. The indexer and dashboard service accounts are set as environment variables in docker-compose.yml, which is also where you will find the manager's indexer credentials. Change all of them before the stack is reachable by anyone else.
How do I connect an agent to Wazuh running in Docker?
Point the agent at the Docker host address on port 1517, which the single-node compose file publishes. Use the Docker host address rather than the container name, since the agent is outside the compose network. Enrolment still requires the password and a group that already exists.
Should I run the Wazuh agent as a container?
Only for collecting logs sent to it or for integrations. A containerised agent cannot see the host it runs on, so it gives you no file integrity monitoring, no inventory and no vulnerability detection for that machine. To monitor a Docker host, install the agent on the host.