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
proxy
network
[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
proxy
that your reverse proxy container also uses.docker network create proxy
-
Nginx Proxy Manager (NPM) or equivalent, attached to the
proxy
network. -
DNS pointing
fleet.example.com
at 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=1000
In 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 db
automatically 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
proxy
network. - Rotate Fleet API tokens and enroll secrets regularly.
- Fleet unhealthy:
Check logs with
docker logs fleet
orwget -qO- http://127.0.0.1:8080/healthz
inside the container. - Proxy cannot reach Fleet:
Confirm both NPM and Fleet are attached to the
proxy
network. From NPM container:curl http://fleet:8080/healthz
- Agents not enrolling:
Verify TCP stream proxy on port
8220
is 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
validate
check always appears and succeeds. -
A paths filter decides whether any
docker-compose.yml
files 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
validate
job to pass before merging.
Because the workflow now always reports a validate
check (even when skipping), unrelated PRs won’t be blocked.