When I first built my NTP-PPS server, I followed this blog on how to do it: https://blog.networkprofile.org/gps-backed-local-ntp-server/. Once I was done, I wanted to be able to check on it occasionally to make sure everything was still working as expected. I just wanted to be able to monitor it without doing a bunch of extra work with Grafana and whatever else would be needed. That is where I came up with this app, to fill a need that I had for a dashboard for my NTP server. I couldn't find anything that I liked or was anywhere close to what I wanted, so I had to wait for AI to get good enough and for me to want to use it to come up with this solution. I do hope you enjoy it and consider giving it a star.
The initial deployment for this app pulls source data from Chrony on your Docker host and shows the servers it is using. It also displays the current system time and time offset from NTP. This is similar to an "NTP Client" that pulls time based on how the Docker host is set up for time resolution. If you deploy this locally to your NTP server, it will look similar to the "Remote" host connection, including PPS, NMEA, satellite data, and connected clients. For a Local deployment on a different host than your NTP server, it will look like the image below.
When connecting the app to a personal NTP GPS-enabled server over SSH, you will then see the NMEA and PPS data, along with visualized GPS data. All you have to do is click the "Connection Setup" button, enter the SSH credentials for your personal NTP server, and it will connect and populate the data correctly. The login credentials are stored locally in a "config.json" file wherever you set the bind mount (default is ./data:/app/data). The password is stored encrypted, and a separate key is used to decrypt it when needed.
Connecting to the remote server with SSH keys is as easy as copy and paste: open the connection settings, choose "Remote", fill in all fields except password, and paste in your private key. The remote session will be activated and connected.
With the color picker, you can make the interface match your favorite color (as close as possible). All you need to do is locate the "Theme" button and click it. When you select the color, it will automatically change the color scheme to what you pick. You can change it daily or keep it as it is. The color selection is stored locally in the browser so the only time it would revert back is if your browser cache and storage was totally cleared. This does mean that you can have different themes for different browsers on different computers though.
The NTP Sources data will refresh every 2 seconds and the Satellites data will refresh every 30 seconds. The GPS Satellite Time display will update every 30 seconds as the satellite data is updated.
The "View Clients" button reveals a list of connected clients and lets you track IP addresses currently getting time from your NTP server, without drilling into the CLI. This works in both "Remote" mode and "Local" mode when deployed on the NTP server host with network_mode: "host" and /run/chrony:/run/chrony mounted.
You can deploy the app using the following Docker Compose:
services:
ntp-dashboard:
image: ghcr.io/nighthawkatl/ntp-dashboard:latest ## -> Change to "nighthawkatl/ntp-dashboard:latest" to pull from Docker Hub.
container_name: ntp-dashboard ## you can call it whatever you want. This is just a friendly suggestion.
network_mode: "host" ## Required to allow direct communication with the chrony package on the host.
environment:
- LOG_LEVEL=INFO # Default level for normal operation
# Supported levels:
# - DEBUG # Most verbose (for troubleshooting)
# - INFO # Standard runtime logs (recommended default)
# - WARNING # Warnings and errors only
# - ERROR # Errors only
# - CRITICAL # Critical failures only
volumes:
- ./data:/app/data ## Bind mounts are suggested to have easy-access to the data files.
- /run/chrony:/run/chrony # Needed for local-only deployments to access chrony correctly and gather data
restart: unless-stopped ## Typical deployment unless you wish to change this.In order to get the most out of this app, even for the "local-only" deployment in Docker, you will need to install Chrony on your host. For Debian-based or Ubuntu users, this is as simple as sudo apt install chrony. There is a link in the wiki for "troubleshooting" on how to install Chrony for other distros.
The network mode must be set to "host" to allow direct access to the chrony service that is running on the host. If this is changed to "bridge" or anything else, it will not work as expected.
Local GPS probing from inside the container is optional. The default build installs Chrony tooling only, which avoids shipping Alpine's gpsd package by default. If you need local gpspipe support in the container, build with INSTALL_GPSD_CLIENTS=true. It is typically not needed unless you are running the dashboard directly on the NTP server. gpspipe currently has a critical CVE, CVE-2025-67268, affecting apk / alpine/gpsd / 3.26.1-r0. To keep the default deployment safer, gpsd support is excluded by default for remote-connection installs. If you intend to run the app local to the NTP server, build from source and set the argument to install gpsd. See compose.yaml here.
Usage is low running either the amd64 or the arm64 image. Network is near 0% even if you are using the "remote" mode to access a local NTP server on your network.
arm64 (local):
amd64 (remote):
arm64 (remote):
These are listed in no particular order
Encrypting of password in config.json filePWA conversionRelease update notificationsCompact imageConvert javascript in HTML to a script call as a separate file rather than being in the HTMLColor picker to choose your favorite color in light or dark mode- Work on updates and clearing vulnerabilities to get on a good maintenance and release schedule
- Come up with a few new features and ideas to improve the UI/UX (keeping the ball rolling)
Fix logging to show in the container logs for those times the app may not load
Please check the wiki.
Please review the Security Policy for guidance on reporting vulnerabilities and checking remediation status.
This is a current work in progress. It was coded with the help of AI to get the base project running with my ideas.