$linuxjunkies
>

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.

BeginnerUbuntuDebianFedoraArch8 min readUpdated June 7, 2026

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 of LC_* 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 timedatectl on systemd hosts and by the TZ environment variable or /etc/timezone elsewhere.

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. Run locale -a to see what is available, then generate or install the missing one.
  • Time correct but timezone wrong after reboot – check that /etc/localtime is a symlink to the right zoneinfo file, not a stale copy. Re-run timedatectl set-timezone.
  • Hardware clock drifting – this is separate from timezone. Ensure systemd-timesyncd or chronyd is running: timedatectl show | grep NTP.
  • Python raises UnicodeDecodeError in a script that worked before – Python 3 respects LANG; if the locale is C or POSIX it defaults to ASCII. Set LANG=en_US.UTF-8 or pass PYTHONUTF8=1 as an environment variable (Python 3.7+).
  • Wayland sessions ignoring /etc/locale.conf – Wayland compositors launched via systemd user sessions source environment.d snippets. Create ~/.config/environment.d/locale.conf with LANG=en_US.UTF-8 if your desktop environment is not picking up the system locale.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch 2024.05

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