Running an NTP Time Server in Your Homelab
Accurate time synchronization matters more than it seems. Certificate validation, log correlation, Kerberos authentication, and replication protocols all depend on accurate clocks. A homelab NTP server centralizes time sync, reduces external NTP queries, and opens the door to a Stratum 1 setup using GPS.
Photo by Eric Tompkins on Unsplash
Why Run a Local NTP Server
- Latency: A server on your LAN responds in microseconds vs. milliseconds from public NTP
- Reliability: Devices stay synced during internet outages
- Accuracy: GPS-disciplined Stratum 1 achieves sub-microsecond accuracy
- Reduced external traffic: One server queries upstream; everything else queries locally
- Log correlation: Unified time source makes multi-server log analysis reliable
Stratum Levels
NTP uses a stratum hierarchy:
| Stratum | Description |
|---|---|
| 0 | Reference clock (GPS, atomic) — not directly networked |
| 1 | Server connected directly to a Stratum 0 device |
| 2 | Server syncing from a Stratum 1 |
| 3 | Server syncing from a Stratum 2 |
Public NTP pools (pool.ntp.org) are typically Stratum 2-3. A GPS-disciplined Raspberry Pi gives you Stratum 1 on your LAN.
Basic Setup: chrony as an NTP Server
Chrony is the recommended NTP implementation on modern Linux — it's more accurate than ntpd and handles network disruptions better.
Install and Configure
# Debian/Ubuntu
sudo apt install chrony
# RHEL/Fedora
sudo dnf install chrony
Edit /etc/chrony.conf:
# Upstream NTP sources (4 for resilience)
pool 0.pool.ntp.org iburst
pool 1.pool.ntp.org iburst
pool 2.pool.ntp.org iburst
pool 3.pool.ntp.org iburst
# Allow your LAN to query this server
allow 192.168.1.0/24
# Serve time even if not fully synced (useful for isolated networks)
local stratum 10
# Log directory
logdir /var/log/chrony
# Good defaults
makestep 1.0 3
rtcsync
driftfile /var/lib/chrony/drift
sudo systemctl enable --now chronyd
Verify It's Working
# Check sources
chronyc sources -v
# Check tracking stats
chronyc tracking
# See if it's serving
chronyc clients
Like what you're reading? Subscribe to HomeLab Starter — free weekly guides in your inbox.
Configuring Clients to Use Your Server
Linux clients (chrony)
# /etc/chrony.conf on client
server 192.168.1.10 iburst prefer
Linux clients (systemd-timesyncd)
# /etc/systemd/timesyncd.conf
[Time]
NTP=192.168.1.10
FallbackNTP=pool.ntp.org
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status
Router-distributed NTP (recommended)
Configure your DHCP server (OPNsense, pfSense, Dnsmasq) to push your NTP server address via DHCP option 42. All DHCP clients automatically receive and use your server without individual configuration.
In OPNsense: Services → DHCPv4 → [Interface] → NTP servers → enter your server IP.
In dnsmasq: dhcp-option=42,192.168.1.10
Stratum 1 with GPS
For sub-millisecond accuracy, connect a GPS module to a Raspberry Pi and use the GPS pulse-per-second (PPS) signal as a hardware reference.
Hardware
- Raspberry Pi 3B+ or 4 (~$35-50)
- GPS module with PPS output: Adafruit Ultimate GPS HAT or u-blox NEO-6M breakout (~$30-40)
- The PPS pin provides a precise 1-pulse-per-second signal with <1µs accuracy
Software: gpsd + chrony with PPS
sudo apt install gpsd gpsd-clients pps-tools
Edit /etc/default/gpsd:
DEVICES="/dev/ttyAMA0 /dev/pps0"
GPSD_OPTIONS="-n"
Verify GPS lock:
cgps -s # Shows satellite info
ppstest /dev/pps0 # Confirms PPS signal
Configure chrony with PPS:
# /etc/chrony.conf on the Pi
# GPS Serial data (lower precision, but provides time of day)
refclock SHM 0 offset 0.5 delay 0.2 refid GPS
# GPS PPS signal (high precision)
refclock PPS /dev/pps0 lock GPS refid PPS
# Upstream peers as fallback
pool pool.ntp.org iburst
# Serve to LAN
allow 192.168.1.0/24
local stratum 1
After GPS acquires a fix (5-15 minutes), chronyc sources shows the PPS source at Stratum 0 and your server becomes Stratum 1.
Accuracy Results
A Raspberry Pi Stratum 1 with GPS PPS typically achieves:
- Accuracy vs. GPS: 1-10 microseconds
- Accuracy for LAN clients: 10-100 microseconds
Compare this to public pool.ntp.org: 1-10 milliseconds.
Monitoring Time Sync
chronyc commands
chronyc sources # List time sources and their offsets
chronyc sourcestats # Statistical data for each source
chronyc tracking # Current sync status
chronyc clients # Who's querying your server
chronyc activity # Active sources count
ntpdate check (quick one-off)
ntpdate -q 192.168.1.10 # Query without adjusting
Prometheus metrics
Chrony exposes metrics via a Prometheus exporter:
chrony_exporter --listen-address=:9123
Add to your Prometheus scrape config and visualize in Grafana.
Firewall Rules
NTP uses UDP port 123. Allow inbound from your LAN:
# iptables
iptables -A INPUT -p udp --dport 123 -s 192.168.1.0/24 -j ACCEPT
# ufw
ufw allow from 192.168.1.0/24 to any port 123 proto udp
Common Issues
Chrony not syncing: Check chronyc sources — if offset is large, chrony waits for it to converge. makestep 1.0 3 in the config forces a step correction during the first 3 updates if offset exceeds 1 second.
"No valid sources" error: Verify upstream NTP is reachable, firewall allows outbound UDP 123.
Clients not using your server: Confirm allow directive includes the client subnet. Test with chronyc clients after a few minutes.
GPS not locking: Check antenna placement (needs sky view), verify UART is enabled on Pi (raspi-config → Interfaces → Serial).
When to Run a Stratum 1
A Stratum 2/3 chrony server (syncing from public NTP) is sufficient for nearly all homelab use cases. The accuracy improvement from Stratum 1 GPS matters if you're:
- Running time-sensitive distributed databases (CockroachDB, TiDB)
- Doing packet timing or network analysis
- Running Kerberos or industrial control systems
- Interested in the project as a learning exercise
For most homelabs: install chrony, point at pool.ntp.org, set allow for your LAN, done. Add the GPS module if you want the accuracy and the project.
