Skip to content

Commit 6a83ce4

Browse files
Support reading timezone info from env (#748)
* add tzdata package * updated timezone documentation * add missing bind-mount from updated docs * Add timezone deprecation warnings and related logging to backup commands * fix lint CI job error * Address PR comments: refactor timezone deprecation warning handling, update docs
1 parent 0955d6f commit 6a83ce4

5 files changed

Lines changed: 141 additions & 10 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ FROM alpine:3.23
1414

1515
WORKDIR /root
1616

17-
RUN apk add --no-cache ca-certificates && \
17+
RUN apk add --no-cache ca-certificates tzdata && \
1818
chmod a+rw /var/lock
1919

2020
COPY --from=builder /app/cmd/backup/backup /usr/bin/backup

cmd/backup/command.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ func (c *command) runAsCommand() error {
3636
}
3737

3838
for _, config := range configurations {
39+
warnings, warnErr := config.timezoneDeprecationWarnings()
40+
if warnErr != nil {
41+
return errwrap.Wrap(warnErr, "error collecting startup warnings")
42+
}
43+
for _, w := range warnings {
44+
c.logger.Warn(w)
45+
}
3946
if err := runScript(config); err != nil {
4047
return errwrap.Wrap(err, "error running script")
4148
}
@@ -102,6 +109,14 @@ func (c *command) schedule(strategy configStrategy) error {
102109

103110
for _, cfg := range configurations {
104111
config := cfg
112+
warnings, warnErr := config.timezoneDeprecationWarnings()
113+
if warnErr != nil {
114+
return errwrap.Wrap(warnErr, "error collecting startup warnings")
115+
}
116+
for _, w := range warnings {
117+
c.logger.Warn(w)
118+
}
119+
105120
id, err := c.cr.AddFunc(config.BackupCronExpression, func() {
106121
c.logger.Info(
107122
fmt.Sprintf(

cmd/backup/config.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package main
55

66
import (
7+
"bufio"
78
"bytes"
89
"crypto/x509"
910
"encoding/pem"
@@ -97,6 +98,7 @@ type Config struct {
9798
GoogleDriveImpersonateSubject string `split_words:"true"`
9899
GoogleDriveEndpoint string `split_words:"true"`
99100
GoogleDriveTokenURL string `split_words:"true"`
101+
Timezone string `envconfig:"TZ"`
100102
source string
101103
additionalEnvVars map[string]string
102104
}
@@ -322,3 +324,81 @@ func (c *Config) resolve() (reset func() error, warnings []string, err error) {
322324
}
323325
return
324326
}
327+
328+
func mountedPaths(path string) (map[string]struct{}, error) {
329+
file, err := os.Open(path)
330+
if err != nil {
331+
return nil, err
332+
}
333+
defer func() { _ = file.Close() }()
334+
335+
mounts := make(map[string]struct{})
336+
scanner := bufio.NewScanner(file)
337+
338+
for scanner.Scan() {
339+
line := scanner.Text()
340+
parts := strings.SplitN(line, " - ", 2)
341+
fields := strings.Fields(parts[0])
342+
if len(fields) < 5 {
343+
continue
344+
}
345+
mounts[fields[4]] = struct{}{}
346+
}
347+
348+
if err := scanner.Err(); err != nil {
349+
return nil, err
350+
}
351+
352+
return mounts, nil
353+
}
354+
355+
func (c *Config) timezoneDeprecationWarnings() ([]string, error) {
356+
mounts, err := mountedPaths("/proc/self/mountinfo")
357+
if err != nil {
358+
return nil, errwrap.Wrap(err, "error reading mount info")
359+
}
360+
361+
deprecatedMounts := []string{
362+
"/etc/timezone",
363+
"/etc/localtime",
364+
"/usr/share/zoneinfo",
365+
}
366+
367+
var found []string
368+
for _, mnt := range deprecatedMounts {
369+
if _, ok := mounts[mnt]; ok {
370+
found = append(found, mnt)
371+
}
372+
}
373+
374+
if len(found) == 0 {
375+
return nil, nil
376+
}
377+
378+
var warnings []string
379+
380+
// Primary deprecation message (compressed)
381+
warnings = append(warnings,
382+
fmt.Sprintf(
383+
"Deprecated timezone bind mounts detected: %s. Support for these will be removed in a future version.",
384+
strings.Join(found, ", "),
385+
),
386+
)
387+
388+
// Guidance based on TZ usage
389+
if c.Timezone == "" {
390+
warnings = append(warnings,
391+
"Set the container timezone using the `TZ` environment variable instead.",
392+
"Refer to the documentation for migration details.",
393+
)
394+
} else {
395+
warnings = append(warnings,
396+
fmt.Sprintf(
397+
"`TZ=%s` is set, but deprecated timezone bind mounts are still present. Remove the bind mounts after confirming timezone handling works as expected.",
398+
c.Timezone,
399+
),
400+
)
401+
}
402+
403+
return warnings, nil
404+
}

cmd/backup/print_config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ func runPrintConfig() error {
3434
for _, warning := range warnings {
3535
fmt.Printf("warning:%s\n", warning)
3636
}
37+
timezoneWarnings, warnErr := config.timezoneDeprecationWarnings()
38+
if warnErr != nil {
39+
return errwrap.Wrap(warnErr, "error collecting timezone deprecation warnings")
40+
}
41+
for _, warning := range timezoneWarnings {
42+
fmt.Printf("warning:%s\n", warning)
43+
}
3744
// insert line breaks before each field name, assuming field names start with uppercase letters
3845
formatted := formatter.ReplaceAllString(fmt.Sprintf("%+v", *config), "\n$1")
3946
fmt.Printf("%s\n", formatted)
Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,55 @@
11
---
2-
title: Set the timezone the container runs in
2+
title: Setting the Time Zone
33
layout: default
44
parent: How Tos
55
nav_order: 8
66
---
77

8-
# Set the timezone the container runs in
8+
# Setting the Time Zone
99

10-
By default a container based on this image will run in the UTC timezone.
11-
As the image is designed to be as small as possible, additional timezone data is not included.
12-
In case you want to run your cron rules in your local timezone (respecting DST and similar), you can mount your Docker host's `/etc/timezone`, `/etc/localtime`, and `/usr/share/zoneinfo` in read-only mode:
10+
## Use Environment Variable `TZ`
11+
12+
A container started using this image will default to UTC. To modify the time zone, set the `TZ` environment variable to a valid [tz database time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones):
1313

1414
```yml
1515
services:
1616
backup:
17-
image: offen/docker-volume-backup:v2
17+
image: offen/docker-volume-backup:latest
18+
environment:
19+
- TZ=Europe/Berlin
1820
volumes:
1921
- data:/backup/my-app-backup:ro
20-
- /etc/timezone:/etc/timezone:ro
21-
- /etc/localtime:/etc/localtime:ro
22-
- /usr/share/zoneinfo:/usr/share/zoneinfo:ro
2322

2423
volumes:
2524
data:
2625
```
26+
27+
## Notes
28+
29+
This approach is preferred because it:
30+
31+
- avoids dependency on host configuration
32+
- works consistently across environments
33+
34+
### Compatibility
35+
36+
- Bind-mounting timezone files will continue to work if `TZ` is not set.
37+
- If `TZ` is set, it takes precedence over any bind-mounted timezone configuration.
38+
- An invalid `TZ` value will cause the container to default to UTC.
39+
40+
:warning: **Deprecation Warning**
41+
The method described below (bind-mounting files from the host) is **deprecated**. Please use the new method described above (`TZ`)
42+
43+
> ```yml
44+
> services:
45+
> backup:
46+
> image: offen/docker-volume-backup:latest
47+
> volumes:
48+
> - data:/backup/my-app-backup:ro
49+
> - /etc/timezone:/etc/timezone:ro
50+
> - /etc/localtime:/etc/localtime:ro
51+
> - /usr/share/zoneinfo:/usr/share/zoneinfo:ro
52+
>
53+
> volumes:
54+
> data:
55+
> ```

0 commit comments

Comments
 (0)