# Wazuh on Docker - Deployment with Docker Compose

> Deploy Wazuh 4.14 with Docker Compose - single/multi-node setup, TLS certificates, persistent volumes, env vars, upgrades, and troubleshooting

Source: https://opennix.org/en/docs/wazuh/deployment/wazuh-docker-deployment/


Wazuh maintains the official [wazuh-docker](https://github.com/wazuh/wazuh-docker) repository containing pre-built Docker images and Docker Compose configurations for all central components. This deployment method suits rapid provisioning of test environments, labs, and smaller production installations.

## Prerequisites

### Hardware requirements

| Configuration | CPU | RAM | Disk |
|---|---|---|---|
| Single-node | 4 cores | 8 GB | 50 GB |
| Multi-node (per node) | 4 cores | 4 GB | 50 GB |

### Software requirements

- Docker Engine 24.0 or later
- Docker Compose v2 (the `docker compose` plugin, not the deprecated `docker-compose` binary)
- Git for cloning the repository
- 64-bit OS: Linux, macOS, or Windows (via WSL2)

Verify installed versions:

```bash
docker --version
docker compose version
```

### Port requirements

| Port | Purpose |
|---|---|
| 443 | Wazuh Dashboard (HTTPS) |
| 1514 | Agent connections |
| 1515 | Agent enrollment |
| 9200 | Wazuh Indexer API |
| 55000 | Wazuh Server API |

Confirm that the listed ports are available on the host.

## Clone the repository

Clone the wazuh-docker repository and check out the appropriate version tag:

```bash
git clone https://github.com/wazuh/wazuh-docker.git -b v4.14.3
cd wazuh-docker
```

The repository contains two primary directories:

- `single-node/` - all components on a single host
- `multi-node/` - distributed architecture with multiple indexer nodes

## Single-node deployment

### Generate certificates

Before the first launch, generate TLS certificates for inter-component communication:

```bash
cd single-node
docker compose -f generate-indexer-certs.yml run --rm generator
```

Certificates are stored in `config/wazuh_indexer_ssl_certs/` and automatically mounted into the containers.

### Start the stack

```bash
docker compose up -d
```

Wait for all containers to initialize (typically 60-90 seconds):

```bash
docker compose ps
```

All three services should report `running (healthy)`:

```
NAME                              STATUS
single-node-wazuh.manager-1       Up (healthy)
single-node-wazuh.indexer-1       Up (healthy)
single-node-wazuh.dashboard-1     Up (healthy)
```

### Access the Dashboard

Open `https://<host-ip>:443` in a browser. Default credentials:

- Username: `admin`
- Password: `SecretPassword`

Change the password after initial login. The procedure is covered in the [Environment variables](#environment-variables) section.

## Multi-node deployment

The multi-node configuration deploys a three-node indexer cluster, two manager nodes (master and worker), and one dashboard node. All containers run on the same Docker host, but the architecture mirrors a distributed deployment.

### Generate certificates

```bash
cd multi-node
docker compose -f generate-indexer-certs.yml run --rm generator
```

### Start the cluster

```bash
docker compose up -d
```

Verify the indexer cluster health:

```bash
curl -sk -u admin:SecretPassword https://localhost:9200/_cluster/health?pretty
```

Expect `status: green` and `number_of_nodes: 3`.

### Verify the manager cluster

```bash
curl -sk -u wazuh-wui:SecretPassword \
  -X POST "https://localhost:55000/security/user/authenticate?raw=true" \
  | xargs -I {} curl -sk -H "Authorization: Bearer {}" \
  "https://localhost:55000/cluster/nodes?pretty"
```

The output should list two nodes: master and worker.

## Custom certificates

For production environments, use certificates issued by your organization's certificate authority instead of the self-signed ones.

### Certificate replacement

1. Prepare PEM-encoded certificate files:
   - CA certificate (`root-ca.pem`)
   - Certificate and key for each indexer node
   - Certificate and key for the manager
   - Certificate and key for the dashboard

2. Place the files in `config/wazuh_indexer_ssl_certs/`

3. Update paths in `docker-compose.yml` if file names differ from the defaults

4. Restart the stack:

```bash
docker compose down
docker compose up -d
```

### Certificate requirements

- Format: PEM (Base64-encoded)
- Subject Alternative Name (SAN): must include the node DNS name or IP address
- Key Usage: digitalSignature, keyEncipherment
- Extended Key Usage: serverAuth, clientAuth

## Persistent volumes

Docker Compose uses named volumes by default for data persistence. Volumes survive container restarts and recreation.

### Volumes in single-node configuration

| Volume | Purpose | Container path |
|---|---|---|
| `wazuh_api_configuration` | Server API configuration | `/var/ossec/api/configuration` |
| `wazuh_etc` | Manager configuration | `/var/ossec/etc` |
| `wazuh_logs` | Manager logs | `/var/ossec/logs` |
| `wazuh_queue` | Event queue | `/var/ossec/queue` |
| `wazuh_var_multigroups` | Multi-group data | `/var/ossec/var/multigroups` |
| `wazuh_integrations` | Integration scripts | `/var/ossec/integrations` |
| `wazuh_active_response` | Active Response scripts | `/var/ossec/active-response/bin` |
| `wazuh_agentless` | Agentless configuration | `/var/ossec/agentless` |
| `wazuh_wodles` | Wodles (modules) | `/var/ossec/wodles` |
| `wazuh_filebeat_etc` | Filebeat configuration | `/etc/filebeat` |
| `wazuh_filebeat_var` | Filebeat data | `/var/lib/filebeat` |
| `wazuh-indexer-data` | Indexer data | `/var/lib/wazuh-indexer` |

### Volume backups

```bash
# List volumes
docker volume ls | grep wazuh

# Back up the indexer data volume
docker run --rm \
  -v single-node_wazuh-indexer-data:/data \
  -v $(pwd)/backup:/backup \
  alpine tar czf /backup/indexer-data.tar.gz -C /data .
```

### Using bind mounts

For explicit control over data locations on the host, replace named volumes with bind mounts in `docker-compose.yml`:

```yaml
volumes:
  - ./data/wazuh-indexer:/var/lib/wazuh-indexer
  - ./data/wazuh-logs:/var/ossec/logs
```

Create directories in advance and set correct permissions:

```bash
mkdir -p data/wazuh-indexer data/wazuh-logs
chown -R 1000:1000 data/wazuh-indexer
```

## Environment variables

### Changing the indexer admin password

The password is set via environment variables in `docker-compose.yml`:

```yaml
services:
  wazuh.indexer:
    environment:
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=MyNewSecurePassword123!
```

Password requirements:

- Minimum 8 characters
- At least one uppercase letter, one lowercase letter, one digit, and one special character
- Must not contain the username

### Key manager environment variables

| Variable | Description | Default |
|---|---|---|
| `INDEXER_URL` | Indexer URL | `https://wazuh.indexer:9200` |
| `INDEXER_USERNAME` | Indexer username | `admin` |
| `INDEXER_PASSWORD` | Indexer password | `SecretPassword` |
| `FILEBEAT_SSL_VERIFICATION_MODE` | SSL verification | `full` |

### Key dashboard environment variables

| Variable | Description | Default |
|---|---|---|
| `INDEXER_URL` | Indexer URL | `https://wazuh.indexer:9200` |
| `INDEXER_USERNAME` | Indexer username | `admin` |
| `INDEXER_PASSWORD` | Indexer password | `SecretPassword` |
| `WAZUH_API_URL` | Server API URL | `https://wazuh.manager` |
| `API_USERNAME` | API username | `wazuh-wui` |
| `API_PASSWORD` | API password | `MyS3cr37P450r.*-` |

## Mounting custom configuration

### Custom ossec.conf

To use a custom manager configuration, create an `ossec.conf` file and mount it:

```yaml
services:
  wazuh.manager:
    volumes:
      - ./config/ossec.conf:/var/ossec/etc/ossec.conf
```

Extract the default configuration from the container as a starting point:

```bash
docker compose cp wazuh.manager:/var/ossec/etc/ossec.conf ./config/ossec.conf
```

### Custom rules and decoders

Mount directories containing custom rules:

```yaml
services:
  wazuh.manager:
    volumes:
      - ./config/rules/local_rules.xml:/var/ossec/etc/rules/local_rules.xml
      - ./config/decoders/local_decoder.xml:/var/ossec/etc/decoders/local_decoder.xml
```

For guidance on writing rules, refer to the [Wazuh capabilities](/docs/wazuh/capabilities/) section.

### Custom integrations

```yaml
services:
  wazuh.manager:
    volumes:
      - ./config/integrations/custom-integration.py:/var/ossec/integrations/custom-integration.py
```

After configuration changes, restart the manager:

```bash
docker compose restart wazuh.manager
```

## Upgrading containers

### Minor version upgrades

1. Stop the current stack:

```bash
docker compose down
```

2. Update the image tag in `docker-compose.yml` or clone the new repository version:

```bash
cd ..
git fetch --tags
git checkout v4.14.3
```

3. Start the stack with updated images:

```bash
docker compose up -d
```

Data in named volumes is preserved across upgrades.

### Major version upgrades

When upgrading across major versions (e.g., from 4.x to 5.x):

1. Back up all volumes
2. Review the migration guide in the [official documentation](https://documentation.wazuh.com/current/upgrade-guide/index.html)
3. Perform indexer data migration if the index structure has changed

## Production considerations

### Resource limits

Set resource limits in `docker-compose.yml` for predictable behavior:

```yaml
services:
  wazuh.indexer:
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "2.0"
        reservations:
          memory: 2G
          cpus: "1.0"
```

### JVM Heap tuning

For the indexer, allocate 50% of the container memory to JVM Heap:

```yaml
services:
  wazuh.indexer:
    environment:
      - OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g
```

Do not exceed 32 GB - the JVM loses Compressed OOPs optimizations above this threshold.

### Logging

Configure the Docker logging driver for rotation:

```yaml
services:
  wazuh.manager:
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "10"
```

### Monitoring

Add healthchecks for all services if not already defined:

```yaml
services:
  wazuh.indexer:
    healthcheck:
      test: ["CMD-SHELL", "curl -sk https://localhost:9200 -u admin:$${OPENSEARCH_INITIAL_ADMIN_PASSWORD} | grep -q 'wazuh-indexer'"]
      interval: 30s
      timeout: 10s
      retries: 5
```

### Network security

In production, restrict port access via firewall rules. Do not expose port 9200 (Indexer API) to external networks - indexer access should be limited to the internal Docker network or routed through the Dashboard.

## Troubleshooting

### Indexer container fails to start

**Symptoms:** container restarts in a loop; logs show `max virtual memory areas vm.max_map_count [65530] is too low`.

**Solution:** increase the limit on the host system:

```bash
sudo sysctl -w vm.max_map_count=262144
```

To persist across reboots:

```bash
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
```

### Certificate errors on startup

**Symptoms:** logs contain `ssl_exception` or `certificate_unknown`.

**Solution:**

1. Verify certificate presence:

```bash
ls -la config/wazuh_indexer_ssl_certs/
```

2. Regenerate certificates:

```bash
docker compose -f generate-indexer-certs.yml run --rm generator
```

3. Restart the stack:

```bash
docker compose down
docker compose up -d
```

### Dashboard shows "Wazuh API is not reachable"

**Symptoms:** the dashboard loads but displays an API connection error.

**Solution:**

1. Check the manager status:

```bash
docker compose logs wazuh.manager | tail -50
```

2. Confirm that the API username and password in the dashboard environment variables match the manager configuration

3. Test API reachability from within the dashboard container:

```bash
docker compose exec wazuh.dashboard curl -sk -u wazuh-wui:MyS3cr37P450r.*- \
  https://wazuh.manager:55000/manager/info?pretty
```

### Agents cannot connect

**Symptoms:** agents on external hosts fail to enroll.

**Solution:**

1. Confirm that ports 1514 and 1515 are published in `docker-compose.yml`:

```yaml
ports:
  - "1514:1514"
  - "1515:1515"
```

2. Check the firewall on the Docker host

3. When enrolling agents, use the Docker host IP address, not the container internal IP

### High disk space consumption

**Symptoms:** `/var/lib/docker` fills up rapidly.

**Solution:**

1. Configure index rotation in the indexer via an ISM policy
2. Configure Docker log rotation (see the [Logging](#logging) section)
3. Periodically clean unused images:

```bash
docker image prune -f
```

## Additional resources

- [Deployment options overview](/docs/wazuh/deployment/)
- [Wazuh installation - step-by-step guide](/docs/wazuh/installation/)
- [Wazuh quickstart](/docs/wazuh/installation/wazuh-quickstart/)
- [Official wazuh-docker repository](https://github.com/wazuh/wazuh-docker)

