Wrong time on your server? Set the time zone and keep the clock in sync

Orders saved an hour off. A scheduled post that goes out late. Two-factor codes that don't work. These often come from the server's clock or time zone. This guide fixes the clock first, then makes PHP, MariaDB and cron agree on the time.

We tested this guide on Ubuntu 24.04 (PHP 8.3, MariaDB 10.11) and Debian 13 (PHP 8.4, MariaDB 11.8), each in a fresh container with systemd. One thing we could not test there: a container can't set the clock, so the time sync service stays off. We checked the sync tools (systemd-timesyncd and chrony) as far as a container allows.

1. Look at the clock

timedatectl

You'll see something like this:

               Local time: Fri 2026-11-13 13:36:12 UTC
           Universal time: Fri 2026-11-13 13:36:12 UTC
                 RTC time: n/a
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Two lines matter most:

  • System clock synchronized: yes means the clock is right.
  • NTP service: active means a service keeps it right, by asking time servers on the internet.

New Ubuntu and Debian servers start on UTC. That's fine, and many people keep it (see step 3).

2. Keep the clock in sync

A clock that drifts by even a minute breaks things. Authenticator codes for SSH two-factor stop matching, HTTPS checks can fail, and logs from different places no longer line up. If NTP service is inactive, turn it on:

sudo timedatectl set-ntp true

If you get Failed to set ntp: NTP not supported, no sync service is installed. Ubuntu 24.04 and Debian 13 use a small one called systemd-timesyncd. Install it, then run the command again:

sudo apt install systemd-timesyncd
sudo timedatectl set-ntp true

See which time server it uses and when it last checked:

timedatectl timesync-status

Time servers answer on UDP port 123. A firewall that allows outgoing traffic (the UFW default) is fine. If your hosting company blocks it, use their own time server.

Prefer chrony? It's another sync service, a little more precise. Installing it removes systemd-timesyncd (we saw this on both systems), so you never run two at once:

sudo apt install chrony
chronyc tracking
chronyc sources

In chronyc sources, the line starting with ^* is the server your clock follows now. For a normal website, either service is fine. Pick one and leave it running.

3. Pick a time zone (UTC is a good choice)

You have two sensible options:

  • Keep the server on UTC, and set your local time zone inside the app (steps 4 and 5). UTC has no summer time, so there's no hour that happens twice or not at all. Logs from different servers line up.
  • Set the server to your local time, if one team in one place reads the logs and you want times to match the wall clock.

To change it, find the exact name first. Names are Region/City:

timedatectl list-timezones | grep Berlin
sudo timedatectl set-timezone Europe/Berlin
date
Fri Nov 13 14:36:19 CET 2026

A typo gives Failed to set time zone: Invalid or not installed time zone. Nothing changes until the name is right. To go back to UTC: sudo timedatectl set-timezone UTC.

4. PHP doesn't follow the server

This one surprises people. After we set the server to Berlin, PHP still said UTC, on both systems:

php -r 'echo date_default_timezone_get(), " ", date("H:i T"), PHP_EOL;'
UTC 13:36 UTC

PHP has its own setting, date.timezone. Debian and Ubuntu leave it empty, and then PHP uses UTC. To set it for both PHP-FPM (your websites) and the command line, use your PHP version in place of 8.4 (ls /etc/php shows it):

echo "date.timezone = Europe/Berlin" | sudo tee /etc/php/8.4/mods-available/timezone.ini
sudo phpenmod timezone
sudo systemctl reload php8.4-fpm

phpenmod links the file into both the fpm and cli settings folders. Run the php -r line again, and it now says Europe/Berlin. We also checked a page served by Nginx and PHP-FPM: a reload was enough.

WordPress and Laravel use their own setting and ignore date.timezone:

  • WordPress: Settings → General → Timezone. WordPress keeps PHP on UTC and converts times itself. Pick a city (such as Berlin), not a fixed offset like UTC+1, so summer time is handled.
  • Laravel: 'timezone' in config/app.php (UTC by default). Many teams keep it on UTC and only convert when showing dates to users. After a change, run php artisan config:cache if you cache the config.

5. MariaDB keeps the old time zone until it restarts

MariaDB reads the server's time zone once, when it starts. In our test, after changing the server to Berlin, it still said UTC:

sudo mariadb -e "SELECT @@global.time_zone, @@system_time_zone, NOW();"
SYSTEM  UTC  2026-11-13 13:36:18

A restart fixes it. It stops the database for a few seconds, so pick a quiet moment:

sudo systemctl restart mariadb
SYSTEM  CET  2026-11-13 14:36:19

Names like "Europe/Berlin" don't work yet in MariaDB. Its time zone tables start empty, so CONVERT_TZ(..., 'UTC', 'Europe/Berlin') returns NULL. Some apps need these names. Load them from the system's list:

sudo sh -c 'mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb mysql'

It prints nothing when it works. You can run it again safely. Check the result (we got about 500 names):

sudo mariadb -e "SELECT COUNT(*) FROM mysql.time_zone_name;"

On MySQL, the tool is called mysql_tzinfo_to_sql. When the system's time zone data is updated later (the tzdata package), run this again.

One more thing: a TIMESTAMP column is stored as UTC and shown in the connection's time zone. A DATETIME column is stored exactly as written. If an app saved DATETIME values in local time for years, changing time zones won't fix the old rows. Test on a copy first.

6. Cron jobs and the time change

Cron runs jobs in the server's time zone. In our test, cron noticed the change by itself, without a restart: a job set for 14:38 ran at 14:38 Berlin time. But when we moved the clock forward one hour, cron also ran a job from the "skipped" hour once, just like on a summer-time change. So:

  • Change the time zone at a quiet moment, not just before a backup or a big import.
  • Check your jobs' times afterwards: sudo crontab -l, crontab -l for each site user, and /etc/cron.d/. A job set for 0 3 * * * now runs at 3:00 in the new time zone.
  • Timers (systemctl list-timers) also use the server's time zone. They showed the new zone right away.

If a job doesn't run at all, see cron job not running.

If something goes wrong

  • Clock still wrong, "synchronized: no": check timedatectl timesync-status (or chronyc sources) and the service log: journalctl -u systemd-timesyncd -n 30. Usually UDP port 123 is blocked.
  • Times on the site are an hour off: compare all four: date, the php -r line, the MariaDB query, and your app's setting (WordPress or Laravel). One of them disagrees.
  • Off by exactly one hour, only half the year: something uses a fixed offset (like +01:00 or "UTC+1") instead of a city name.
  • Two-factor codes fail: fix the sync first (step 2), not the time zone. The codes use UTC.

Official sources

Next steps: on WordPress, make scheduled posts reliable with a real cron job. On Laravel, see queue workers and the scheduler.

ServerNest is being built to check that every server's clock is in sync and warn you when it isn't. Join early access to try it first.

← Back to the security blog