fix(container): propagate host timezone to all containers

Replace TZ env var with /etc/localtime bind-mount in docker-compose
and inject timezone spec opts into containerd bot containers.
This commit is contained in:
BBQ
2026-02-20 03:22:31 +08:00
parent 3c1ab85349
commit 4fc0ca5110
7 changed files with 71 additions and 8 deletions
+1 -1
View File
@@ -7,9 +7,9 @@ services:
POSTGRES_DB: memoh
POSTGRES_USER: memoh
POSTGRES_PASSWORD: memoh123
TZ: ${TZ:-UTC}
volumes:
- postgres_data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
ports:
- "5432:5432"
healthcheck:
+3 -7
View File
@@ -7,9 +7,9 @@ services:
POSTGRES_DB: memoh
POSTGRES_USER: memoh
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-memoh123}
TZ: ${TZ:-UTC}
volumes:
- postgres_data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
expose:
- "5432"
healthcheck:
@@ -47,8 +47,6 @@ services:
- COMMIT_HASH=${MEMOH_COMMIT:-unknown}
- BUILD_TIME=${MEMOH_BUILD_TIME:-unknown}
container_name: memoh-migrate
environment:
TZ: ${TZ:-UTC}
entrypoint: ["/app/memoh-server", "migrate", "up"]
volumes:
- ${MEMOH_CONFIG:-./conf/app.docker.toml}:/app/config.toml:ro
@@ -70,13 +68,12 @@ services:
container_name: memoh-server
privileged: true
pid: host
environment:
TZ: ${TZ:-UTC}
volumes:
- ${MEMOH_CONFIG:-./conf/app.docker.toml}:/app/config.toml:ro
- containerd_data:/var/lib/containerd
- server_cni_state:/var/lib/cni
- memoh_data:/opt/memoh/data
- /etc/localtime:/etc/localtime:ro
ports:
- "8080:8080"
depends_on:
@@ -93,10 +90,9 @@ services:
context: .
dockerfile: docker/Dockerfile.agent
container_name: memoh-agent
environment:
TZ: ${TZ:-UTC}
volumes:
- ${MEMOH_CONFIG:-./conf/app.docker.toml}:/config.toml:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "8081:8081"
depends_on:
+26
View File
@@ -0,0 +1,26 @@
package containerd
import (
"os"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/opencontainers/runtime-spec/specs-go"
)
// TimezoneSpecOpts returns OCI spec options that propagate the host timezone
// into the container via /etc/localtime bind-mount and TZ environment variable.
func TimezoneSpecOpts() []oci.SpecOpts {
var opts []oci.SpecOpts
if _, err := os.Stat("/etc/localtime"); err == nil {
opts = append(opts, oci.WithMounts([]specs.Mount{{
Destination: "/etc/localtime",
Type: "bind",
Source: "/etc/localtime",
Options: []string{"rbind", "ro"},
}}))
}
if tz := os.Getenv("TZ"); tz != "" {
opts = append(opts, oci.WithEnv([]string{"TZ=" + tz}))
}
return opts
}
+36
View File
@@ -0,0 +1,36 @@
package containerd
import (
"os"
"testing"
)
func TestTimezoneSpecOpts_WithTZ(t *testing.T) {
t.Setenv("TZ", "Asia/Shanghai")
opts := TimezoneSpecOpts()
if _, err := os.Stat("/etc/localtime"); err == nil {
if len(opts) < 1 {
t.Fatal("expected at least mount opt when /etc/localtime exists")
}
}
found := false
for range opts {
found = true
}
if !found {
t.Fatal("expected at least one spec opt when TZ is set")
}
}
func TestTimezoneSpecOpts_WithoutTZ(t *testing.T) {
t.Setenv("TZ", "")
opts := TimezoneSpecOpts()
for _, opt := range opts {
if opt == nil {
t.Fatal("unexpected nil spec opt")
}
}
if _, err := os.Stat("/etc/localtime"); err != nil && len(opts) != 0 {
t.Fatalf("expected no opts when /etc/localtime absent and TZ empty, got %d", len(opts))
}
}
+2
View File
@@ -210,6 +210,7 @@ func (h *ContainerdHandler) CreateContainer(c echo.Context) error {
}),
oci.WithProcessArgs("/bin/sh", "-lc", fmt.Sprintf("bootstrap(){ [ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }; if [ -d /opt/mcp-template ]; then mkdir -p %q; for f in /opt/mcp-template/*; do name=$(basename \"$f\"); [ -e %q/\"$name\" ] || cp -a \"$f\" %q/\"$name\" 2>/dev/null || true; done; fi; }; bootstrap; exec /app/mcp", dataMount, dataMount, dataMount)),
}
specOpts = append(specOpts, ctr.TimezoneSpecOpts()...)
_, err = h.service.CreateContainer(ctx, ctr.CreateContainerRequest{
ID: containerID,
@@ -878,6 +879,7 @@ func (h *ContainerdHandler) SetupBotContainer(ctx context.Context, botID string)
}),
oci.WithProcessArgs("/bin/sh", "-lc", fmt.Sprintf("bootstrap(){ [ -e /app/mcp ] || { mkdir -p /app; [ -f /opt/mcp ] && cp -a /opt/mcp /app/mcp 2>/dev/null || true; }; if [ -d /opt/mcp-template ]; then mkdir -p %q; for f in /opt/mcp-template/*; do name=$(basename \"$f\"); [ -e %q/\"$name\" ] || cp -a \"$f\" %q/\"$name\" 2>/dev/null || true; done; fi; }; bootstrap; exec /app/mcp", dataMount, dataMount, dataMount)),
}
specOpts = append(specOpts, ctr.TimezoneSpecOpts()...)
_, err = h.service.CreateContainer(ctx, ctr.CreateContainerRequest{
ID: containerID,
+1
View File
@@ -134,6 +134,7 @@ func (m *Manager) EnsureBot(ctx context.Context, botID string) error {
},
}),
}
specOpts = append(specOpts, ctr.TimezoneSpecOpts()...)
_, err = m.service.CreateContainer(ctx, ctr.CreateContainerRequest{
ID: m.containerID(botID),
+2
View File
@@ -177,6 +177,7 @@ func (m *Manager) CreateVersion(ctx context.Context, botID string) (*VersionInfo
},
}),
}
specOpts = append(specOpts, ctr.TimezoneSpecOpts()...)
_, err = m.service.CreateContainerFromSnapshot(ctx, ctr.CreateContainerRequest{
ID: containerID,
@@ -318,6 +319,7 @@ func (m *Manager) RollbackVersion(ctx context.Context, botID string, version int
},
}),
}
specOpts = append(specOpts, ctr.TimezoneSpecOpts()...)
_, err = m.service.CreateContainerFromSnapshot(ctx, ctr.CreateContainerRequest{
ID: containerID,