This container runs chrony on Alpine Linux.
chrony is a versatile implementation of the Network Time Protocol (NTP). It can synchronise the system clock with NTP servers, reference clocks (e.g. GPS receiver), and manual input using wristwatch and keyboard. It can also operate as an NTPv4 (RFC 5905) server and peer to provide a time service to other computers in the network.
Architectures officially supported by this Docker container. Simply pulling this container from Docker Hub should retrieve the correct image for your architecture.
Pull and run -- it's this simple.
# pull from docker hub
$> docker pull simonrupf/chronyd
# run ntp
$> docker run --name=ntp \
--restart=always \
--detach \
--publish=123:123/udp \
--cap-add CAP_NET_BIND_SERVICE \
simonrupf/chronyd
# OR run ntp with higher security
$> docker run --name=ntp \
--restart=always \
--detach \
--publish=123:123/udp \
--cap-add CAP_NET_BIND_SERVICE \
--read-only \
--tmpfs=/etc/chrony:rw,mode=1750,uid=100,gid=101 \
--tmpfs=/run/chrony:rw,mode=1750,uid=100,gid=101 \
--tmpfs=/var/lib/chrony:rw,mode=1750,uid=100,gid=101 \
simonrupf/chronyd
Using the docker-compose.yml file included in this git repo, you can build the container yourself (should you choose to).
*Note: this docker-compose files uses the 3.9 compose format, which requires Docker Engine release 19.03.0+
# run ntp
$> docker compose up -d ntp
# (optional) check the ntp logs
$> docker compose logs ntp
(These instructions assume you already have a swarm)
# deploy ntp stack to the swarm
$> docker stack deploy -c docker-compose.yml chronyd
# check that service is running
$> docker stack services chronyd
# (optional) view the ntp logs
$> docker service logs -f chronyd-ntp
Using the vars file in this git repo, you can update any of the variables to reflect your environment. Once updated, simply execute the build then run scripts.
# build ntp
$> ./build.sh
# run ntp
$> ./run.sh
By default, this container uses the NTP pool's time servers. If you'd
like to use one or more different NTP server(s), you can pass this container an NTP_SERVERS
environment variable. This can be done by updating the vars, docker-compose.yml
files or manually passing --env=NTP_SERVERS="..." to docker run.
Below are some examples of how to configure common NTP Servers.
Do note, to configure more than one server, you must use a comma delimited list WITHOUT spaces.
# (default) NTP pool
NTP_SERVERS="0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org,3.pool.ntp.org"
# cloudflare
NTP_SERVERS="time.cloudflare.com"
# google
NTP_SERVERS="time1.google.com,time2.google.com,time3.google.com,time4.google.com"
# alibaba
NTP_SERVERS="ntp1.aliyun.com,ntp2.aliyun.com,ntp3.aliyun.com,ntp4.aliyun.com"
# local (offline)
NTP_SERVERS="127.127.1.1"
If you're interested in a public list of stratum 1 servers, you can have a look at the following lists.
- https://www.advtimesync.com/docs/manual/stratum1.html (Do make sure to verify the ntp server is active as this list does appaer to have some no longer active servers.)
- https://support.ntp.org/Servers/StratumOneTimeServers
It can also be the case that your use-case does not require a stratum 1 server -- most use-cases don't!
This is optional and not enabled by default. If you provide the NOCLIENTLOG=true envivonrment variable,
chrony will be configured to:
Specifies that client accesses are not to be logged. Normally they are logged, allowing statistics to be reported using the clients command in chronyc. This option also effectively disables server support for the NTP interleaved mode.
By default, this project logs informational messages to stdout, which can be helpful when running the
ntp service. If you'd like to change the level of log verbosity, pass the LOG_LEVEL environment
variable to the container, specifying the level (#) when you first start it. This option matches
the chrony -L option, which support the following levels can to specified: 0 (informational), 1
(warning), 2 (non-fatal error), and 3 (fatal error).
Feel free to check out the project documentation for more information at:
By default the UTC timezone is used, however if you'd like to adjust the included tools to display your local
timezone, all you need to do is provide a TZ environment variable following the standard TZ data format.
As an example, using docker-compose.yaml, that would look like this if you were located in Vancouver, Canada:
...
environment:
- TZ=America/Vancouver
...Note that the chronyd service itself does not need or use this setting as the NTP protocol is in UTC and chronyd logs remain in UTC as well.
A separate image variant, published as simonrupf/chronyd:latest-nts and simonrupf/chronyd:<version>-nts is
published that adds chronyd compiled with NTS support. This was the default in builds of the image befor Alpine
3.24, but got dropped due to needing
heavy dependencies.
If all the NTP_SERVERS you have configured support NTS (Network Time Security) you can pass the ENABLE_NTS=true
option to the container to enable it. As an example, using docker-compose.yaml, that would look like this:
...
environment:
- NTP_SERVERS=time.cloudflare.com
- ENABLE_NTS=true
...If any of the NTP_SERVERS you have configured does not support NTS, you will see a message like the
following during startup:
NTS-KE session with 164.67.62.194:4460 (tick.ucla.edu) timed out
This option enables the control of the system clock.
By default, chronyd will not try to make any adjustments of the clock. It will assume the clock is free running and still track its offset and frequency relative to the estimated true time. This allows chronyd to run without the capability to adjust or set the system clock in order to operate as an NTP server.
Enabling the control requires starting the service as root, granting SYS_TIME capability and a container run-time allowing that access:
...
user: "0:0"
cap_add:
- SYS_TIME
environment:
- ENABLE_SYSCLK=true
...If you have a /dev/ptp0, either a real hardware clock or virtual one provided by a VM host
you can enable the use of it by passing the device to the container. As an example,
using docker-compose.yaml, that would look like this:
...
devices:
- /dev/ptp0:/dev/ptp0This will allow chronyd to use the PTP clock as a reference clock. A virtual clock simply provides the host's system time with great precision and stability; whether that time is accurate depends on the host provider. In our experience, some VPS vendors give pretty good time (off by milliseconds), while others are off by seconds.
For information on configuring the host to have a virtual PTP clock, see the following:
A separate image variant β published as simonrupf/chronyd:latest-gps and
simonrupf/chronyd:<version>-gps β adds gpsd and serves as a stratum-1
NTP source when fed by a serial GPS receiver (e.g. a USB UART GPS dongle).
It is built from Dockerfile.gps and is otherwise functionally identical
to the default image, so all environment variables and volumes documented
above still apply.
The image bundles a tiny s6
supervision tree (s6-svscan as PID 1, one s6-supervise per daemon) so
that chronyd and gpsd are cleanly co-supervised: signals are forwarded,
a crashed gpsd is restarted automatically, and a crashed chronyd brings
the container down so Docker's restart policy can recreate it.
services:
ntp-gps:
image: simonrupf/chronyd:latest-gps
container_name: ntp
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
environment:
- GPS_DEVICE=/dev/ttyUSB0 # required
- GPS_BAUD=9600 # default 9600
- GPS_REFCLOCK_OFFSET=0.0 # default 0.0; compensate serial latency
- GPS_PPS=false # set true to also consume PPS via SHM seg 1
sysctls:
# gpsd 3.26 unconditionally binds an IPv6 listening socket; Docker
# disables IPv6 on container loopback by default, so without this
# sysctl gpsd exits with "Can't bind to IPv6/TCP port β¦ Address not
# available". Alternative: use `network_mode: host`.
net.ipv6.conf.lo.disable_ipv6: "0"
ports:
- 123:123/udp
group_add:
- dialout # so the chrony user can read the serial device
restart: alwaysdocker run equivalent of that sysctl:
--sysctl net.ipv6.conf.lo.disable_ipv6=0
When the image starts, the entrypoint generates chrony.conf (same logic
as the default image) and additionally appends:
refclock SHM 0 refid GPS offset ${GPS_REFCLOCK_OFFSET} precision 1e-1
β¦and, when GPS_PPS=true, also:
refclock SHM 1 refid PPS precision 1e-7 prefer
s6-svscan then takes over as PID 1 and supervises:
chronyd: same arguments as the default image (driven byLOG_LEVEL,ENABLE_SYSCLK, etc.)gpsd:gpsd -N -F /run/chrony/gpsd.sock -s ${GPS_BAUD} ${GPS_DEVICE}
The healthcheck verifies both services:
chronyc -n tracking && [ "$(s6-svstat -u /run/chrony/services/gpsd)" = "true" ]
If gpsd dies persistently (e.g. the GPS is unplugged), s6 keeps
restarting it and Docker marks the container unhealthy after the default
three consecutive failures. chronyd itself keeps running and continues
to serve clients at stratum 2 from NTP_SERVERS until the receiver is
back.
The serial device must be readable by the chrony user. The easiest
options are:
chmod g+r /dev/ttyUSB0 && chown :dialout /dev/ttyUSB0on the host plusgroup_add: [dialout]on the container, or- run the container as root with
user: "0:0"if you don't mind the privilege.
The -gps image is a few MB larger than the default image (gpsd + s6).
If you don't need a GPS refclock, stick with simonrupf/chronyd:latest β
the default image is unchanged.
From any machine that has ntpdate you can query your new NTP container with the follow
command:
$> ntpdate -q <DOCKER_HOST_IP>
Here is a sample output from my environment:
$> ntpdate -q 10.13.13.9
server 10.13.1.109, stratum 4, offset 0.000642, delay 0.02805
14 Mar 19:21:29 ntpdate[26834]: adjust time server 10.13.13.109 offset 0.000642 sec
If you see a message, like the following, it's likely the clock is not yet synchronized. You should see this go away if you wait a bit longer and query again.
$> ntpdate -q 10.13.13.9
server 10.13.13.9, stratum 16, offset 0.005689, delay 0.02837
11 Dec 09:47:53 ntpdate[26030]: no server suitable for synchronization found
To see details on the ntp status of your container, you can check with the command below on your docker host:
$> docker exec ntp chronyc tracking
Reference ID : D8EF2300 (time1.google.com)
Stratum : 2
Ref time (UTC) : Sun Mar 15 04:33:30 2020
System time : 0.000054161 seconds slow of NTP time
Last offset : -0.000015060 seconds
RMS offset : 0.000206534 seconds
Frequency : 5.626 ppm fast
Residual freq : -0.001 ppm
Skew : 0.118 ppm
Root delay : 0.022015510 seconds
Root dispersion : 0.001476757 seconds
Update interval : 1025.2 seconds
Leap status : Normal
Here is how you can see a peer list to verify the state of each ntp source configured:
$> docker exec ntp chronyc sources
210 Number of sources = 2
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^+ time.cloudflare.com 3 10 377 404 -623us[ -623us] +/- 24ms
^* time1.google.com 1 10 377 1023 +259us[ +244us] +/- 11ms
Finally, if you'd like to see statistics about the collected measurements of each ntp source configured:
$> docker exec ntp chronyc sourcestats
210 Number of sources = 2
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev
==============================================================================
time.cloudflare.com 35 18 139m +0.014 0.141 -662us 530us
time1.google.com 33 13 128m -0.007 0.138 +318us 460us
Are you seeing messages like these and wondering what is going on?
$ docker logs -f ntps
[...]
2021-05-25T18:41:40Z System clock wrong by -2.535004 seconds
2021-05-25T18:41:40Z Could not step system clock
2021-05-25T18:42:47Z System clock wrong by -2.541034 seconds
2021-05-25T18:42:47Z Could not step system clock
Good question! Since chronyd is running with the -x flag, it will not try to control
the system (container host) clock. This of course is necessary because the process does not
have priviledge (for good reason) to modify the clock on the system.
Like any host on your network, simply use your preferred ntp client to pull the time from the running ntp container on your container host.