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.