Dynamic Message of the Day — a single Bash script that displays system information on login.
- Color schemes (switchable via config block at top of script)
- Enable/disable individual information sections
- Per-host description, environment label and SLA tag
- Maintenance log with add/delete/list support
- Automatic dependency installation on
--install --updatefor non-interactive binary updates (Ansible / Puppet / cron)- Multi-distribution support (Debian, RHEL, SUSE, Arch, Alpine families)
- All IPv4 addresses + optional IPv6 from all active interfaces
- Load average (1m / 5m / 15m) and realistic available memory (
MemAvailable) - Graphical utilization bars for memory, swap and disk
- Network interface state and link speed
- Optional Fail2Ban section — summary, per-jail IP list, parallel reverse DNS
- Optional Failed Systemd Services section (auto-hidden when all services are healthy)
- Parallel section rendering for fast output
- Update count cache to avoid slow package manager queries on every login
- LDAP / AD / NIS user support via
getent - Single self-contained shell script, no external dependencies beyond coreutils
Distributions actively supported (last ~10 years, 2015 – 2026):
| Distribution | Versions / Releases | Status |
|---|---|---|
| Debian | 8 (Jessie) – 12 (Bookworm) | Tested |
| Ubuntu | 16.04 LTS – 24.04 LTS | Tested |
| Linux Mint | 18 – 22 | Tested |
| Raspberry Pi OS | Bullseye, Bookworm | Tested |
| Kali Linux | Rolling (2020+) | Tested |
| Pop!_OS | 20.04 – 22.04 | Compatible |
| Elementary OS | 6 – 7 | Compatible |
| MX Linux | 19 – 23 | Compatible |
| Devuan | 3 – 5 | Compatible |
| Zorin OS | 16 – 17 | Compatible |
| Distribution | Versions | Status |
|---|---|---|
| CentOS | 7, 8, Stream 8/9 | Tested |
| RHEL | 7, 8, 9 | Tested |
| Rocky Linux | 8, 9 | Tested |
| AlmaLinux | 8, 9 | Tested |
| Fedora | 35 – 41 | Tested |
| Oracle Linux | 7, 8, 9 | Compatible |
| CloudLinux | 7, 8 | Compatible |
| Distribution | Versions | Status |
|---|---|---|
| openSUSE Leap | 15.x | Tested |
| openSUSE Tumbleweed | Rolling | Tested |
| SLES | 12, 15 | Compatible |
| Distribution | Versions | Status |
|---|---|---|
| Arch Linux | Rolling | Compatible |
| Manjaro | Rolling | Compatible |
| EndeavourOS | Rolling | Compatible |
| Distribution | Versions | Status |
|---|---|---|
| Alpine Linux | 3.x | Compatible |
"Tested" = verified by the author. "Compatible" = dependency auto-install supported, not explicitly tested. Tell me if you have verified it on a distribution not listed here.
The script must run as root.
sudo -i
git clone https://github.com/rtulke/dynmotd.git
cd dynmotd
./dynmotd.sh --install--install automatically:
- Detects your Linux distribution
- Installs any missing dependencies via your native package manager
- Copies the script to
/usr/local/bin/dynmotd - Creates
/etc/profile.d/motd.shso dynmotd runs on every login - Runs first-time environment setup (function, environment label, SLA)
To verify the installation, log out and back in:
exit
sudo -iIf you prefer to install packages yourself before running --install:
Debian / Ubuntu / Raspberry Pi OS / Mint
apt update && apt install -y coreutils procps hostname sed gawk grep dnsutils lsb-releaseCentOS 7 / RHEL 7
yum install -y hostname procps-ng gawk bind-utilsCentOS Stream 8-9 / Rocky / AlmaLinux / RHEL 8-9
dnf install -y hostname procps-ng gawk bind-utilsFedora
dnf install -y hostname procps-ng gawk bind-utilsopenSUSE / SLES
zypper install -y hostname procps gawk bind-utils lsb-releaseArch / Manjaro
pacman -Sy inetutils procps-ng gawk bindAlpine Linux
apk add bind-tools busybox-extras procps gawkUsage: dynmotd [OPTION] [value]
e.g. dynmotd -a "deployed new SSL certificate"
Options:
-a | --addlog "..." Add a maintenance log entry
-d | --rmlog [line-number] Delete a log entry by line number
-l | --log List all log entries
-c | --config Reconfigure environment settings
-i | --install Install dynmotd and its dependencies
-U | --update Update binary only (no setup, safe for Ansible/cron)
-u | --uninstall Uninstall dynmotd (log deletion is optional)
-v | --version Show version and exit
-h | --help Show this help
dynmotd --uninstallThe uninstaller asks two separate questions:
- Whether to delete the log and configuration data in
/root/.dynmotd/ - Final confirmation before removing the binary and profile hook
System packages that were installed as dependencies are never removed automatically. Remove them with your package manager if desired.
To update an already-installed dynmotd to a newer version without running the full setup again:
cd dynmotd
git pull
sudo bash dynmotd.sh --update--update only replaces the binary at /usr/local/bin/dynmotd. It does not ask setup questions, does not touch logs or configuration, and does not reinstall packages — safe to run from Ansible, Puppet, or a cron job.
Edit the config block at the top of the installed script:
vim /usr/local/bin/dynmotdEach section has an _INFO toggle and an optional _ALWAYS override:
| Variable | Default | Description |
|---|---|---|
SYSTEM_INFO |
1 |
System info (hostname, IP, kernel, CPU, memory, load) |
STORAGE_INFO |
1 |
Storage / disk usage with utilization bars |
NETWORK_INFO |
1 |
Network interfaces — state and link speed |
USER_INFO |
1 |
User sessions and SSH keys |
UPDATE_INFO |
1 |
Available package updates |
UPDATE_ALWAYS |
0 |
Show Update section even when UPDATE_INFO="0" |
ENVIRONMENT_INFO |
1 |
Environment label (function, env, SLA) |
FAILED_SERVICES_INFO |
1 |
Failed systemd services (auto-hidden when none failed) |
FAILED_SERVICES_ALWAYS |
0 |
Always show Failed Services, even when all healthy (= none) |
FAIL2BAN_INFO |
1 |
Fail2Ban summary — total banned + jail overview |
FAIL2BAN_ALWAYS |
0 |
Always show Fail2Ban, even if not installed |
SHOWFAIL2BAN_IPS |
1 |
List banned IPs per jail (no DNS, no delay) |
RESOLVEFAIL2BAN_IPS |
1 |
Resolve banned IPs via reverse DNS (parallel, requires SHOWFAIL2BAN_IPS="1") |
MAINTENANCE_INFO |
1 |
Maintenance log entries |
MAINTENANCE_ALWAYS |
0 |
Show Maintenance section even when MAINTENANCE_INFO="0" |
WEATHER_INFO |
1 |
Weather via wttr.in (auto-hidden if curl unavailable or fetch fails) |
WEATHER_ALWAYS |
0 |
Always show Weather section, even if fetch fails (= unavailable) |
WEATHER_CITY |
(empty) | City for weather lookup — empty = auto-detect from server IP |
WEATHER_CACHE_HOURS |
1 |
Hours before weather data is re-fetched (0 = always live) |
WEATHER_UNITS |
(empty) | Unit system: m = metric, u = USCS, M = wind in m/s (empty = wttr.in default) |
VERSION_INFO |
1 |
Version banner |
1 = enabled, 0 = disabled.
Sections are rendered in this fixed order:
- System Info
- Weather
- Storage Info
- Network Interfaces
- User Data
- Update Info
- Environment Data
- Failed Services
- Fail2Ban
- Maintenance Information
- Version banner
LIST_LOG_ENTRY="2" # number of log lines shown in the MOTDUPDATE_CACHE_HOURS="6" # hours before the update count is refreshed (0 = always live)The update cache avoids running a full package manager check on every login. The cached count is stored in /root/.dynmotd/update_cache and refreshed automatically when it expires.
Seven schemes are pre-defined. Uncomment exactly one block to activate it (F1 = labels, F2 = borders, F3 = values, F4 = warnings):
| # | Name | Character |
|---|---|---|
| 1 | DOT (default) | grey labels · pink borders · green values |
| 2 | Retro Hacker | all green, Matrix style |
| 3 | Retro Alert | all red, maximum urgency |
| 4 | Ocean | cyan/blue, cool and professional |
| 5 | Solarized Dark | grey with warm yellow accent |
| 6 | Nord | ice blue, clean and modern |
| 7 | Amber | brown/yellow, classic CRT terminal |
Example — switching to Nord:
vim /usr/local/bin/dynmotdComment out the active scheme and uncomment Nord:
## 1. DOT - day of the tentacle (default)
#F1=${C_GREY}
#F2=${C_PINK}
#F3=${C_LGREEN}
#F4=${C_RED}
## 6. nord — ice blue, clean and modern
F1=${C_LBLUE}
F2=${C_LCYAN}
F3=${C_WHITE}
F4=${C_YELLOW}The Hostname field uses hostname --fqdn. If /etc/hostname contains only the short name, only the short name is shown. Fix:
hostname mail.example.com
hostname > /etc/hostnameAlso check /etc/hosts:
127.0.1.1 mail.example.com
Note: The
Address v4/Address v6fields are read directly from network interfaces viaip -brief addr showand are not affected by hostname resolution.
This happens when the key has no comment field. Either add a comment to the end of the key line in ~/.ssh/authorized_keys, or generate keys with:
ssh-keygen -C "your.name@example.com"