SensorsIot / IOTstack

Docker stack for getting started on IOT on the Raspberry PI
GNU General Public License v3.0
1.45k stars 308 forks source link

Configuring time zone #284

Open senarvi opened 3 years ago

senarvi commented 3 years ago

Many services display time incorrectly, unless the local time zone is configured by setting the TZ environment variable. Some of the service.yml files in IOTstack set the TZ environment variable to Etc/UTC, Europe/Paris, or Europe/Berlin - wherever the person is living who contributed that service.

Docker Compose supports substituting environment variables in Compose files. We could instead use the following in service.yml:

  environment:
    - TZ=${TZ}

And the user could configure time zone by setting the TZ environment variable, or creating a file .env in the IOTstack directory that contains something like:

TZ=Europe/Paris 
877dev commented 3 years ago

@senarvi thanks for this post, I also recently noticed the same issue.

I temporarily fixed it by adding the timezone manually to each container in docker-compose.yml. It could be added instead to compose-override.yml if the menu is being used.

But your suggestion seems the best one!

senarvi commented 3 years ago

I didn't know about docker-compose.override.yml. That can also be used to modify the configuration without having to modify docker-compose.yml (which will be overwritten by the menu).

Paraphraser commented 3 years ago

You can't simply add TZ to every container's definition (regardless of which method you use) and expect it to "just work". A container actually has to support it.

I've experimented with this before without getting anywhere but @877dev raising the matter on Discord made me start thinking about it again and I may've stumbled across the magic incantation.

I'm currently playing with AdGuard Home. It doesn't respect TZ so it's a good test container.

What's my Raspberry Pi's time zone?

$ cat /etc/timezone 
Australia/Sydney

At the moment, this is UTC+11.

What time does my RPi think it is?

$ date
Tue 30 Mar 2021 03:40:57 PM AEDT

The work-in-progress service definition:

$ cat docker-compose.yml
version: '3.6'

services:

  adguardhome:
    container_name: adguardhome
    image: adguard/adguardhome
    restart: unless-stopped
    environment:
      - TZ=Australia/Sydney
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8089:8089/tcp"
      - "3001:3000/tcp"
    volumes:
       - ./volumes/adguardhome/workdir:/opt/adguardhome/work
       - ./volumes/adguardhome/confdir:/opt/adguardhome/conf

Bring it up.

$ UP
Creating adguardhome ... done

Does the container receive the TZ environment variable?

$ docker exec adguardhome ash -c 'echo $TZ'
Australia/Sydney

You betcha! What time does the container think it is?

$ docker exec adguardhome date
Tue Mar 30 04:41:51 UTC 2021

UTC. 😢

Now for the magic incantation:

$ docker exec adguardhome apk add tzdata
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/armv7/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/armv7/APKINDEX.tar.gz
(1/1) Installing tzdata (2021a-r0)
Executing busybox-1.31.1-r19.trigger
OK: 8 MiB in 17 packages

What does the container think the time is now?

$ docker exec adguardhome date
Tue Mar 30 15:42:17 AEDT 2021

😀 We're cookin' with renewables (gas being so passé).

It seems that that is all there is to it, at least for images built on top of Alpine.

The problem we face is implementing this. If we want to do it at the IOTstack level, we need to use Dockerfiles like we're doing for NodeRed. That creates problems of its own for users who find it difficult to understand that different approaches are needed to update images we use "as is" from DockerHub vs those where Dockerfiles are involved.

One step removed is trying to convince the upstream image provider (in this example that would be the AdGuard Home team) to add tzdata to their image. Probably do-able but it could easily become a bit of an albatross if IOTstack committed to trying to make sure every container respected TZ.

One step further removed again is trying to convince the base image providers (in this example that'd be Alpine) to do it there. I don't think that would wrong-foot any downstream user of the image who had already done it but the starting position with these base images seems to be a combination of "bare minimum" plus "no unnecessary changes" so I wouldn't hold my breath.

If anyone has any ideas on how we might go about this, I'm all ears!

Paraphraser commented 3 years ago

Err, hold the phone. I'm currently also building Mosquitto from a Dockerfile (long story). I added TZ to the service definition and tzdata to the local image but - nada. More investigation needed.

Paraphraser commented 3 years ago

I tried lots of things with Mosquitto but had zero success in getting it to behave.

Of the containers I'm running, the following respect TZ:

nodered: Tue Mar 30 20:31:16 AEDT 2021
nextcloud_db: Tue Mar 30 20:31:17 AEDT 2021 (which implies MariaDB)
grafana: Tue Mar 30 20:31:17 AEDT 2021
wireguard: Tue Mar 30 20:31:18 AEDT 2021
influxdb: Tue Mar 30 20:31:18 AEDT 2021
pihole: Tue Mar 30 20:31:18 AEDT 2021

I didn't have any luck with Mosquitto or NextCloud.

mosquitto: Tue Mar 30 09:31:16 UTC 2021
nextcloud: Tue Mar 30 09:31:17 UTC 2021
877dev commented 3 years ago

Excellent work, quite interesting how it works. So it does not actually hurt to declare the TZ variable, it's just not used in unsupported containers.

As I just swapped back to IOTstack old menu, there are references to .env files. I presume the TZ could be added in the .env OR in docker-compose.yml ?

You can add these to the list that respect TZ:

zigbee2mqtt: Tue Mar 30 16:21:48 BST 2021
blynk_server: Tue Mar 30 16:22:13 BST 2021

Mosquitto does not respect TZ for me either:

mosquitto: Tue Mar 30 15:27:00 UTC 2021

Portainer CE really does not like it:

$ ~/IOTstack $ docker exec portainer-ce date
OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"date\": executable file not found in $PATH": unknown
Paraphraser commented 3 years ago

I honestly don't know what plane of existence Portainer's designers inhabit but their decision to remove pretty much everything from the container mystifies me. Still, I've never seen the point of Portainer. For a long time I hoped I'd have the "ahah" moment but I've given up on that.

Yes, you can create any old environment variable you like. Docker will transport any variable you define into the container. It the container respects the variable, it will take action. If it doesn't know about the variable, it ignores it. Environment variables are just "there". No syntax checking. No spelling checking. No validation. They either work or they don't.

One odd thing I did notice yesterday was one container where I had an environment file but I added "environment" plus TZ to docker-compose.yml. That didn't work. No error. Just didn't work. When I stopped being lazy and moved the TZ into the environment file, it worked. I had been assuming you could mix and match. I've yet to check the compose-file doco and re-test so I'm 100% sure but, right now, I'd say "don't use both".

That said, it's on my to-do list to migrate all my env-files into environment directives in docker-compose.yml. I do think that's a useful improvement from new menu - even though I can't abide some other things from new menu like the explosion of networks.

I think getting Mosquitto to respect TZ will need the maintainer to undo whatever it is that he has done to break it in the first place. I've stared at the Dockerfile until my eyes have gone square but I can't spot it.

Paraphraser commented 3 years ago

Well, hang up the phone, switch carriers, re-dial, and hold the phone again.

The compose spec says that anything in an environment: directive overrides anything defined via an environment file. Testing shows that is true.

Testing also confirms a fragment can have both a list of variables under an environment: and one or more environment files under an env_file: directive. Mix and match as much as you like.

It also doesn't matter what order the environment: and env_file: directives appear in, inline always overrides a file.

Now, consider these:

Guess what? The quoted form does not work. Even though it should not matter (in theory), it does in practice. It has to do with how the variable is used under the hood:

Assume the following in InfluxDB's fragment in docker-compose.yml:

environment:
- MY_TZ_PLAIN=Australia/Sydney
- MY_TZ_QUOTED="Australia/Sydney"

We UP the container and then open a shell into the container. First step is to prove that the variables are being transported into the container (remembering that in what follows the "#" is the root user's prompt, not a comment):

# echo "MY_TZ_PLAIN=$MY_TZ_PLAIN, MY_TZ_QUOTED=$MY_TZ_QUOTED"
MY_TZ_PLAIN=Australia/Sydney, MY_TZ_QUOTED="Australia/Sydney"

Let's construct the paths that are used to set up timezones:

# PLAIN_PATH="/usr/share/zoneinfo/$MY_TZ_PLAIN"
# QUOTED_PATH="/usr/share/zoneinfo/$MY_TZ_QUOTED"

# echo $PLAIN_PATH 
/usr/share/zoneinfo/Australia/Sydney

# echo $QUOTED_PATH
/usr/share/zoneinfo/"Australia/Sydney"

I'm sure you can see where this is heading...

# ls -l "$PLAIN_PATH"
lrwxrwxrwx 1 root root 3 Feb  1 22:59 /usr/share/zoneinfo/Australia/Sydney -> ACT

# ls -l "$QUOTED_PATH"
ls: cannot access '/usr/share/zoneinfo/"Australia/Sydney"': No such file or directory

That's the problem.

But wait, there's more!

$ docker exec mosquitto ash -c 'echo "TZ=$TZ"'
TZ=Australia/Sydney

$ docker exec mosquitto date
Tue Mar 30 23:58:18 UTC 2021

$ docker exec mosquitto apk add tzdata
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/armhf/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/armhf/APKINDEX.tar.gz
(1/1) Installing tzdata (2021a-r0)
Executing busybox-1.31.1-r19.trigger
OK: 12 MiB in 23 packages

$ docker exec mosquitto date
Wed Mar 31 10:58:41 AEDT 2021

That's all there was to it. Dang'd quotes! 🤬

We can add Mosquitto to the list in the "providing tzdata is added" column.

I will now fetch a two-by-four and beat myself over the head.

senarvi commented 3 years ago

I managed to set the timezone with TZ to every service that I use, where it makes a visible change to the user, i.e. in the UI. Is the wrong date in e.g. mosquitto actually a problem?

Paraphraser commented 3 years ago

I submitted a Pull Request for AdGuard Home to add tzdata and it was approved and incorporated within a couple of hours. Very impressive! So that's another one to add to the list. It'll likely be "TZ ready" by the time I get around to proposing an IOTstack Pull Request to add the template (I'm actually waiting on feedback for some other Human Interface issues that turned up in my testing).

The answer to your question is "yes and no". At the technical level, no. Unless someone does something seriously silly, all modern systems run on UTC internally. You could argue that timezone correction only matters to human viewers and even then it isn't really all that hard to add or subtract your offset from UTC. The "get over it" argument.

That said, a container displaying UTC can surprise users who don't understand that it's all UTC under the hood, to the point where they think something must be wrong. That is, after all, sort of how we got here, isn't it?

A UTC display can also occasionally be misleading. You run a docker logs CONTAINER and see a time which is close to "now" without realising it is one of those rare "even a stopped clock is right twice a day" situations. You think something must be working when it's been dead for hours. You kick yourself when you work out why.

I'm also sure you're aware of having looked at something like a log entry then glanced at the time-of-day clock on your screen/wrist/wall to make an instant mental assessment of "how long ago?" and "is the difference reasonable?". Those calculations are made much harder if the container is displaying UTC, especially if it's close to the top of the hour.

Plus, the very time you start looking at logs is when something stopped working. The recent unilateral changes (see Read this if your Mosquitto is broken) is a case in point. You're in a panic. The log is the only thing you have to go on. That's really not the right time to be considering whether it's the right time (awful pun intended).

So, on balance, I'd say we're building for humans so we should accommodate human needs and implement TZ where we can.

Seeing as I got such a warm reception with AdGuardHome, I might give the Mosquitto people a try. They make things a bit harder than most repos; some nonsense about signing an agreement before you're allowed to contribute. I'm not into hoop-jumping of that kind (life's too short) but an issue pointing out that simply adding tzdata would do the trick might get me over the line.

senarvi commented 3 years ago

Right, at least it affects the timestamps in the logs. Great work!

877dev commented 3 years ago

Mix and match as much as you like.

It also doesn't matter what order the environment: and env_file: directives appear in, inline always overrides a file.

Now, consider these:

Yes I found that to be the case too!

That's all there was to it. Dang'd quotes!

Go easy with the two by four ! :)

Paraphraser commented 2 years ago

Mosquitto was fixed a while back to add tzdata via Dockerfile. Most of the rest of this is either fixed or obsolete but the basic idea of TZ=${TZ} is still on the table so I'd leave this one open.

ukkopahis commented 2 years ago

I'd suggest TZ=${IOTSTACK_TZ:-Etc/UTC}