Fix Locale, Charset and Timezone Issues
Fix broken locale, charset, and timezone settings on Linux—covering locale-gen, LC_* variables, timedatectl, container images, and cron environment traps.
Before you start
- ▸sudo or root access on the target system
- ▸A terminal with an active shell session
- ▸Basic familiarity with editing files and running commands as root
- ▸For container fixes: Docker or Podman installed and a working Dockerfile
Broken locale or timezone settings cause cascading oddities: garbled output in terminals, wrong timestamps in logs, cron jobs firing at unexpected hours, and applications refusing to start with cryptic errors about unsupported charsets. These problems are especially common after minimal OS installs, Docker base images, or SSH sessions that inherit the wrong environment. Here is how to diagnose and fix them cleanly.
Understanding the Moving Parts
Three independent systems control the territory you need to cover:
- Locale – controls language, number formatting, date display, and collation. Set via
LANG,LC_ALL, and the family ofLC_*variables. - Charset / encoding – almost always UTF-8 in 2024, but containers and legacy systems sometimes ship with ASCII or ISO-8859 defaults that break Unicode.
- Timezone – managed by
timedatectlon systemd hosts and by theTZenvironment variable or/etc/timezoneelsewhere.
Diagnose First
Before changing anything, capture the current state:
locale
Healthy output on a UTF-8 system looks like:
# output will vary; a healthy system shows something like:
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
# ... (all entries populated, none say "POSIX" or show errors)
Warning signs: blank values, POSIX, C, or the shell printing locale: Cannot set LC_* errors.
timedatectl status
Check for Time zone and whether NTP service is active. Also verify the system clock is correct; a wrong timezone and wrong clock are separate problems.
Generate and Set a Locale
Debian and Ubuntu
Locales must be generated before they can be used. The locale-gen tool reads /etc/locale.gen and compiles the requested locales.
sudo dpkg-reconfigure locales
This is the preferred interactive method. It presents a checklist of locales and asks which one to set as the system default. For scripted or headless installs:
sudo sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
sudo locale-gen
sudo update-locale LANG=en_US.UTF-8
Log out and back in (or run exec bash) so the new /etc/default/locale is sourced.
Fedora, RHEL, Rocky, AlmaLinux
On RPM-based systems, locales are included in glibc-langpack-* packages rather than generated at runtime.
sudo dnf install glibc-langpack-en
sudo localectl set-locale LANG=en_US.UTF-8
localectl writes to /etc/locale.conf and takes effect on next login or reboot. To apply immediately in the current shell:
source /etc/locale.conf
export LANG
Arch Linux
Uncomment desired locales in /etc/locale.gen, then generate and set:
sudo sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
sudo locale-gen
echo 'LANG=en_US.UTF-8' | sudo tee /etc/locale.conf
Fix LC_* Variable Pollution
A common source of errors is an SSH client forwarding its own LC_* variables to a server that does not have those locales installed. You see errors like:
# perl: warning: Setting locale failed.
# perl: warning: Please check that your locale settings:
# LANGUAGE = (unset),
# LC_ALL = (unset),
# LANG = "en_AU.UTF-8"
# are supported and installed on the system.
Two approaches: fix the server, or stop the client from forwarding.
Stop SSH locale forwarding on the client – comment out or remove SendEnv LANG LC_* from ~/.ssh/config or /etc/ssh/ssh_config.
Stop the server from accepting forwarded locales – remove or comment out AcceptEnv LANG LC_* in /etc/ssh/sshd_config, then restart sshd:
sudo systemctl restart sshd
If LC_ALL is set to something broken, it overrides every other LC_* setting. Unset it explicitly:
unset LC_ALL
export LANG=en_US.UTF-8
For a permanent per-user fix, add those lines to ~/.bashrc or ~/.profile.
Set the Timezone
On any modern systemd host:
timedatectl list-timezones | grep -i america
timedatectl set-timezone America/New_York
This updates the symlink /etc/localtime -> /usr/share/zoneinfo/America/New_York and writes /etc/timezone (on Debian-family) automatically. No reboot required; the change takes effect instantly for new processes.
On containers and minimal systems without systemd, do it manually:
ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime
echo 'America/New_York' > /etc/timezone
Container-Specific Gotchas
Docker and Podman base images (Alpine, Debian-slim, UBI-minimal) deliberately strip locale data and timezone files to save space.
Alpine – install the tzdata package and desired locale support:
apk add --no-cache tzdata
cp /usr/share/zoneinfo/UTC /etc/localtime
echo 'UTC' > /etc/timezone
Debian-slim – install locales and generate inside the Dockerfile:
RUN apt-get update && apt-get install -y --no-install-recommends locales \
&& sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen \
&& locale-gen
ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
Passing the timezone via an environment variable is the cleanest runtime method:
docker run -e TZ=Europe/London myimage
This requires tzdata to be present in the image; the TZ variable is then read by glibc at runtime without needing /etc/localtime.
Cron and Non-Login Shell Gotchas
Cron runs jobs in a minimal environment. It does not source /etc/profile, ~/.bashrc, or /etc/locale.conf. Scripts that work fine interactively fail under cron because locale and timezone variables are absent.
The cleanest fix is to set variables at the top of the crontab:
crontab -e
LANG=en_US.UTF-8
TZ=America/New_York
0 2 * * * /usr/local/bin/my-backup.sh
Alternatively, export the variables inside the script itself before any locale-sensitive operations. For systemd timer units (the modern replacement for cron), set Environment= in the unit file:
[Service]
Environment=LANG=en_US.UTF-8
Environment=TZ=America/New_York
ExecStart=/usr/local/bin/my-backup.sh
Verify Everything
After making changes, open a new shell or SSH session (existing sessions cache the old environment):
locale
date
timedatectl status
Confirm LANG shows your chosen locale, date returns the correct local time, and timedatectl shows the correct timezone with NTP active. A quick UTF-8 sanity check:
echo $'\xc3\xa9' # should print: é
If that prints a question mark or escape sequence, the terminal or locale is still not UTF-8.
Troubleshooting
locale: Cannot set LC_CTYPE to default locale– the locale listed in your environment is not installed. Runlocale -ato see what is available, then generate or install the missing one.- Time correct but timezone wrong after reboot – check that
/etc/localtimeis a symlink to the right zoneinfo file, not a stale copy. Re-runtimedatectl set-timezone. - Hardware clock drifting – this is separate from timezone. Ensure
systemd-timesyncdorchronydis running:timedatectl show | grep NTP. - Python raises
UnicodeDecodeErrorin a script that worked before – Python 3 respectsLANG; if the locale isCorPOSIXit defaults to ASCII. SetLANG=en_US.UTF-8or passPYTHONUTF8=1as an environment variable (Python 3.7+). - Wayland sessions ignoring
/etc/locale.conf– Wayland compositors launched via systemd user sessions sourceenvironment.dsnippets. Create~/.config/environment.d/locale.confwithLANG=en_US.UTF-8if your desktop environment is not picking up the system locale.
Frequently asked questions
- What is the difference between LANG and LC_ALL?
- LANG is the default fallback for any LC_* category that is not explicitly set. LC_ALL overrides every single LC_* variable unconditionally, including LANG. Setting LC_ALL=C is a common debugging trick but breaks Unicode applications; unset it when you are done.
- Do I need to reboot after changing the timezone with timedatectl?
- No. timedatectl updates /etc/localtime immediately and new processes see the change at once. Long-running daemons may need a restart to pick up the new timezone if they cached it at startup.
- Why does locale work in my SSH session but fail in cron?
- Cron runs a minimal environment that does not source your shell profile or /etc/locale.conf. You must explicitly set LANG and TZ at the top of your crontab or inside the script itself.
- My container has UTC time but the TZ variable is set correctly—why?
- The TZ variable is only honoured by glibc if the tzdata package (and thus /usr/share/zoneinfo) is present in the image. Minimal images like Alpine-slim or distroless omit tzdata; install it explicitly.
- How do I make a Wayland desktop session pick up the locale I set in /etc/locale.conf?
- Create ~/.config/environment.d/locale.conf containing LANG=en_US.UTF-8 (one variable per line). Systemd user sessions read this directory and export the values to the compositor and all child processes.
Related guides
Back Up Linux with Borg or restic
Set up encrypted, deduplicated backups with BorgBackup or restic: local and remote repos, retention pruning, restoring files, and systemd timer scheduling.
How to Check Disk Health with SMART
Learn to use smartctl to read SMART attributes, run drive self-tests, and identify early warning signs of HDD and SSD failure before data loss occurs.
Debug systemd Units that Won't Start
Learn a repeatable workflow to debug systemd services that won't start: status output, journalctl, systemd-analyze verify, and safe override.conf patches.
Linux Server Disaster Recovery Checklist
A practical Linux server disaster recovery checklist: what to back up, RTO/RPO planning, immutable off-site copies, automated restore drills, and verification.