This stack runs Fleet with MySQL and Redis in Docker. All services are internal-only; external access is provided through a reverse proxy (e.g., Nginx Proxy Manager) on a shared proxy network.
For more information on putting this stack behind Nginx Proxy Manager, see this repo.
- Fleet app server (UI/API + osquery enroll endpoint)
- MySQL as the primary data store
- Redis for background jobs and caching
- Health checks to ensure dependency ordering
- Named volumes for persistence
- No host ports published — only exposed to other containers on the
proxynetwork
[Internet]
|
HTTPS
|
[Nginx Proxy Manager] (on external 'proxy' network)
| \
| \ TCP 8220 (stream)
HTTP 8080 \
| \
[fleet:8080] [fleet:8220]
|
depends_on
|
[redis:6379] [mysql:3306]
- TLS is terminated by NPM.
- Fleet listens on plain HTTP (
8080) inside the cluster. - The osquery enroll endpoint uses raw TCP on port
8220.
-
Docker Engine v24+ and Docker Compose v2 on a 64-bit Linux host.
-
An external Docker network named
proxythat your reverse proxy container also uses.docker network create proxy
-
Nginx Proxy Manager (NPM) or equivalent, attached to the
proxynetwork. -
DNS pointing
fleet.example.comat your reverse proxy host. -
TLS handled entirely by the proxy.
Create a .env file alongside the compose file:
# Timezone
TZ=America/New_York
# MySQL credentials
MYSQL_ROOT_PASSWORD=change_me_root
MYSQL_DATABASE=fleet
MYSQL_USER=fleet
MYSQL_PASSWORD=change_me_user
# Fleet configuration
FLEET_SERVER_PRIVATE_KEY= # Run 'openssl rand -base64 32' to generate
FLEET_LOGGING_JSON=true
FLEET_OSQUERY_STATUS_LOG_PLUGIN=filesystem
FLEET_FILESYSTEM_STATUS_LOG_FILE=/logs/osquery_status.log
FLEET_FILESYSTEM_RESULT_LOG_FILE=/logs/osquery_result.log
FLEET_LICENSE_KEY=
# Vulnerability settings
FLEET_OSQUERY_LABEL_UPDATE_INTERVAL=1h
FLEET_VULNERABILITIES_CURRENT_INSTANCE_CHECKS=true
FLEET_VULNERABILITIES_DATABASES_PATH=/vulndb
FLEET_VULNERABILITIES_PERIODICITY=1h
# Only needed if forcing container user
PUID=1000
PGID=1000In Nginx Proxy Manager:
-
HTTP Host Proxy
- Domain:
fleet.example.com - Forward Hostname / IP:
fleet - Forward Port:
8080 - Enable SSL and request a Let’s Encrypt certificate.
- Force SSL.
- Domain:
-
TCP Stream Proxy
- Add a new Stream in NPM.
- Listen Port:
8220 - Forward Hostname / IP:
fleet - Forward Port:
8220 - This is a raw TCP proxy. Do not wrap it in HTTP.
Bring up the services:
docker compose --env-file .env up -d- Fleet will run
prepare dbautomatically on startup. - Health checks ensure MySQL and Redis are ready before Fleet starts.
Access Fleet at:
https://fleet.example.com
mysql— MySQL dataredis— Redis AOF datadata— Fleet application statelogs— Local Fleet logs (if using filesystem log plugin)vulndb— Cached vulnerability databases
Back these up regularly.
-
What to back up:
mysql(mandatory)data,logs,vulndb(recommended)redis(optional; cold cache can be rebuilt)
-
Snapshot example:
docker run --rm -v mysql:/vol -v $PWD:/backup alpine \ tar -C /vol -czf /backup/mysql.tgz .
-
Restore:
- Create empty volumes.
- Extract backup into volumes.
- Restart the stack.
- Do not publish MySQL or Redis ports to the host.
- Use strong, unique passwords for MySQL.
- Restrict which containers can join the
proxynetwork. - Rotate Fleet API tokens and enroll secrets regularly.
- Fleet unhealthy:
Check logs with
docker logs fleetorwget -qO- http://127.0.0.1:8080/healthzinside the container. - Proxy cannot reach Fleet:
Confirm both NPM and Fleet are attached to the
proxynetwork. From NPM container:curl http://fleet:8080/healthz - Agents not enrolling:
Verify TCP stream proxy on port
8220is reachable externally.
- For larger installs, pin specific image tags (
mysql:8.x,redis:7.x,fleetdm/fleet:<version>). - Tune MySQL (
innodb_buffer_pool_size,utf8mb4). - Use external logging instead of filesystem logs.
- Scale Fleet horizontally by running multiple replicas behind the same proxy, backed by a shared MySQL and Redis.
This repo includes a GitHub Actions workflow that validates changes to docker-compose.yml files.
-
The workflow always runs on PRs, so the
validatecheck always appears and succeeds. -
A paths filter decides whether any
docker-compose.ymlfiles changed. -
If compose files changed:
- Validation runs with
docker compose config. - PR is auto-merged with squash.
- The branch is deleted.
- Validation runs with
-
If no compose files changed:
- The job succeeds immediately with a skip message.
Use a repository ruleset or classic branch protection to:
- Block direct pushes to
main. - Require pull requests for changes.
- Require the
validatejob to pass before merging.
Because the workflow now always reports a validate check (even when skipping), unrelated PRs won’t be blocked.