← All articles
The clock shows 23:27 on a digital display.

Self-Hosting a Time Server with Chrony: Accurate NTP for Your Network

Infrastructure 2026-02-14 · 7 min read ntp time-sync chrony infrastructure
By Selfhosted Guides Editorial TeamSelf-hosting practitioners covering open source software, home lab infrastructure, and data sovereignty.

Time synchronization is one of those things you don't think about until it breaks. And when it breaks, everything breaks. TLS certificates fail validation because the server thinks it's 2019. Distributed databases lose consistency because two nodes disagree on what "now" means. Log correlation becomes impossible when your servers are seconds apart. Cron jobs fire at the wrong moment. Kerberos authentication fails completely if clocks drift more than five minutes.

Photo by unavailable parts on Unsplash

Running your own NTP time server ensures every device on your network agrees on what time it is, without relying on external services. Chrony is the modern tool for the job — it's faster, more accurate, and more resource-efficient than the classic ntpd.

Chrony NTP server architecture diagram showing upstream time sources and local network clients

Why Run a Local Time Server

Most devices sync time from public NTP pools (pool.ntp.org). That works fine for single machines, but running your own time server gives you:

When you don't need this

If you have three servers and a couple of desktops, just point them at pool.ntp.org and call it a day. A local NTP server makes sense when you have:

Why Chrony Over ntpd

The classic ntpd (the reference NTP daemon) has been the standard for decades. Chrony was written as a modern replacement and has real advantages:

Feature chrony ntpd
Initial sync speed Seconds Minutes
Accuracy Sub-microsecond Microseconds
Intermittent connectivity Handles well Struggles
Resource usage ~1 MB RAM ~3 MB RAM
Security (NTS support) Yes No
Configuration complexity Simple Moderate
Default on RHEL/Fedora Yes (since RHEL 7) No
Default on Ubuntu Optional Yes (via timedatectl)

Chrony synchronizes faster after boot, handles networks that go up and down (laptops, VPN connections), and supports Network Time Security (NTS) for authenticated time. Unless you have a specific reason to run ntpd, use chrony.

Setting Up Chrony as a Time Server

Option 1: Bare metal / VM (recommended)

Time synchronization works best when the daemon has direct access to the system clock. Containers add a layer of abstraction that can reduce accuracy. For a production time server, install chrony directly on the host.

Fedora / RHEL / CentOS:

sudo dnf install chrony
sudo systemctl enable --now chronyd

Debian / Ubuntu:

sudo apt install chrony
sudo systemctl enable --now chronyd

Arch Linux:

sudo pacman -S chrony
sudo systemctl enable --now chronyd

Configuring chrony as a server

Edit /etc/chrony.conf (or /etc/chrony/chrony.conf on Debian-based systems):

# Upstream NTP servers — use geographically close sources
# The 'iburst' option sends a burst of requests on startup for faster sync
server time.cloudflare.com iburst nts
server time.google.com iburst
server 0.pool.ntp.org iburst
server 1.pool.ntp.org iburst

# Allow NTP clients on your local network
allow 192.168.0.0/16
allow 10.0.0.0/8
allow 172.16.0.0/12

# Serve time even when not fully synchronized
# (useful if upstream servers are temporarily unreachable)
local stratum 10

# Record the rate at which the system clock gains/loses time
driftfile /var/lib/chrony/drift

# Enable kernel synchronization of the hardware clock
rtcsync

# Step the system clock if the offset is larger than 1 second
# during the first three clock updates
makestep 1.0 3

# Log statistics for monitoring
logdir /var/log/chrony
log measurements statistics tracking

Restart chrony after making changes:

sudo systemctl restart chronyd

Firewall configuration

NTP uses UDP port 123. Open it for your local network:

# firewalld (Fedora, RHEL)
sudo firewall-cmd --permanent --add-service=ntp
sudo firewall-cmd --reload

# ufw (Ubuntu)
sudo ufw allow from 192.168.0.0/16 to any port 123 proto udp

# iptables
sudo iptables -A INPUT -p udp -s 192.168.0.0/16 --dport 123 -j ACCEPT

Only allow NTP connections from your local network. There's no reason to serve time to the entire internet.

Option 2: Docker deployment

If you prefer containers (understanding the accuracy trade-off), chrony can run in Docker:

version: "3.8"

services:
  chrony:
    container_name: chrony
    image: cturra/ntp:latest
    ports:
      - "123:123/udp"
    environment:
      NTP_SERVERS: "time.cloudflare.com,time.google.com,0.pool.ntp.org"
      LOG_LEVEL: "0"
    cap_add:
      - SYS_TIME
    restart: always

The SYS_TIME capability is required — chrony needs to adjust the system clock. Without it, the container starts but can't actually synchronize time.

docker compose up -d

Note that running NTP in a container means the container adjusts the host's system clock through the kernel. This works, but the additional abstraction layer means you'll get millisecond accuracy rather than microsecond accuracy. For most homelabs, that's fine.

Like what you're reading? Subscribe to Self-Hosted Weekly — free weekly guides in your inbox.

Verifying the Setup

Check chrony's status

# Show current time sources and their status
chronyc sources -v

# Show detailed tracking information
chronyc tracking

# Show statistics about each source
chronyc sourcestats

The sources output looks like this:

MS Name/IP address         Stratum Poll Reach LastRx Last sample
===============================================================================
^* time.cloudflare.com           3   6   377    34   +124us[ +135us] +/-   12ms
^+ time.google.com               1   6   377    33   -234us[ -223us] +/-   15ms
^+ 0.pool.ntp.org                2   6   377    35   +456us[ +467us] +/-   22ms
^+ 1.pool.ntp.org                2   6   377    34   -89us[  -78us] +/-   18ms

Key indicators:

Check from a client

From another machine on your network:

# Test time sync from a client
chronyc -h 192.168.1.100 tracking

# Or using ntpdate for a quick check
ntpdate -q 192.168.1.100

Configuring Clients

Linux clients

Point chrony on client machines to your local server:

# /etc/chrony.conf on client machines
server 192.168.1.100 iburst

# Optionally keep pool servers as fallback
pool pool.ntp.org iburst maxsources 2

Or use systemd-timesyncd (simpler, good enough for clients):

# /etc/systemd/timesyncd.conf
[Time]
NTP=192.168.1.100
FallbackNTP=pool.ntp.org
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status

Docker containers

Containers inherit the host's clock, so they automatically get accurate time if the host is synchronized. No per-container NTP configuration needed.

Network equipment

Most routers, switches, and access points have NTP settings. Point them to your local server's IP address. This ensures your firewall logs, router logs, and server logs all have consistent timestamps.

Windows clients

For Windows machines on your network:

# Set NTP server (PowerShell as admin)
w32tm /config /manualpeerlist:"192.168.1.100" /syncfromflags:manual /update
Restart-Service w32time
w32tm /resync

Network Time Security (NTS)

NTS is the modern replacement for NTP authentication. It uses TLS to verify that time responses actually came from the server you expect and haven't been tampered with in transit. Without NTS, NTP traffic is unauthenticated — a man-in-the-middle could feed your machines incorrect time.

To enable NTS on your chrony server:

# In /etc/chrony.conf — use NTS-capable upstream servers
server time.cloudflare.com iburst nts
server nts.netnod.se iburst nts

# Enable NTS for clients connecting to this server
ntsserverkey /etc/pki/tls/private/chrony-nts.key
ntsservercert /etc/pki/tls/certs/chrony-nts.crt

You'll need a TLS certificate for your server. Let's Encrypt works fine:

# Using certbot
sudo certbot certonly --standalone -d ntp.yourdomain.com

# Then reference the certs in chrony.conf
ntsserverkey /etc/letsencrypt/live/ntp.yourdomain.com/privkey.pem
ntsservercert /etc/letsencrypt/live/ntp.yourdomain.com/fullchain.pem

NTS is worth enabling if you're serving time to machines outside your trusted LAN. For a home network behind a firewall, standard NTP is fine.

Chrony monitoring output showing time source synchronization status

Monitoring

Tracking accuracy over time

Chrony logs statistics that you can graph with Prometheus + Grafana. The chrony_exporter project exposes chrony metrics:

services:
  chrony-exporter:
    container_name: chrony_exporter
    image: superq/chrony-exporter:latest
    ports:
      - "9123:9123"
    command:
      - "--chrony.address=host.docker.internal:323"
    restart: always

Key metrics to watch:

Simple health check

A basic script to alert if chrony loses sync:

#!/bin/bash
# Check if chrony is synchronized
LEAP=$(chronyc tracking | grep "Leap status" | awk '{print $4}')
if [ "$LEAP" != "Normal" ]; then
    echo "WARNING: Chrony is not synchronized! Leap status: $LEAP"
    # Send alert via your preferred method
fi

Stratum Explained

NTP uses a hierarchy called "stratum" to indicate how many hops a time source is from an atomic clock:

Each hop adds a small amount of inaccuracy, but on a LAN, the added offset at each stratum is negligible (microseconds). The local stratum 10 directive in the config is a fallback — it tells chrony to serve time at stratum 10 if all upstream sources are unreachable. This way, at least your machines stay in sync with each other even during an internet outage.

Honest Trade-offs

Run a local chrony server if you:

Skip it if you:

The bottom line: A local NTP server is low-maintenance infrastructure that prevents an entire class of subtle, hard-to-debug problems. Chrony makes it easy — install it, point it at a few upstream servers, open port 123, and forget about it. Your future self will thank you the first time you need to correlate logs across five servers during an incident.

Get free weekly tips in your inbox. Subscribe to Self-Hosted Weekly