diff --git a/Dockerfile b/Dockerfile index 5fa9ff7..bba7df5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ FROM alpine:3.23 WORKDIR /root -RUN apk add --no-cache ca-certificates && \ +RUN apk add --no-cache ca-certificates tzdata && \ chmod a+rw /var/lock COPY --from=builder /app/cmd/backup/backup /usr/bin/backup diff --git a/cmd/backup/command.go b/cmd/backup/command.go index 10fa0e1..ee6ed59 100644 --- a/cmd/backup/command.go +++ b/cmd/backup/command.go @@ -36,6 +36,13 @@ func (c *command) runAsCommand() error { } for _, config := range configurations { + warnings, warnErr := config.timezoneDeprecationWarnings() + if warnErr != nil { + return errwrap.Wrap(warnErr, "error collecting startup warnings") + } + for _, w := range warnings { + c.logger.Warn(w) + } if err := runScript(config); err != nil { return errwrap.Wrap(err, "error running script") } @@ -102,6 +109,14 @@ func (c *command) schedule(strategy configStrategy) error { for _, cfg := range configurations { config := cfg + warnings, warnErr := config.timezoneDeprecationWarnings() + if warnErr != nil { + return errwrap.Wrap(warnErr, "error collecting startup warnings") + } + for _, w := range warnings { + c.logger.Warn(w) + } + id, err := c.cr.AddFunc(config.BackupCronExpression, func() { c.logger.Info( fmt.Sprintf( diff --git a/cmd/backup/config.go b/cmd/backup/config.go index 850909c..766848b 100644 --- a/cmd/backup/config.go +++ b/cmd/backup/config.go @@ -4,6 +4,7 @@ package main import ( + "bufio" "bytes" "crypto/x509" "encoding/pem" @@ -97,6 +98,7 @@ type Config struct { GoogleDriveImpersonateSubject string `split_words:"true"` GoogleDriveEndpoint string `split_words:"true"` GoogleDriveTokenURL string `split_words:"true"` + Timezone string `envconfig:"TZ"` source string additionalEnvVars map[string]string } @@ -322,3 +324,81 @@ func (c *Config) resolve() (reset func() error, warnings []string, err error) { } return } + +func mountedPaths(path string) (map[string]struct{}, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + + mounts := make(map[string]struct{}) + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + parts := strings.SplitN(line, " - ", 2) + fields := strings.Fields(parts[0]) + if len(fields) < 5 { + continue + } + mounts[fields[4]] = struct{}{} + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return mounts, nil +} + +func (c *Config) timezoneDeprecationWarnings() ([]string, error) { + mounts, err := mountedPaths("/proc/self/mountinfo") + if err != nil { + return nil, errwrap.Wrap(err, "error reading mount info") + } + + deprecatedMounts := []string{ + "/etc/timezone", + "/etc/localtime", + "/usr/share/zoneinfo", + } + + var found []string + for _, mnt := range deprecatedMounts { + if _, ok := mounts[mnt]; ok { + found = append(found, mnt) + } + } + + if len(found) == 0 { + return nil, nil + } + + var warnings []string + + // Primary deprecation message (compressed) + warnings = append(warnings, + fmt.Sprintf( + "Deprecated timezone bind mounts detected: %s. Support for these will be removed in a future version.", + strings.Join(found, ", "), + ), + ) + + // Guidance based on TZ usage + if c.Timezone == "" { + warnings = append(warnings, + "Set the container timezone using the `TZ` environment variable instead.", + "Refer to the documentation for migration details.", + ) + } else { + warnings = append(warnings, + fmt.Sprintf( + "`TZ=%s` is set, but deprecated timezone bind mounts are still present. Remove the bind mounts after confirming timezone handling works as expected.", + c.Timezone, + ), + ) + } + + return warnings, nil +} diff --git a/cmd/backup/print_config.go b/cmd/backup/print_config.go index 69ca31d..10e36b0 100644 --- a/cmd/backup/print_config.go +++ b/cmd/backup/print_config.go @@ -34,6 +34,13 @@ func runPrintConfig() error { for _, warning := range warnings { fmt.Printf("warning:%s\n", warning) } + timezoneWarnings, warnErr := config.timezoneDeprecationWarnings() + if warnErr != nil { + return errwrap.Wrap(warnErr, "error collecting timezone deprecation warnings") + } + for _, warning := range timezoneWarnings { + fmt.Printf("warning:%s\n", warning) + } // insert line breaks before each field name, assuming field names start with uppercase letters formatted := formatter.ReplaceAllString(fmt.Sprintf("%+v", *config), "\n$1") fmt.Printf("%s\n", formatted) diff --git a/docs/how-tos/set-container-timezone.md b/docs/how-tos/set-container-timezone.md index 21b6983..b9bbfe7 100644 --- a/docs/how-tos/set-container-timezone.md +++ b/docs/how-tos/set-container-timezone.md @@ -1,26 +1,55 @@ --- -title: Set the timezone the container runs in +title: Setting the Time Zone layout: default parent: How Tos nav_order: 8 --- -# Set the timezone the container runs in +# Setting the Time Zone -By default a container based on this image will run in the UTC timezone. -As the image is designed to be as small as possible, additional timezone data is not included. -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: +## Use Environment Variable `TZ` + +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): ```yml services: backup: - image: offen/docker-volume-backup:v2 + image: offen/docker-volume-backup:latest + environment: + - TZ=Europe/Berlin volumes: - data:/backup/my-app-backup:ro - - /etc/timezone:/etc/timezone:ro - - /etc/localtime:/etc/localtime:ro - - /usr/share/zoneinfo:/usr/share/zoneinfo:ro volumes: data: ``` + +## Notes + +This approach is preferred because it: + +- avoids dependency on host configuration +- works consistently across environments + +### Compatibility + +- Bind-mounting timezone files will continue to work if `TZ` is not set. +- If `TZ` is set, it takes precedence over any bind-mounted timezone configuration. +- An invalid `TZ` value will cause the container to default to UTC. + +:warning: **Deprecation Warning** +The method described below (bind-mounting files from the host) is **deprecated**. Please use the new method described above (`TZ`) + +> ```yml +> services: +> backup: +> image: offen/docker-volume-backup:latest +> volumes: +> - data:/backup/my-app-backup:ro +> - /etc/timezone:/etc/timezone:ro +> - /etc/localtime:/etc/localtime:ro +> - /usr/share/zoneinfo:/usr/share/zoneinfo:ro +> +> volumes: +> data: +> ``` \ No newline at end of file