Getting started with FleetDM

Could this replace Ansible?

Posted by Isaac on Tuesday, August 25, 2026

I was recently chatting with some colleagues about deployment tooling and FleetDM came up. I felt like I was aware of most deployment tools but this was new to me.

Logos are how companies show brands that use their products and of whom they are proud to share. So it is a severe negative to me they show Flock amongst their scrolling list.

/img/2026-08-fleetdm-01.png

But let’s still consider them as they are Open Source and easy to deploy

Containerized setup

The easiest way to fire up Fleet is to launch it in docker

$ curl -sSL https://fleetdm.com/resources/install-fleetctl.sh | bash
Fetching the latest version of fleetctl...
Latest version available on NPM: 4.90.1
Downloading fleetctl 4.90.1 for Linux...
fleetctl installed successfully in /home/builder/.fleetctl/

I can now fire up a new instance with fleetctl preview

$ ~/.fleetctl/fleetctl preview
Downloading dependencies from fleetdm/fleet:main into /home/builder/.fleet/preview...
Pulling Docker dependencies...
Starting Docker containers...
Waiting for server to start up...
Initializing server...
Loading starter library...

Fleet will now log you into the UI automatically.
You can also open the UI at this URL: http://localhost:1337/previewlogin.
Email: admin@example.com
Password: preview1337#

Enrolling local host...
Trying to clear orbit and osquery directories...
Waiting for host to enroll...
Starting simulated Linux hosts...
Gtk-Message: 14:30:00.470: Not loading module "atk-bridge": The functionality is provided by GTK natively. Please try to not load it.
Preview environment complete. Enjoy using Fleet!

Use the fleetctl stop and fleetctl reset subcommands to shut down Fleet or start fresh.

We now see just this docker instance enrolled

/img/2026-08-fleetdm-02.png

Adding hosts

We can go to “hosts” and click “Add hosts”

/img/2026-08-fleetdm-03.png

I’ll then select Linux to get the command to enroll a host

/img/2026-08-fleetdm-04.png

I’ll add my local host that is running docker

$ export PATH=$PATH:/home/builder/.fleetctl
$ fleetctl package --type=deb --enable-scripts --fleet-desktop --fleet-url=https://localhost:8412 --enroll-secret=D/Va4Y/xhtLdB2SYg3hX1J+OnlUSZxTs                                                              Generating your fleetd agent...

                                                                                   Success! You generated fleetd at /home/builder/fleet-osquery_1.59.0_amd64.deb

To add hosts to Fleet, install fleetd.
Learn how: https://fleetdm.com/learn-more-about/enrolling-hosts

Immediately it picked up all the containers running as well as the local host (bosgamerz9)

/img/2026-08-fleetdm-05.png

Host Details

We can see the basic information like agent version, disk space and IP address on the details page

/img/2026-08-fleetdm-06.png

The “Software” section creates an SBOM we can check for any issues or CVEs

/img/2026-08-fleetdm-07.png

It works both ways - I can also pick a software version and see the versions and hosts that use it (say we had a package vulnerability and wanted to see which devices were already patched)

/img/2026-08-fleetdm-08.png

Scripts

Let’s say we wanted to do something on one of those hosts. We could invoke “Run script” from “Actions”

/img/2026-08-fleetdm-09.png

But before we can do that, we need to upload a script.

Lets create a basic “show uptime” script

$ ./showUptime.sh
+ uptime
 14:44:38 up 3 days,  6:20,  1 user,  load average: 1.48, 1.74, 1.39
$ cat ./showUptime.sh
#!/bin/bash
set -x
uptime

I’ll then upload that in the “Scripts” area

/img/2026-08-fleetdm-10.png

I can now chose to run that on this host

/img/2026-08-fleetdm-11.png

I see a pending

/img/2026-08-fleetdm-12.png

And it says pending as it says it will run when the host comes online

/img/2026-08-fleetdm-13.png

We can also see the script action listed in “Upcoming” activities

/img/2026-08-fleetdm-14.png

My first issue came about when it said “Scripts are disabled”

/img/2026-08-fleetdm-15.png

I’m a bit confused as my enroll command did include scripts:

fleetctl package --type=deb --enable-scripts --fleet-desktop --fleet-url=https://localhost:8412 --enroll-secret=D/Va4Y/xhtLdB2SYg3hX1J+OnlUSZxTs

and Script Execution is enabled in Org Settings

/img/2026-08-fleetdm-16.png

But alas I see its now grayed out

/img/2026-08-fleetdm-17.png

I tried with sudo as well as service. I also installed with dpkg

$ sudo /home/builder/.fleetctl/fleetctl package --type=deb --enable-scripts --service --fleet-desktop --fleet-url=https://localhost:8412 --enroll-secret=D/Va4Y/xhtLdB2SYg3hX1J+OnlUSZxTs
Generating your fleetd agent...

Success! You generated fleetd at /home/builder/fleet-osquery_1.59.0_amd64.deb

To add hosts to Fleet, install fleetd.
Learn how: https://fleetdm.com/learn-more-about/enrolling-hosts
$ sudo systemctl status orbit
Unit orbit.service could not be found.
$ sudo dpkg -i /home/builder/fleet-osquery_1.59.0_amd64.deb
Selecting previously unselected package fleet-osquery.
(Reading database… 263971 files and directories currently installed.)
Preparing to unpack …/fleet-osquery_1.59.0_amd64.deb…
Unpacking fleet-osquery (1.59.0)…
Setting up fleet-osquery (1.59.0)…
Created symlink '/etc/systemd/system/multi-user.target.wants/orbit.service' → '/usr/lib/systemd/system/orbit.service'.

Once I had the service in place, I could more clearly see the issue - it didn’t like the “https”

$ sudo systemctl status orbit
● orbit.service - Orbit osquery
     Loaded: loaded (/usr/lib/systemd/system/orbit.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-08-20 15:06:44 CDT; 1min 55s ago
 Invocation: e30c01ea5bdc4023845ab0ec857bab71
   Main PID: 2187781 (orbit)
      Tasks: 23 (limit: 11368)
     Memory: 37.4M (peak: 39.4M)
        CPU: 2.270s
     CGroup: /system.slice/orbit.service
             ├─2187781 /opt/orbit/bin/orbit/orbit
             ├─2188433 /opt/orbit/bin/osqueryd/linux/stable/osqueryd --pidfile=/opt/orbit/osquery.pid --extensions_socket=/opt/orbit/orbit-osquery.em --logger_path=/opt/orbit/osquery_log --enroll_secret_env ENROLL_SECRET --tls_hostname=localhost:8412 --en>
             └─2188438 /opt/orbit/bin/osqueryd/linux/stable/osqueryd

Aug 20 15:07:23 bosgamerz9 orbit[2187781]: 2026-08-20T15:07:23-05:00 INF enroll failed, retrying error="enroll request: POST /api/fleet/orbit/enroll: Post \"https://localhost:8412/api/fleet/orbit/enroll\": tls: failed to verify certificate: x509: certific>
Aug 20 15:07:24 bosgamerz9 orbit[2188438]: W0820 15:07:24.238571 2188438 tls_enroll.cpp:101] Failed enrollment request to https://localhost:8412/api/v1/osquery/enroll (Request error: certificate verify failed) retrying...
Aug 20 15:07:33 bosgamerz9 orbit[2187781]: 2026-08-20T15:07:33-05:00 INF orbit enroll attempt failed error="enroll request: POST /api/fleet/orbit/enroll: Post \"https://localhost:8412/api/fleet/orbit/enroll\": tls: failed to verify certificate: x509: cert>
Aug 20 15:07:33 bosgamerz9 orbit[2187781]: 2026-08-20T15:07:33-05:00 INF enroll failed, retrying error="enroll request: POST /api/fleet/orbit/enroll: Post \"https://localhost:8412/api/fleet/orbit/enroll\": tls: failed to verify certificate: x509: certific>
Aug 20 15:07:49 bosgamerz9 orbit[2188438]: W0820 15:07:49.261708 2188438 tls_enroll.cpp:101] Failed enrollment request to https://localhost:8412/api/v1/osquery/enroll (Request error: certificate verify failed) retrying...
Aug 20 15:07:53 bosgamerz9 orbit[2187781]: 2026-08-20T15:07:53-05:00 INF orbit enroll attempt failed error="enroll request: POST /api/fleet/orbit/enroll: Post \"https://localhost:8412/api/fleet/orbit/enroll\": tls: failed to verify certificate: x509: cert>
Aug 20 15:07:53 bosgamerz9 orbit[2187781]: 2026-08-20T15:07:53-05:00 INF enroll failed, retrying error="enroll request: POST /api/fleet/orbit/enroll: Post \"https://localhost:8412/api/fleet/orbit/enroll\": tls: failed to verify certificate: x509: certific>
Aug 20 15:08:25 bosgamerz9 orbit[2188438]: W0820 15:08:25.285691 2188438 tls_enroll.cpp:101] Failed enrollment request to https://localhost:8412/api/v1/osquery/enroll (Request error: certificate verify failed) retrying...
Aug 20 15:08:33 bosgamerz9 orbit[2187781]: 2026-08-20T15:08:33-05:00 INF orbit enroll attempt failed error="enroll request: POST /api/fleet/orbit/enroll: Post \"https://localhost:8412/api/fleet/orbit/enroll\": tls: failed to verify certificate: x509: cert>
Aug 20 15:08:33 bosgamerz9 orbit[2187781]: 2026-08-20T15:08:33-05:00 INF enroll failed, retrying error="enroll request: POST /api/fleet/orbit/enroll: Post \"https://localhost:8412/api/fleet/orbit/enroll\": tls: failed to verify certificate: x509: certifi

Let’s add “–insecure” and try again

$ sudo /home/builder/.fleetctl/fleetctl package --type=deb --enable-scripts --service --insecure --fleet-desktop --fleet-url=https://localhost:8412 --enroll-secret=D/Va4Y/xhtLdB2SYg3hX1J+OnlUSZxTs
Generating your fleetd agent...

Success! You generated fleetd at /home/builder/fleet-osquery_1.59.0_amd64.deb

To add hosts to Fleet, install fleetd.
Learn how: https://fleetdm.com/learn-more-about/enrolling-hosts
$ sudo dpkg -i /home/builder/fleet-osquery_1.59.0_amd64.deb
(Reading database… 263997 files and directories currently installed.)
Preparing to unpack …/fleet-osquery_1.59.0_amd64.deb…
Unpacking fleet-osquery (1.59.0) over (1.59.0)…
Setting up fleet-osquery (1.59.0)…

I can see the setting now reflected in the defaults

$ sudo cat /etc/default/orbit

ORBIT_UPDATE_URL=https://updates.fleetdm.com
ORBIT_ORBIT_CHANNEL=stable
ORBIT_OSQUERYD_CHANNEL=stable
ORBIT_UPDATE_INTERVAL=15m0s

ORBIT_FLEET_DESKTOP=true
ORBIT_DESKTOP_CHANNEL=stable


ORBIT_INSECURE=true

ORBIT_FLEET_URL=https://localhost:8412


ORBIT_ENROLL_SECRET=D/Va4Y/xhtLdB2SYg3hX1J+OnlUSZxTs

ORBIT_ENABLE_SCRIPTS=true

This time it ran without issue

/img/2026-08-fleetdm-18.png

While the User Management seems to be a locked out pay feature:

/img/2026-08-fleetdm-19.png

I bet we could sort this with a script.

Here I’ll add a fake user:

$ sudo useradd -m -s /bin/bash myfakeuser

Now we can also edit the scripts in the browser

/img/2026-08-fleetdm-20.png

and confirm to save

/img/2026-08-fleetdm-21.png

I can see my updated password now when I try and run it

/img/2026-08-fleetdm-22.png

which looks like it worked

/img/2026-08-fleetdm-23.png

Kubernetes Install

Let’s add the helm repo and update

$ helm repo add fleet https://fleetdm.github.io/fleet/charts
"fleet" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "fleet" chart repository
...Successfully got an update from the "datadog" chart repository
Update Complete. ⎈Happy Helming!⎈

This will use some subcharts for MariaDB and Valkey so let’s first just create a namespace and a MySQL secret that will get picked up

$ kubectl create ns fleet
namespace/fleet created
$ vi mysql.secret.yaml
$ cat ./mysql.secret.yaml
kind: Secret
apiVersion: v1
metadata:
  name: mysql
  namespace: fleet
stringData:
  mysql-password: my-new-mysql-password
$ kubectl apply -f ./mysql.secret.yaml
secret/mysql created

I see many examples of fleet values, and they all vary.

The only reliable way to see the current defaults is to just use helm

$ helm show values fleet/fleet
## Section: Kubernetes
# All settings related to how Fleet is deployed in Kubernetes
hostName: fleet.localhost
replicas: 3 # The number of Fleet instances to deploy
revisionHistoryLimit: 10 # Number of old ReplicaSets for Fleet deployment to retain for rollback (set to 0 for unlimited)
imageRepository: fleetdm/fleet
imageTag: v4.90.1 # Version of Fleet to deploy
# imagePullPolicy is optional. If unset, Kubernetes defaults to IfNotPresent
# for tagged images and Always for the :latest tag. Valid values: Always,
# IfNotPresent, Never.
# imagePullPolicy: IfNotPresent
# imagePullSecrets is optional.
# imagePullSecrets:
#   - name: docker
#   - name: internal
podAnnotations: {} # Additional annotations to add to the Fleet pod
serviceAnnotations: {} # Additional annotations to add to the Fleet service
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
resources:
  limits:
    cpu: 1 # 1 CPU Core
    memory: 4Gi
  requests:
    cpu: 0.1 # 0.1 CPU Core
    memory: 50Mi

# Node labels for pod assignment
# ref: https://kubernetes.io/docs/user-guide/node-selection/
nodeSelector: {}

# Tolerations for pod assignment
# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
tolerations: []

# Configurable affinity for pod assignment
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - podAffinityTerm:
          labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - fleet
          topologyKey: kubernetes.io/hostname
        weight: 100

ingress:
  enabled: false
  className: ""
  annotations:
    {}
    # kubernetes.io/tls-acme: "true"
    # nginx.ingress.kubernetes.io/proxy-body-size: 10m
    # kubernetes.io/ingress.class: nginx
    # cert-manager.io/cluster-issuer: letsencrypt
  hosts:
    - host: chart-example.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

## Section: Fleet
# All of the settings relating to configuring the Fleet server
fleet:
  # Will also be the targetPort for the service
  listenPort: 8080
  # servicePort is optional.  Use if the service port needs to differ from the listenPort above.
  # servicePort: 9080
  # Name of the Secret resource storing S3 bucket and optionally TLS secrets
  secretName: fleet
  # Whether or not to run `fleet db prepare` to run SQL migrations before starting Fleet
  autoApplySQLMigrations: true
  # Add extra annotations to the migration Job
  migrationJobAnnotations:
  tls:
    enabled: true
    # Set to true if you need a separate secret for just TLS data.
    # Useful with cert-manager and similar deployments.
    uniqueTLSSecret: false
    secretName: fleet-tls
    compatibility: modern
    certSecretKey: server.cert
    keySecretKey: server.key
  auth:
    bcryptCost: 12
    saltKeySize: 24
  app:
    tokenKeySize: 24
    inviteTokenValidityPeriod: 120h # 5 days
  session:
    keySize: 64
    duration: 2160h # 90 days
  logging:
    debug: false
    json: false
    disableBanner: false
  mdm:
    windows:
      wstepIdentityCertKey: ""
      wstepIdentityKeyKey: ""
  carving:
    s3:
      bucketName: ""
      prefix: ""
      accessKeyID: ""
      secretKey: s3-bucket
      region: ""
      endpointURL: ""
      forceS3PathStyle: false
      gcsIAMAuth: false
      stsAssumeRoleARN: ""
  softwareInstallers:
    s3:
      bucketName: ""
      prefix: ""
      accessKeyID: ""
      secretKey: software-installers
      region: ""
      endpointURL: ""
      forceS3PathStyle: false
      gcsIAMAuth: false
      stsAssumeRoleARN: ""
  license:
    secretName: ""
    licenseKey: license-key
  extraVolumes: []
  extraVolumeMounts: []
  # Currently only passes readOnlyRootFilesystem, runAsNonRoot, runAsUser, runAsGroup
  securityContext:
    readOnlyRootFilesystem: true
    runAsNonRoot: true
    runAsUser: 3333
    runAsGroup: 3333
  # Add additional CA's to the Fleet container truststore
  # To add CA's, set enabled: true
  # Supports adding CA's from Config maps and Secrets
  # configMaps:
  #   - name: fleet-ca-config
  #   - name: ca-config-map-2
  #   - name: ca-config-map-3
  #   - name: ca-config-map-N
  # secrets:
  #   - name: fleet-ca-secret
  #   - name: ca-secret-2
  #   - name: ca-secret-3
  #   - name: ca-secret-N
  additionalCAs:
    enabled: false
    configMaps:
    # - name: fleet-ca-config
    secrets:
    # - name: fleet-ca-secret

# Whether to make fleet vulnerability processing run in a dedicated container
# if you set dedicated=false, you need to increase the main resources section
# to 4Gi or the fleet container will be OOMKilled when vulnerability processing
# tries to run.
vulnProcessing:
  dedicated: false
  schedule: "0 * * * *"
  resources:
    limits:
      cpu: 1 # 1GHz
      memory: 4Gi
    requests:
      cpu: 0.1 # 100Mhz
      memory: 50Mi

## Section: osquery
# All of the settings related to osquery's interactions with the Fleet server
osquery:
  # Name of the secret resource containing optional secrets for AWS credentials
  secretName: osquery
  nodeKeySize: 24
  labelUpdateInterval: 30m
  detailUpdateInterval: 30m

  # To change where Fleet store the logs sent from osquery, set the values below
  logging:
    statusPlugin: filesystem
    resultPlugin: filesystem

    # To congigure the filesystem logger, change the values below
    filesystem:
      statusLogFile: osquery_status # will be placed in the /logs volume
      resultLogFile: osquery_result # will be placed in the /logs volume
      enableRotation: false
      enableCompression: false
      volumeSize: 20Gi # the maximum size of the volume

    # To configure the AWS Firehose logger, change the values below
    firehose:
      region: ""
      accessKeyID: ""
      secretKey: firehose
      stsAssumeRoleARN: ""
      statusStream: ""
      resultStream: ""

    # To configure the AWS Kinesis logger, change the values below
    kinesis:
      region: ""
      accessKeyID: ""
      secretKey: kinesis
      stsAssumeRoleARN: ""
      statusStream: ""
      resultStream: ""

    # To configure the AWS Lambda logger, change the values below
    lambda:
      region: ""
      accessKeyID: ""
      secretKey: lambda
      stsAssumeRoleARN: ""
      statusFunction: ""
      resultFunction: ""

    # To configure the GCP PubSub logger, change the values below
    pubsub:
      project: ""
      statusTopic: ""
      resultTopic: ""

## Section: database
# All of the connection settings for MySQL
database:
  # Name of the Secret resource containing MySQL password and TLS secrets
  secretName: mysql
  address: 127.0.0.1:3306
  database: fleet
  username: fleet
  passwordKey: mysql-password
  maxOpenConns: 50
  maxIdleConns: 50
  connMaxLifetime: 0
  tls:
    enabled: false
    ## Commented options below are optional.  Uncomment to use.
    # caCertKey: ca.cert
    ## Client certificates require both the certKey and keyKey
    # certKey: client.cert
    # keyKey: client.key
    config: ""
    serverName: ""

## Section: database_read_replica:
# All of the connection settings for MySQL read replica
# Commented options are optional. Uncomment to use.
# database_read_replica:
#  # Name of the Secret resource containing MySQL password and TLS secrets
#  address: 127.0.0.1:3306
#  database: fleet
#  username: fleet-ro
#  ## Password configuration. Pick whether you'd like to load from secret or from an accessible mount path.
#  ## Added from Secret
#  secretName: mysql-ro
#  passwordKey: mysql-ro-password
#  ## Added from Mount Path
#  passwordPath: /path/to/password
#  maxOpenConns: 50
#  maxIdleConns: 50
#  connMaxLifetime: 0
#  tls:
#    enabled: false
#    ## Commented options below are optional.  Uncomment to use.
#    # caCertKey: ca.cert
#    ## Client certificates require both the certKey and keyKey
#    # certKey: client.cert
#    # keyKey: client.key
#    config: ""
#    serverName: ""

## Section: cache
# All of the connection settings for Redis
cache:
  address: 127.0.0.1:6379
  database: "0"
  usePassword: false
  secretName: redis
  passwordKey: redis-password

## Section: GKE
# Settings that make running on Google Kubernetes Engine easier
gke:
  # The CloudSQL Proxy runs as a container in the Fleet Pod that proxies connections to a Cloud SQL instance
  cloudSQL:
    enableProxy: false
    imageRepository: gcr.io/cloudsql-docker/gce-proxy
    imageTag: 1.17-alpine
    verbose: true
    instanceName: ""
  # The GKE Ingress requires a few changes that other ingress controllers don't
  ingress:
    useManagedCertificate: false
    useGKEIngress: false
    # 0 to allow the nodeport to be automatically selected, otherwise allowed range (30000-32767)
    nodePort: 0
  # Workload Identity allows the K8s service account to assume the IAM permissions of a GCP service account
  workloadIdentityEmail: ""

## Section: Environment Variables
# All of the environment variables that can be set for Fleet
environments:
  # MDM Settings
  # The following environment variable is required if you are using
  # Fleet's macOS MDM features.  In a production environment, it is recommended that
  # you store this private key in a secret and use envsFrom to reference the secret below.
  # For more information, check out the docs: https://fleetdm.com/docs/configuration/fleet-server-configuration#server-private-key
  FLEET_SERVER_PRIVATE_KEY: ""

## Section: Environment Variables from Secrets/CMs
# envsFrom:
# - name: MY_ENV_NAME_1
#   valueFrom:
#     configMapKeyRef:
#       name: cm-name
#       key: cm-key-name
# - name: MY_ENV_NAME_2
#   valueFrom:
#     secretKeyRef:
#       name: secret-name
#       key: secret-key-name

## Section: MySQL subchart
## Set mysql.enabled=true to deploy a dev/test MySQL instance.
## For production, use an external MySQL and leave this disabled.
## When enabled, also point Fleet to the subchart service and secret:
##   database.address: <release-name>-mysql:3306
##   database.secretName: <release-name>-mysql
mysql:
  enabled: false
  auth:
    rootPassword: ""
    username: fleet
    password: ""
    database: fleet

## Section: Redis/Valkey subchart
## Set redis.enabled=true to deploy a dev/test Valkey (Redis-compatible) instance.
## For production, use an external Redis/Valkey and leave this disabled.
## NOTE: The condition key is "redis.enabled" for backwards compatibility,
## but the sub-chart is now Valkey (https://valkey.io).
## When enabled, also point Fleet to the subchart service:
##   cache.address: <release-name>-valkey:6379
## If valkey.auth.enabled=true, also set cache.usePassword/cache.secretName/
## cache.passwordKey to match your Valkey secret configuration.
redis:
  enabled: false

valkey:
  replica:
    enabled: false
  auth:
    enabled: false

Since I’ll be using a tpk.pw DNS entry from Azure DNS, lets quickly create that

$ az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 76.156.69.232 -n fleetdm
{
  "ARecords": [
    {
      "ipv4Address": "76.156.69.232"
    }
  ],
  "TTL": 3600,
  "etag": "897208ab-fd69-4c1a-a0e6-0548d18f6a4c",
  "fqdn": "fleetdm.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/fleetdm",
  "name": "fleetdm",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}

Now I can properly set the ingress settings in the values file I want to use:

$ cat fleet.values.yaml
hostname: fleetdm.tpk.pw

ingress:
  enabled: true
  className: "nginx"
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/tls-acme: "true"
  hosts:
    - host: fleetdm.tpk.pw
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls:
  - secretName: fleet-tls
    hosts:
      - fleetdm.tpk.pw

fleet:
  tls:
    enabled: false
    # Set to true if you need a separate secret for just TLS data.
    # Useful with cert-manager and similar deployments.
    uniqueTLSSecret: true
    secretName: fleet-tls
    compatibility: modern
    certSecretKey: server.cert
    keySecretKey: server.key

# Enable subcharts
mysql:
  enabled: true
  auth:
    rootPassword: "your-root-password"
    username: fleet
    password: "your-db-password"
    database: fleet

redis:
  enabled: true

# Point Fleet to the subchart services
database:
  address: fleet-mysql:3306
  database: fleet
  username: fleet
  secretName: fleet-mysql
  passwordKey: mysql-password

cache:
  address: fleet-valkey:6379
  database: "0"
  usePassword: false

Now to install

$ helm upgrade --install fleet fleet/fleet   --namespace fleet   --values fleet.values.yaml
Release "fleet" does not exist. Installing it now.
NAME: fleet
LAST DEPLOYED: Fri Aug 21 06:19:29 2026
NAMESPACE: fleet
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
$ kubectl get po -n fleet
NAME                            READY   STATUS      RESTARTS   AGE
fleet-68fc5f7bc7-4d697          1/1     Running     0          101s
fleet-68fc5f7bc7-6wgrc          1/1     Running     0          99s
fleet-68fc5f7bc7-sgcnl          1/1     Running     0          96s
fleet-migration-29dfg           0/1     Completed   0          101s
fleet-mysql-0                   1/1     Running     0          15m
fleet-valkey-56c4fdc58f-2tvfw   1/1     Running     0          15m

I can now hit the main page and setup the initial user

/img/2026-08-fleetdm-24.png

I setup an admin user and org details

/img/2026-08-fleetdm-25.png

I can now login and see my dashboard

/img/2026-08-fleetdm-26.png

Let’s add a different Linux host this time

/img/2026-08-fleetdm-27.png

I still need to prep the target with the fleetctl binary

builder@builder-T100:~$ curl -sSL https://fleetdm.com/resources/install-fleetctl.sh | bash
Fetching the latest version of fleetctl...
Latest version available on NPM: 4.90.1
Downloading fleetctl 4.90.1 for Linux...
fleetctl installed successfully in /home/builder/.fleetctl/
builder@builder-T100:~$ ls /home/builder/.fleetctl/
CHANGELOG.md  fleetctl  LICENSE  README.md
builder@builder-T100:~$ export PATH=$PATH:/home/builder/.fleetctl/
builder@builder-T100:~$

Now I can install fleet (orbit) by creating the deb package and installing it.

Ideally, we have this deb package created once (for our helm deploy) and installed at system build time.

builder@builder-T100:~$ fleetctl package --type=deb --enable-scripts --fleet-desktop --fleet-url=https://fleetdm.tpk.pw --enroll-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Generating your fleetd agent...

Success! You generated fleetd at /home/builder/fleet-osquery_1.59.0_amd64.deb

To add hosts to Fleet, install fleetd.
Learn how: https://fleetdm.com/learn-more-about/enrolling-hosts
builder@builder-T100:~$ sudo dpkg -i /home/builder/fleet-osquery_1.59.0_amd64.deb
Selecting previously unselected package fleet-osquery.
(Reading database ... 340802 files and directories currently installed.)
Preparing to unpack .../fleet-osquery_1.59.0_amd64.deb ...
Unpacking fleet-osquery (1.59.0) ...
Setting up fleet-osquery (1.59.0) ...
Created symlink /etc/systemd/system/multi-user.target.wants/orbit.service → /lib/systemd/system/orbit.service.
builder@builder-T100:~$ sudo systmctl status orbit
sudo: systmctl: command not found
builder@builder-T100:~$ sudo systemctl status orbit
● orbit.service - Orbit osquery
     Loaded: loaded (/lib/systemd/system/orbit.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2026-08-21 06:46:40 CDT; 21s ago
   Main PID: 2781593 (orbit)
      Tasks: 27 (limit: 8932)
     Memory: 37.0M
        CPU: 4.139s
     CGroup: /system.slice/orbit.service
             ├─2781593 /opt/orbit/bin/orbit/orbit
             ├─2782808 /opt/orbit/bin/osqueryd/linux/stable/osqueryd --pidfile=/opt/orbit/osquery.pid --extension>
             └─2782816 /opt/orbit/bin/osqueryd/linux/stable/osqueryd "" "" "" "" "" "" "" "" "" "" "" "" "" "" "">

Aug 21 06:47:00 builder-T100 orbit[2781593]: 2026-08-21T06:47:00-05:00 INF killing any pre-existing fleet-desktop>
Aug 21 06:47:01 builder-T100 orbit[2781593]: 2026-08-21T06:47:01-05:00 INF attempting to get user session type an>
Aug 21 06:47:01 builder-T100 orbit[2781593]: 2026-08-21T06:47:01-05:00 INF running sudo display=:0 id=1000 path=/>
Aug 21 06:47:01 builder-T100 orbit[2781593]: 2026-08-21T06:47:01-05:00 INF running command cmd="/usr/bin/sudo -n >
Aug 21 06:47:01 builder-T100 sudo[2783000]:     root : PWD=/home/builder ; USER=builder ; COMMAND=/bin/bash --log>
Aug 21 06:47:01 builder-T100 sudo[2783000]: pam_unix(sudo-i:session): session opened for user builder(uid=1000) b>
Aug 21 06:47:01 builder-T100 orbit[2782816]: I0821 06:47:01.518265 2782816 eventfactory.cpp:156] Event publisher >
Aug 21 06:47:01 builder-T100 orbit[2782816]: I0821 06:47:01.518364 2782816 eventfactory.cpp:156] Event publisher >
Aug 21 06:47:01 builder-T100 orbit[2782816]: I0821 06:47:01.518376 2782816 eventfactory.cpp:156] Event publisher >
Aug 21 06:47:01 builder-T100 orbit[2782816]: I0821 06:47:01.518385 2782816 eventfactory.cpp:156] Event publisher >
Aug 21 06:47:01 builder-T100 orbit[2782816]: I0821 06:47:01.888200 2783028 interface.cpp:137] Registering exten

I can now see it listed

/img/2026-08-fleetdm-28.png

I’ll now run a report against this host

/img/2026-08-fleetdm-29.png

However, it never did anything (I tried a few times)

I next tried creating a named report

/img/2026-08-fleetdm-30.png

Scripts

Let’s upload that uptime script

/img/2026-08-fleetdm-31.png

I can quickly run the invoke script from the webpage

/img/2026-08-fleetdm-32.png

If I wanted to see the script as run against multiple hosts, I could view it from the Scripts page

/img/2026-08-fleetdm-33.png

It took a few minutes (5-7m), but it did run and come back with results

/img/2026-08-fleetdm-34.png

What about offline?

I have a situation I wanted to consider about a host that is only online from time to time.

Let’s stop the orbit service to simulate being offline

builder@builder-T100:~$ sudo systemctl stop orbit
builder@builder-T100:~$ sudo systemctl status orbit
○ orbit.service - Orbit osquery
     Loaded: loaded (/lib/systemd/system/orbit.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Fri 2026-08-21 07:29:24 CDT; 6s ago
    Process: 2781593 ExecStart=/opt/orbit/bin/orbit/orbit (code=exited, status=0/SUCCESS)
   Main PID: 2781593 (code=exited, status=0/SUCCESS)
        CPU: 1min 27.410s

Aug 21 07:29:22 builder-T100 orbit[2781593]: 2026-08-21T07:29:22-05:00 INF interrupt osqueryd runner error="received signal terminated"
Aug 21 07:29:22 builder-T100 orbit[2781593]: 2026-08-21T07:29:22-05:00 INF interrupt capabilities checker error="received signal terminated"
Aug 21 07:29:22 builder-T100 orbit[2781593]: 2026-08-21T07:29:22-05:00 INF interrupt osqueryd extension runner error="received signal terminated"
Aug 21 07:29:22 builder-T100 orbit[2781593]: 2026/08/21 07:29:22 ERROR error processing request err="deregistering extension: read unix @->/opt/orbit/orbit-osquery.em: read: connection reset by peer"
Aug 21 07:29:22 builder-T100 orbit[2781593]: 2026-08-21T07:29:22-05:00 INF interrupt desktop runner error="received signal terminated"
Aug 21 07:29:22 builder-T100 orbit[2781593]: 2026-08-21T07:29:22-05:00 INF interrupt signal handler error="received signal terminated"
Aug 21 07:29:24 builder-T100 orbit[2781593]: 2026-08-21T07:29:24-05:00 ERR unexpected exit error="received signal terminated"
Aug 21 07:29:24 builder-T100 systemd[1]: orbit.service: Deactivated successfully.
Aug 21 07:29:24 builder-T100 systemd[1]: Stopped Orbit osquery.
Aug 21 07:29:24 builder-T100 systemd[1]: orbit.service: Consumed 1min 27.410s CPU time.

After a couple minutes the page refreshed to show our host is now offline

/img/2026-08-fleetdm-35.png

To test, I’ll just make a little touch script

builder@bosgamerz9:~/Workspaces/fbsnew$ vi ~/touchFile.sh
builder@bosgamerz9:~/Workspaces/fbsnew$ chmod 755 ~/touchFile.sh
builder@bosgamerz9:~/Workspaces/fbsnew$ cat ~/touchFile.sh
#!/bin/bash
set -x
touch /tmp/i_was_here

Let’s add it and set it to run when the host comes online

Now let’s bring the host online and see if the script runs

Debian installs

I wanted next to test if i could use FleetDM to install and/or update remote Debian packages

I created this repo with a hello world Debian

The action ran without issue

/img/2026-08-fleetdm-38.png

I then created the initial release

/img/2026-08-fleetdm-39.png

I’ll make an install script

builder@bosgamerz9:~/Workspaces/fbsnew$ chmod 755 ~/installHelloWorld.sh 
builder@bosgamerz9:~/Workspaces/fbsnew$ cat ~/installHelloWorld.sh 
#!/bin/bash
set -x
cd /tmp
wget https://github.com/idjohnson/dpkgForTesting/releases/download/1.0.0/hello-fresh_1.0_all.deb
dpkg -i ./hello-fresh_1.0_all.deb

And as we see from the postinst this should create a file in /tmp/hello.txt if it works.

Let’s see it in action:

I can now see my package listed:

/img/2026-08-fleetdm-41.png

Debian upgrades

Let’s rev this up now to 1.1. I’ll update the packaging scripts and files here

I then published a new 1.1 release

/img/2026-08-fleetdm-42.png

Now let’s create an upgrade script

builder@bosgamerz9:~/Workspaces/dpkgForTesting$ vi ~/upgradeHelloWorld.sh 
builder@bosgamerz9:~/Workspaces/dpkgForTesting$ cat ~/upgradeHelloWorld.sh 
#!/bin/bash
set -x
cd /tmp
wget https://github.com/idjohnson/dpkgForTesting/releases/download/1.1/hello-fresh_1.1_all.deb
dpkg -i ./hello-fresh_1.1_all.deb

Let’s now run it:

Initially it doesn’t show the updated version

/img/2026-08-fleetdm-44.png

But we can click “Refresh”

/img/2026-08-fleetdm-45.png

Summary

I have traditionally used AWX to handle server maintenance

/img/2026-08-fleetdm-46.png

However, the challenge is that AWX must see the hosts to login and do work. This has the advantage that no “Ansible Agent” needs to run. We just need to SSH over and do work.

This works great in a flat network, but creates some real challenges when looking at a distributed network (with distinct dev/stage/prod) or trying to support hosts that run in a colo or target customer environment.

We cannot exactly expect Customer X to create an SSH hole in their network for our Ansible system to remote in and do work.

This is where Fleet comes to play. It does have an agent always running, yes, but the goal here is compliance, reporting and script invokation.

They have a lot of features behind a paywall like installing certificates or setting passwords

/img/2026-08-fleetdm-47.png

And to me, that is fine - in that case it is used as an MDM platform for device enrollment and ITSM.

But for me, I just want to be able to reliably modify systems whose IPs may change, or they may go offline and pop up in a totally different network.

I can even schedule a script - such as an Ubuntu upgrade to go live on a date and not need to rely on updating an apt repository or setting up schedule upgrade tasks in Linux.

/img/2026-08-fleetdm-48.png

Free is free, but there is also a very reasonably priced paid option that enforce disk encryption, push certificates and do much much more for US$7/host/month.

Imagine, if you will, you have to manage 100 hospital devices and quickly rotate passwords, or create inventory lists and you need to support desktop and mobile OSes. The idea that would just be $700/mo ($8400/year) seems ridiculously cheap. Especially when you consider this is a self-hosted SOC 2 complaint system.

As this really solves some specific issues I have, I will definitely be adding FleetDM as a new fixture in my toolset