Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions cmd/backup/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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(
Expand Down
80 changes: 80 additions & 0 deletions cmd/backup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package main

import (
"bufio"
"bytes"
"crypto/x509"
"encoding/pem"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions cmd/backup/print_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 38 additions & 9 deletions docs/how-tos/set-container-timezone.md
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@m90 let me know your thoughts on the new structure of this file. I also propose that we change the tag from v2 to latest as a placeholder for the docker-compose file code snippets.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs are great now, thank you.

I would like to stick to using v2 though as it's basically "latest" plus it gives you the guarantee that snippets flying around the internet keep working in case a v3 is ever released: If someone (or a LLM) copies portions of the current documentation, keeps them elsewhere, and then starts using them at a point when latest has moved on to something v3, things might break.

I.e. v2 should give us the benefits of latest (not having to update docs over and over again, some sort of autoupdate) without the downsides.

Original file line number Diff line number Diff line change
@@ -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:
> ```
Loading