Skip to Content

Production Guide: Deploy Wazuh with Docker Compose + Caddy + Single-Node Indexer on Ubuntu

A practical single-node Wazuh SIEM/XDR deployment with HTTPS, agent enrollment, backups, and operational checks.

Security monitoring often starts as a spreadsheet of servers, firewall rules, and audit reminders. That works until the first real incident: a suspicious sudo session, a public SSH service receiving thousands of attempts, or an application host that suddenly starts talking to an unexpected network. Wazuh gives a small team a practical open-source SIEM and XDR platform: agents collect events, the manager normalizes them, the indexer stores them, and the dashboard turns alerts into workflows. This guide shows a production-oriented single-node deployment for a small business or internal platform team using Docker Compose, Caddy, and the Wazuh single-node stack on Ubuntu.

The pattern below is designed for a realistic first rollout. It keeps the Wazuh dashboard behind HTTPS, separates secrets into an environment file, records the operational ports you must expose, and includes backup and verification steps. It is not a replacement for a multi-node high-availability Wazuh cluster, but it is a strong baseline for monitoring 10–200 Linux servers, jump hosts, and self-hosted applications before you invest in clustered indexers and dedicated security operations processes.

Architecture and flow overview

The deployment has four moving parts. Caddy terminates public HTTPS for the dashboard and proxies traffic to the Wazuh dashboard container. The Wazuh manager receives agent traffic on TCP 1514, registration traffic on TCP 1515, and API requests internally. The Wazuh indexer stores alerts and inventory data. The dashboard provides the web UI for analysts and administrators. In a simple environment, all containers run on one hardened Ubuntu host with persistent Docker volumes and host firewall rules.

The important boundary is this: the dashboard should be reachable only through Caddy over HTTPS, while agent ports should be exposed only to trusted server networks. If agents connect across the public internet, restrict by firewall source ranges or a VPN overlay. Treat the Wazuh API and indexer credentials like production database secrets, because they can expose security telemetry and host inventory.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with 4 vCPU, 8–16 GB RAM, and at least 100 GB SSD storage.
  • A DNS record such as wazuh.example.com pointing to the server.
  • Root or sudo access, with UFW available for firewall policy.
  • Outbound internet access to pull Docker images and request TLS certificates.
  • A plan for where Wazuh agents will live: private subnet, VPN, or specific static source IP ranges.

Step-by-step deployment

1) Install Docker, Compose, Caddy, and firewall basics

Start from a patched server. Install the container runtime, Caddy for the public reverse proxy, and basic troubleshooting tools. The firewall rules below expose HTTPS and the Wazuh agent ports. Replace the sample agent network with your own trusted subnet; do not leave registration open to the entire internet unless you understand the risk.

sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get install -y ca-certificates curl gnupg git jq ufw caddy
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 10.20.0.0/16 to any port 1514 proto tcp
sudo ufw allow from 10.20.0.0/16 to any port 1515 proto tcp
sudo ufw --force enable

If the copy button is unavailable, select the block text manually.

2) Create the application layout and secrets

Keep generated credentials out of shell history and out of the Compose file. The Wazuh Docker repository expects several passwords for the indexer, API, and dashboard. Generate long random values once, store them in a root-readable environment file, and copy them into your password manager. If you use a secrets manager such as OpenBao, SOPS, or 1Password, render this file during deployment rather than committing it to Git.

sudo mkdir -p /opt/wazuh-stack/{config,backups}
sudo chown -R root:root /opt/wazuh-stack
sudo chmod 750 /opt/wazuh-stack
cd /opt/wazuh-stack
sudo tee .env >/dev/null <<'EOF'
WAZUH_DASHBOARD_DOMAIN=wazuh.example.com
INDEXER_PASSWORD=replace_with_32_chars
API_PASSWORD=replace_with_32_chars
DASHBOARD_PASSWORD=replace_with_32_chars
EOF
sudo chmod 600 .env
sudo python3 - <<'PY'
from pathlib import Path
import secrets, string
p=Path('/opt/wazuh-stack/.env')
chars=string.ascii_letters+string.digits
text=p.read_text()
for key in ['INDEXER_PASSWORD','API_PASSWORD','DASHBOARD_PASSWORD']:
    text=text.replace(f'{key}=replace_with_32_chars', f'{key}=' + ''.join(secrets.choice(chars) for _ in range(40)))
p.write_text(text)
PY

If the copy button is unavailable, select the block text manually.

3) Clone the Wazuh single-node Docker stack and generate certificates

Use the Wazuh-maintained Compose definitions as the base instead of hand-writing every service. Pin to a known release branch during rollout, test upgrades in staging, then update intentionally. The certificate generation step creates TLS material used between Wazuh components. Run it before the stack starts for the first time.

cd /opt/wazuh-stack
sudo git clone --depth 1 --branch v4.12.0 https://github.com/wazuh/wazuh-docker.git source
cd source/single-node
sudo docker compose -f generate-indexer-certs.yml run --rm generator
sudo cp -a config /opt/wazuh-stack/config/generated-certs
sudo chmod -R go-rwx /opt/wazuh-stack/config

If the copy button is unavailable, select the block text manually.

4) Add a production override file

The upstream stack is a good base, but production needs explicit restart policy, predictable logging, and persistent volumes. Keep overrides small so future Wazuh updates remain easy to review. The example below also avoids publishing the dashboard container directly; Caddy will be the only public web entry point.

cd /opt/wazuh-stack/source/single-node
sudo tee docker-compose.override.yml >/dev/null <<'EOF'
services:
  wazuh.manager:
    restart: unless-stopped
    ports:
      - "1514:1514/tcp"
      - "1515:1515/tcp"
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"
  wazuh.indexer:
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"
  wazuh.dashboard:
    restart: unless-stopped
    ports:
      - "127.0.0.1:5601:5601"
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"
EOF

If the copy button is unavailable, select the block text manually.

5) Configure Caddy and start HTTPS

Caddy handles certificate issuance and renewal automatically. Keep the dashboard hostname dedicated to Wazuh and add conservative security headers. If your organization already uses Cloudflare, Fastly, or another edge proxy, keep Caddy on the origin and restrict inbound 80/443 to the provider IP ranges.

source /opt/wazuh-stack/.env
sudo tee /etc/caddy/Caddyfile >/dev/null <

If the copy button is unavailable, select the block text manually.

Configuration and secrets handling best practices

Rotate default Wazuh passwords immediately and document the owner of each credential. The indexer password protects alert data; the API password can expose manager state; dashboard accounts control who can inspect incidents. Use unique credentials per environment, never reuse staging secrets in production, and restrict the .env file to root. If several admins need access, place secrets in a team password manager and grant access through named accounts rather than sharing shell snippets.

Agent enrollment deserves the same care. Registration on port 1515 is convenient during rollout, but it should be limited to trusted source networks. For laptops and roaming servers, route agents through a VPN or use narrow source IP allowlists. Build a small agent naming convention before onboarding: include environment, role, and hostname so alerts are readable at 2 a.m. A name like prod-web-01 is more useful than an automatically generated cloud instance ID.

Finally, define retention and backup expectations early. Security logs grow quickly. Monitor disk usage on Docker volumes, keep snapshots for the indexer data, and export configuration changes whenever you add decoders, rules, or integrations. A SIEM without recoverable configuration becomes another fragile production service.

Verification checklist

After the stack starts, verify the local containers, the HTTPS dashboard, and the agent ports. The first boot can take several minutes while the indexer initializes. Do not troubleshoot the dashboard until the indexer and manager are healthy.

cd /opt/wazuh-stack/source/single-node
sudo docker compose ps
sudo docker compose logs --tail=80 wazuh.indexer
sudo docker compose logs --tail=80 wazuh.manager
curl -I https://wazuh.example.com
sudo ss -lntp | egrep ':(1514|1515|443)'
sudo ufw status numbered

If the copy button is unavailable, select the block text manually.

Next, install one test agent on a non-critical Linux host and confirm it appears in the dashboard. Generate a controlled event such as a failed sudo attempt or SSH login from a known test account. The goal is to validate ingestion, normalization, and analyst visibility before you enroll production systems in bulk.

Backups and upgrade routine

Back up the Compose files, generated certificates, Caddyfile, and Wazuh configuration. Indexer data backups depend on your retention target and storage budget; at minimum, snapshot the server volume before upgrades and keep configuration exports off-host. Schedule a monthly recovery test so you know whether the dashboard, manager, and indexer can be rebuilt from your backup set.

sudo tee /usr/local/sbin/backup-wazuh-config >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
stamp=$(date -u +%Y%m%dT%H%M%SZ)
dest=/opt/wazuh-stack/backups/wazuh-config-${stamp}.tgz
tar -czf "$dest" \
  /opt/wazuh-stack/.env \
  /opt/wazuh-stack/source/single-node/docker-compose.yml \
  /opt/wazuh-stack/source/single-node/docker-compose.override.yml \
  /opt/wazuh-stack/source/single-node/config \
  /etc/caddy/Caddyfile
chmod 600 "$dest"
find /opt/wazuh-stack/backups -name 'wazuh-config-*.tgz' -mtime +30 -delete
EOF
sudo chmod 750 /usr/local/sbin/backup-wazuh-config
sudo /usr/local/sbin/backup-wazuh-config

If the copy button is unavailable, select the block text manually.

For upgrades, read the Wazuh release notes, snapshot the server, pull the new branch in a staging environment, and run an agent compatibility test. Avoid blind image pulls on production. Security tooling is most valuable when it is boring and predictable.

Common issues and fixes

The dashboard loads but shows indexer errors

Wait for the indexer to finish its first initialization, then inspect the indexer logs. Most early failures are certificate path issues, low memory, or mismatched passwords. Confirm the generated certificates exist under the expected Wazuh Docker config directory and that the environment file was passed to Compose.

Agents cannot connect to the manager

Check UFW source rules, cloud security groups, and whether agents use TCP 1514 and 1515. If the agents are outside your private network, prefer a VPN route. Registration failures often mean the enrollment port is blocked or the agent is pointed at the dashboard hostname while DNS resolves through a proxy that only handles HTTPS.

Caddy cannot issue a certificate

Confirm public DNS points to the server and that ports 80 and 443 are reachable from the internet. If an edge proxy is in front, disable forced HTTPS temporarily during issuance or use DNS validation in your edge provider. Read journalctl -u caddy before changing the Wazuh stack.

Disk usage grows faster than expected

Reduce noisy agent rules, shorten retention, add storage, or move to a larger indexer design. Do not delete Docker volumes during an incident unless you have already exported the data you need. Monitor disk usage with alerts before onboarding a large fleet.

FAQ

Is this single-node deployment production ready?

It is production-oriented for small teams, labs, and early security programs. For regulated environments or large fleets, plan a multi-node indexer and manager design with tested backups.

Can I expose the Wazuh dashboard publicly?

You can, but restrict access with SSO, VPN, IP allowlists, or an edge access product. The dashboard contains sensitive host and alert data, so public exposure should be deliberate.

How many agents can one host support?

Capacity depends on event volume, rule noise, storage, and retention. Start with 10–50 agents, watch CPU, RAM, and disk growth, then scale vertically or split components.

Should agent registration stay open?

No. Keep port 1515 limited to trusted networks and close it when enrollment windows end. Permanent open registration increases the blast radius of leaked enrollment settings.

Do I need a separate database?

No external database is required for this single-node pattern. Wazuh uses its manager, indexer, and dashboard components with persistent Docker volumes.

How should I handle alert fatigue?

Start with a small set of important assets, tune noisy rules, and create escalation playbooks. A quieter SIEM with reviewed alerts is better than a noisy dashboard nobody trusts.

Can this integrate with Slack or email?

Yes. Wazuh supports integrations and active responses, but configure notification routing after the base deployment is stable and owners are defined.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Rallly with Docker Compose + Caddy + PostgreSQL + SMTP on Ubuntu
A practical production deployment pattern for private scheduling polls with TLS, SMTP, PostgreSQL, backups, and recovery checks.