English | 繁體中文
A fan controller for Dell PowerEdge servers. It sets the fan duty cycle through iDRAC IPMI OEM raw commands, then selects a fan-curve level from pluggable temperature sources: ESXi NVMe SMART, iDRAC sensors, local Linux disks, and local or remote NVIDIA GPUs.
Caution
This program temporarily overrides Dell's automatic fan control. During initial setup, keep the iDRAC Web UI or a physical console available. Verify manual, restore, and diagnose before running auto for an extended period. If the server overheats, readings become unreliable, or behavior is abnormal, run restore immediately and stop the container.
flowchart LR
TUI[make tui\nInteractive setup] --> ENV[.env\n600 permissions]
ENV --> COMPOSE[Docker Compose]
COMPOSE --> CTRL[fan-control.sh]
CTRL --> IPMI[iDRAC / IPMI\nfan raw command]
CTRL --> ESXI[ESXi\nesxcli SMART]
CTRL --> SDR[iDRAC\nTemperature SDR]
CTRL --> DISK[Linux disks\nsmartctl JSON]
CTRL --> GPU[Local/remote NVIDIA\nnvidia-smi]
CTRL --> LOG[logs/fan_control.log\nhealthcheck]
The controller sends fan commands only to iDRAC; all temperature sources are read-only. Every provider implements the same validate, collect, and adjust interface. When multiple sources are enabled, the controller uses the highest valid adjusted temperature. GPU and disk offsets let unlike sensors share one fan curve without source-specific decision code.
Figure 1: Enabling IPMI over LAN in the iDRAC Web UI. The exact option name may vary by iDRAC firmware version.
Run the setup on the Docker host. The TUI requires no dialog, Python, or other additional packages. It is written in pure Bash, preserves comments from .env.example, updates files atomically, and sets file permissions to 600.
git clone https://github.com/DF-wu/iDRACFanSpeedControl.git
cd iDRACFanSpeedControl
make tuiThe main menu looks like this. Each submenu writes changes back to .env, and the file location is shown again when you exit.
╭────────────────────────────────────────────────────────────╮
│ iDRAC Fan Control · Setup TUI │
╰────────────────────────────────────────────────────────────╯
1) Quick setup wizard 6) Safety, timing, and logging
2) iDRAC / IPMI settings 7) Review redacted configuration
3) Temperature source 8) Safety, timing, and logging
4) ESXi NVMe source 9) Review redacted configuration
5) Local Linux disks 10) Validate configuration
6) Remote NVIDIA GPUs 11) Run read-only diagnostics
7) Fan curve 0) Save and exit
For your first setup, select iDRAC sensors only and leave external sources disabled. After completing the TUI, run:
make validate
docker compose run --rm idrac-fan-control diagnose
docker compose up -d
docker logs -f idrac-fan-controldiagnose is read-only and never sends fan raw commands. It checks iDRAC, every temperature source, the writable log path, and previews the fan level that would be selected. If you do not need a source, remove it from the TUI source preset instead of ignoring its errors.
To edit a different configuration file:
src/fan-control-tui.sh --config /path/to/staging.envcp .env.example .env
chmod 600 .env
$EDITOR .envMinimal iDRAC-only example:
IDRAC_IP=192.0.2.10
IDRAC_ID=root
IDRAC_PASSWORD=change-me
OPERATION_MODE=auto
TEMPERATURE_SOURCES=idracReplace IDRAC_PASSWORD, the IP address, and the management network with real values. Values such as 192.0.2.*, change-me, and replace_with_* in .env.example are intentional placeholders and will be rejected by validate.
| Command | Purpose | Changes fan control |
|---|---|---|
auto |
Continuously controls fans from the configured curve and restores automatic mode on exit when configured | Yes |
once |
Reads temperatures once, applies one decision, and exits | Yes |
manual 35 |
Sets a 35% duty cycle; without a number, uses MANUAL_FAN_SPEED |
Yes |
restore |
Sends the Dell automatic fan-control command | Yes (restore) |
status |
Shows chassis status and raw temperature SDR data | No |
config |
Shows the effective configuration; passwords are shown only as set/length | No |
validate |
Checks values, placeholders, required commands, and source conditions | No |
diagnose |
Probes connections and temperature readings and shows a decision preview | No |
healthcheck |
Checks configuration and whether the auto-control log was updated recently enough | No |
With Docker Compose:
docker compose run --rm idrac-fan-control config
docker compose run --rm idrac-fan-control validate
docker compose run --rm idrac-fan-control diagnose
docker compose run --rm idrac-fan-control status
docker compose run --rm idrac-fan-control once
docker compose run --rm idrac-fan-control manual 35
docker compose run --rm idrac-fan-control restoreThe local script accepts the same commands:
src/FanControlWithEsxiSmart.sh diagnose
src/FanControlWithEsxiSmart.sh configLocal execution requires bash, coreutils, ipmitool, and timeout. Linux disk mode also requires smartctl and jq; SSH password authentication requires sshpass. The Docker image includes these dependencies.
sequenceDiagram
participant U as Operator
participant C as Controller
participant D as iDRAC
U->>C: validate
C-->>U: Static configuration and dependency results
U->>C: diagnose
C->>D: mc info / SDR (read-only)
D-->>C: Connection and temperatures
C-->>U: Source checks + decision preview
U->>C: manual 30
C->>D: Set fan duty (controlled test)
U->>C: restore
C->>D: Dell automatic fan control
U->>C: auto
Before unattended operation, confirm each item:
- IPMI over LAN is enabled in the iDRAC Web UI and the management network is reachable.
- The iDRAC account has the required permissions and its password is not a placeholder.
manual 30sets a low speed,manual 70sets a conservative speed, andrestorereturns control to Dell automatic mode.- Every enabled source reports
[PASS]indiagnose; disable sources you do not use. - Start with a conservative
FAILSAFE_FAN_SPEEDandRESTORE_AUTO_ON_EXIT=true.
TEMPERATURE_SOURCES is a comma-separated list containing esxi, idrac, gpu, linux_disk, and/or remote_gpu. Sources can be combined, and the controller retains each source label for logging and diagnostics. See Temperature source interface for the extension contract and deployment examples.
| Source | Reading | Requirements | Behavior on failure |
|---|---|---|---|
idrac |
Readable sensors from ipmitool sdr type Temperature |
iDRAC IPMI | Marks this source as failed |
esxi |
esxcli storage core device smart get for the configured NVMe device |
SSH and DRIVE_DEVICE |
Marks this source as failed |
gpu |
Temperature of each local GPU reported by nvidia-smi |
NVIDIA Container Toolkit and driver | Marks this source as failed |
linux_disk |
SMART temperature for each configured Linux device | smartctl, jq, and device access |
Fails unreadable devices; keeps other valid disks |
remote_gpu |
Every GPU reported by nvidia-smi on each configured VM |
SSH credentials and remote NVIDIA driver | Fails unreachable hosts; keeps other valid VMs |
flowchart TD
R[All valid readings] --> M{Select highest adjusted temperature}
M --> L[temperature_level]
L --> H{Lower than previous level?}
H -- No --> S[Increase immediately or keep level]
H -- Yes --> X{Below previous threshold - HYSTERESIS?}
X -- No --> S2[Keep previous level]
X -- Yes --> S3[Move to lower level]
S --> F[Apply fan speed]
S2 --> F
S3 --> F
R -. All sources fail .-> FS[FAILSAFE_FAN_SPEED]
The default curve is shown below. validate ensures that thresholds are strictly increasing, fan speeds never decrease, and the fail-safe speed is not lower than the critical speed.
| Level | Decision temperature | Default fan speed |
|---|---|---|
idle |
<65°C |
25% |
low |
65–69°C |
30% |
medium |
70–74°C |
40% |
high |
75–79°C |
50% |
critical |
>=80°C |
60% |
failsafe |
All sources fail | 70% |
Moving to a lower fan level requires the temperature to fall by HYSTERESIS degrees below the relevant threshold, preventing repeated speed changes near a boundary. Moving to a higher level is immediate. Each source owns its adjustment: local and remote GPUs subtract their configured offset, Linux disks subtract LINUX_DISK_TEMP_OFFSET, and all results have a minimum of 0°C.
| Variable | Default | Description |
|---|---|---|
IDRAC_IP |
Empty | iDRAC hostname or IP; required for all fan commands |
IDRAC_ID |
root |
iDRAC user |
IDRAC_PASSWORD |
Empty | Passed to ipmitool -E through the IPMI_PASSWORD environment variable; never appears in argv |
IPMI_INTERFACE |
lanplus |
Usually left unchanged |
IPMI_TIMEOUT / IPMI_RETRIES |
5 / 2 |
Per-command timeout and retry count |
OPERATION_MODE |
manual |
auto, once, or manual |
DRY_RUN |
false |
Only for testing command construction; does not produce real readings |
COMMAND_TIMEOUT |
20 |
Maximum duration in seconds for SSH, IPMI, and GPU commands |
CHECK_INTERVAL |
60 |
Seconds between cycles in auto mode |
RESTORE_AUTO_ON_EXIT |
true |
Restores automatic control when auto mode receives SIGTERM or exits |
| Variable | Default | Description |
|---|---|---|
TEMPERATURE_SOURCES |
esxi |
Comma-separated list of registered source IDs |
WITH_GPU_TEMP |
false |
Legacy compatibility switch; true appends gpu |
GPU_TEMP_OFFSET |
15 |
Offset subtracted from GPU temperature |
ESXI_HOST / ESXI_USERNAME |
Empty / root |
ESXi SSH target |
ESXI_PASSWORD |
Empty | Used when no key is configured; never echoed by the TUI |
ESXI_SSH_KEY |
Empty | Preferred over password authentication when set |
ESXI_SSH_PORT |
22 |
SSH port (1–65535) |
SSH_CONNECT_TIMEOUT |
10 |
SSH connection timeout in seconds |
SSH_STRICT_HOST_KEY_CHECKING |
accept-new |
yes, no, ask, or accept-new |
DRIVE_DEVICE |
Empty | Full ID returned by esxcli storage core device list |
LINUX_DISK_DEVICES |
Empty | Comma-separated Linux device paths, such as /dev/nvme1,/dev/sdb |
LINUX_DISK_TEMP_OFFSET |
0 |
Offset subtracted from local disk temperatures |
LINUX_DISK_NOCHECK |
never |
smartctl power-mode check; standby avoids waking sleeping disks |
REMOTE_GPU_HOSTS |
Empty | Comma-separated Linux VM hostnames or addresses |
REMOTE_GPU_USERNAME / REMOTE_GPU_SSH_PORT |
root / 22 |
SSH identity shared by remote GPU hosts |
REMOTE_GPU_PASSWORD / REMOTE_GPU_SSH_KEY |
Empty / Empty | Remote GPU SSH authentication; keys are preferred |
REMOTE_GPU_TEMP_OFFSET |
15 |
Offset subtracted from remote GPU temperatures |
IDRAC_SENSOR_INCLUDE_REGEX |
Empty | awk regex used to keep matching sensor names only |
IDRAC_SENSOR_EXCLUDE_REGEX |
no reading|disabled|not readable |
Excludes invalid SDR entries |
| Variable | Default | Description |
|---|---|---|
TEMP_LOW/MEDIUM/HIGH/CRITICAL |
65/70/75/80 |
Strictly increasing thresholds in °C |
FAN_SPEED_IDLE/LOW/MEDIUM/HIGH/CRITICAL |
25/30/40/50/60 |
Values from 1–100% that must not decrease |
HYSTERESIS |
2 |
Degrees below the previous threshold required before lowering fan speed |
FAILSAFE_ON_ERROR |
true |
Uses a conservative speed when all sources fail |
FAILSAFE_FAN_SPEED |
70 |
Must be greater than or equal to the critical speed |
MANUAL_FAN_SPEED |
35 |
Used by manual when no argument is provided |
LOG_DIR / LOG_FILE |
/var/log/fan-control / fan_control.log |
Persistent state log |
LOG_LEVEL |
INFO |
DEBUG adds command and source details but never logs passwords |
HEALTHCHECK_MAX_AGE |
0 |
0 means CHECK_INTERVAL*3 + COMMAND_TIMEOUT |
The Compose configuration uses the GHCR image, host networking, and a ./logs volume by default:
docker compose pull
docker compose up -d
docker compose ps
docker inspect --format '{{.State.Health.Status}}' idrac-fan-controlThe healthcheck verifies more than environment configuration. In OPERATION_MODE=auto, it also confirms that fan_control.log has been updated within HEALTHCHECK_MAX_AGE. If every data source fails while FAILSAFE_ON_ERROR=false, the control cycle does not write a success record and the container becomes unhealthy. This is an intentional safety signal.
GPU mode requires the NVIDIA Container Toolkit. Uncomment gpus: all in docker-compose.yml, then configure:
TEMPERATURE_SOURCES=idrac,gpu
GPU_TEMP_OFFSET=15Verify the setup:
docker run --rm --gpus all nvidia/cuda:12.9.0-runtime-ubuntu24.04 nvidia-smi
docker compose run --rm idrac-fan-control diagnoseIf GPU support is unnecessary, build locally without CUDA to reduce the image size:
docker build --build-arg BASE_IMAGE=ubuntu:24.04 -t idrac-fan-control:local .Linux disk mode needs a device mapping for every entry in LINUX_DISK_DEVICES. For example:
services:
idrac-fan-control:
devices:
- /dev/nvme1:/dev/nvme1For a TrueNAS CD6 plus NVIDIA GPUs in other VMs, use TEMPERATURE_SOURCES=linux_disk,remote_gpu, map the CD6 controller device, and mount a read-only SSH key for the GPU VMs. The complete example is in docs/TEMPERATURE_SOURCES.md.
Example normal log entry:
2026-07-18 03:12:10 [INFO] Control temp 68C -> low (30%). Sources: idrac:Inlet Temp=68C
For additional context, temporarily set:
LOG_LEVEL=DEBUGThen inspect:
docker logs -f idrac-fan-control
tail -f logs/fan_control.log
docker compose run --rm idrac-fan-control diagnoseDEBUG logs targets, command types, source selection, and failure stages only. Passwords never appear in argv or the configuration summary. See docs/TROUBLESHOOTING.md for the complete symptom → check → fix table, and USAGE_GUIDE.md for routine and emergency operations.
Shortest recovery path for common problems:
# Restore Dell automatic control first whenever risk is uncertain
docker compose run --rm idrac-fan-control restore
# Inspect the effective configuration and dependency state
docker compose run --rm idrac-fan-control config
docker compose run --rm idrac-fan-control diagnose- Never commit
.env,logs/, private keys, or incident dumps. The TUI sets configuration file permissions to600. - Compose mounts the gitignored
./secretsdirectory read-only at/run/secrets. Use in-container paths such as/run/secrets/esxi_ed25519or/run/secrets/gpu_vms_ed25519. - Prefer
ESXI_SSH_KEY. When password authentication is required, the controller uses theSSHPASSenvironment variable withsshpass -e, keeping the password out of argv. - The iDRAC password is supplied through the
IPMI_PASSWORDenvironment variable andipmitool -E. Theconfigcommand and TUI review show only whether it is set and its character count. - Keep iDRAC and ESXi on an isolated management network. Never expose IPMI over LAN to the public internet.
- Before changing
.env, keep an offline backup with600permissions and testrestore.
ssh root@ESXI_HOST
esxcli storage core device list
esxcli storage core device smart get -d 't10.NVMe____full_identifier_string'Copy the complete identifier into DRIVE_DEVICE; do not shorten it. If the SMART output does not include a numeric Drive Temperature, diagnose marks the ESXi source as failed instead of silently treating the error as 0°C.
make test # bash -n + 52 core assertions + 14 TUI assertions
make validate # Check the current .env with Docker dependencies; does not change fans
make validate-example # DRY_RUN smoke test that does not require .env
make docker-buildTests cover the source interface, Linux SMART collection, multi-host remote GPUs, source offsets, SDR parsing, hysteresis, fail-safe behavior, configuration validation, credential redaction, health freshness, diagnostics, and safe TUI configuration round-trips. Real hardware, SSH targets, disks, iDRAC, and GPUs must still be verified with diagnose on your management network.
.
├── src/
│ ├── FanControlWithEsxiSmart.sh # Controller, validate, diagnose, and healthcheck
│ ├── fan-control-tui.sh # Pure Bash setup TUI
│ └── setIdracFanSpeed.sh # Compatibility wrapper for the legacy script
├── tests/
│ ├── fan-control.test.sh # Core logic and safety tests
│ └── tui.test.sh # .env parser/writer tests
├── docs/
│ ├── TEMPERATURE_SOURCES.md # Source interface and deployment examples
│ └── TROUBLESHOOTING.md # Symptom-based troubleshooting guide
├── images/image.png # iDRAC IPMI settings screenshot
├── .env.example # Fully commented configuration template
├── docker-compose.yml
├── Dockerfile
├── Makefile
├── README.md # English documentation
├── README.zh-TW.md # Traditional Chinese documentation
└── USAGE_GUIDE.md
The project primarily targets Dell PowerEdge R730/R730xd-class systems with iDRAC 8 OEM fan raw commands. Other generations may be compatible, but identical behavior must not be assumed. Complete manual, restore, and diagnose checks before unattended operation. Sensor names, disk permissions, SSH access, and driver behavior vary by environment, so rely on diagnostic output from your own deployment.
MIT. See LICENSE.
Documentation last reviewed: August 15, 2026.
