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
58 changes: 51 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,16 +377,60 @@ services:
ofelia.job-exec.datecron.command: "uname -a"
```

Ofelia polls Docker every 10 seconds to detect label changes and reload the INI
file **only when it has changed**. The interval can be adjusted using
`--docker-poll-interval`. Event-based updates can be enabled with
`--docker-events`; when enabled, polling can be disabled entirely with
`--docker-no-poll`. Setting the interval to `0` also disables both label polling
and INI reloads. Polling can also be disabled in
`ofelia.ini` by adding `no-poll = true` under the `[docker]` section:
### Container Detection and Configuration Reloading

Ofelia separates two concerns:

1. **Container detection**: Detecting when Docker containers start/stop to pick up label changes
2. **Config file watching**: Reloading the INI file when it changes

#### Default Behavior (Recommended)

By default, Ofelia uses **Docker events** for instant container detection and **polls** the config file every 10 seconds:

| Setting | Default | Purpose |
|---------|---------|---------|
| `events` | `true` | Real-time container detection via Docker events |
| `config-poll-interval` | `10s` | How often to check for INI file changes |
| `docker-poll-interval` | `0` | Container polling (disabled, events used instead) |
| `polling-fallback` | `10s` | Auto-enable container polling if events fail |

#### Configuration Options

```ini
[docker]
# INI file reload interval (set to 0 to disable config watching)
config-poll-interval = 10s

# Container detection via Docker events (recommended)
events = true

# Explicit container polling interval (0 = disabled)
# WARNING: Running both events and polling is usually wasteful
docker-poll-interval = 0

# Auto-fallback to polling if event subscription fails (BC-safe default)
# Set to 0 to disable fallback (will only log errors)
polling-fallback = 10s
```

#### CLI Flags

- `--docker-config-poll-interval`: INI file reload interval
- `--docker-events`: Enable/disable Docker event-based container detection
- `--docker-poll-interval`: Container polling interval (fallback)
- `--docker-polling-fallback`: Auto-fallback interval if events fail

#### Backwards Compatibility

The old `poll-interval` and `no-poll` options still work but are deprecated:

```ini
[docker]
# DEPRECATED: Use config-poll-interval and docker-poll-interval instead
poll-interval = 10s

# DEPRECATED: Use docker-poll-interval=0 instead
no-poll = true
```

Expand Down
33 changes: 29 additions & 4 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,10 +619,35 @@ func (c *RunServiceConfig) buildMiddlewares() {
}

type DockerConfig struct {
Filters []string `mapstructure:"filters"`
PollInterval time.Duration `mapstructure:"poll-interval" default:"10s"`
UseEvents bool `mapstructure:"events" default:"false"`
DisablePolling bool `mapstructure:"no-poll" default:"false"`
Filters []string `mapstructure:"filters"`

// ConfigPollInterval controls how often to check for INI config file changes.
// This is independent of container detection. Set to 0 to disable config file watching.
ConfigPollInterval time.Duration `mapstructure:"config-poll-interval" default:"10s"`

// UseEvents enables Docker event-based container detection (recommended).
// When enabled, Ofelia reacts immediately to container start/stop events.
UseEvents bool `mapstructure:"events" default:"true"`

// DockerPollInterval enables periodic polling for container changes.
// This is a fallback for environments where Docker events don't work reliably.
// Set to 0 (default) to disable explicit container polling.
// WARNING: If both events and polling are enabled, this is usually wasteful.
DockerPollInterval time.Duration `mapstructure:"docker-poll-interval" default:"0"`

// PollingFallback auto-enables container polling if event subscription fails.
// This provides backwards compatibility and resilience.
// Set to 0 to disable auto-fallback (will only log errors on event failure).
// Default is 10s for backwards compatibility.
PollingFallback time.Duration `mapstructure:"polling-fallback" default:"10s"`

// Deprecated: Use ConfigPollInterval and DockerPollInterval instead.
// If set, this value is used for both config and container polling (BC).
PollInterval time.Duration `mapstructure:"poll-interval"`

// Deprecated: Use DockerPollInterval=0 instead.
// If true, disables container polling entirely.
DisablePolling bool `mapstructure:"no-poll" default:"false"`
}

func parseIni(cfg *ini.File, c *Config) error {
Expand Down
31 changes: 16 additions & 15 deletions cli/config_initialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,15 @@ func (s *ConfigInitSuite) TestInitializeAppSuccess(c *C) {
defer func() { newDockerHandler = origFactory }()
newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
return &DockerHandler{
ctx: ctx,
filters: cfg.Filters,
notifier: notifier,
logger: logger,
dockerProvider: &mockDockerProviderForInit{},
pollInterval: cfg.PollInterval,
useEvents: cfg.UseEvents,
disablePolling: cfg.DisablePolling,
ctx: ctx,
filters: cfg.Filters,
notifier: notifier,
logger: logger,
dockerProvider: &mockDockerProviderForInit{},
configPollInterval: cfg.ConfigPollInterval,
useEvents: cfg.UseEvents,
dockerPollInterval: cfg.DockerPollInterval,
pollingFallback: cfg.PollingFallback,
}, nil
}

Expand Down Expand Up @@ -181,12 +182,12 @@ func (s *ConfigInitSuite) TestInitializeAppLabelConflict(c *C) {
defer func() { newDockerHandler = origFactory }()
newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
return &DockerHandler{
ctx: ctx,
filters: cfg.Filters,
notifier: notifier,
logger: logger,
dockerProvider: mockProvider,
pollInterval: 0,
ctx: ctx,
filters: cfg.Filters,
notifier: notifier,
logger: logger,
dockerProvider: mockProvider,
configPollInterval: 0,
}, nil
}

Expand Down Expand Up @@ -223,7 +224,7 @@ func (s *ConfigInitSuite) TestInitializeAppComposeConflict(c *C) {
origFactory := newDockerHandler
defer func() { newDockerHandler = origFactory }()
newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
return &DockerHandler{ctx: ctx, filters: cfg.Filters, notifier: notifier, logger: logger, dockerProvider: mockProvider, pollInterval: 0}, nil
return &DockerHandler{ctx: ctx, filters: cfg.Filters, notifier: notifier, logger: logger, dockerProvider: mockProvider, configPollInterval: 0}, nil
}

cfg.logger = &TestLogger{}
Expand Down
90 changes: 48 additions & 42 deletions cli/daemon_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,14 @@ func (s *DaemonLifecycleSuite) TestSuccessfulBootStartShutdown(c *C) {

newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
handler := &DockerHandler{
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
pollInterval: cfg.PollInterval,
useEvents: cfg.UseEvents,
disablePolling: cfg.DisablePolling,
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
configPollInterval: cfg.ConfigPollInterval,
useEvents: cfg.UseEvents,
dockerPollInterval: cfg.DockerPollInterval,
pollingFallback: cfg.PollingFallback,
}
return handler, nil
}
Expand Down Expand Up @@ -332,13 +333,14 @@ func (s *DaemonLifecycleSuite) TestWebServerStartup(c *C) {

newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
handler := &DockerHandler{
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
pollInterval: cfg.PollInterval,
useEvents: cfg.UseEvents,
disablePolling: cfg.DisablePolling,
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
configPollInterval: cfg.ConfigPollInterval,
useEvents: cfg.UseEvents,
dockerPollInterval: cfg.DockerPollInterval,
pollingFallback: cfg.PollingFallback,
}
return handler, nil
}
Expand Down Expand Up @@ -504,13 +506,14 @@ func (s *DaemonLifecycleSuite) TestConfigurationOptionApplication(c *C) {

newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
handler := &DockerHandler{
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
pollInterval: cfg.PollInterval,
useEvents: cfg.UseEvents,
disablePolling: cfg.DisablePolling,
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
configPollInterval: cfg.ConfigPollInterval,
useEvents: cfg.UseEvents,
dockerPollInterval: cfg.DockerPollInterval,
pollingFallback: cfg.PollingFallback,
}
return handler, nil
}
Expand Down Expand Up @@ -550,13 +553,14 @@ func (s *DaemonLifecycleSuite) TestConcurrentServerStartup(c *C) {

newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
handler := &DockerHandler{
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
pollInterval: cfg.PollInterval,
useEvents: cfg.UseEvents,
disablePolling: cfg.DisablePolling,
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
configPollInterval: cfg.ConfigPollInterval,
useEvents: cfg.UseEvents,
dockerPollInterval: cfg.DockerPollInterval,
pollingFallback: cfg.PollingFallback,
}
return handler, nil
}
Expand Down Expand Up @@ -630,13 +634,14 @@ func (s *DaemonLifecycleSuite) TestHealthCheckerInitialization(c *C) {

newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
handler := &DockerHandler{
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
pollInterval: cfg.PollInterval,
useEvents: cfg.UseEvents,
disablePolling: cfg.DisablePolling,
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
configPollInterval: cfg.ConfigPollInterval,
useEvents: cfg.UseEvents,
dockerPollInterval: cfg.DockerPollInterval,
pollingFallback: cfg.PollingFallback,
}
return handler, nil
}
Expand Down Expand Up @@ -698,13 +703,14 @@ func (s *DaemonLifecycleSuite) TestCompleteExecuteWorkflow(c *C) {

newDockerHandler = func(ctx context.Context, notifier dockerLabelsUpdate, logger core.Logger, cfg *DockerConfig, provider core.DockerProvider) (*DockerHandler, error) {
handler := &DockerHandler{
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
pollInterval: cfg.PollInterval,
useEvents: cfg.UseEvents,
disablePolling: cfg.DisablePolling,
ctx: ctx,
dockerProvider: &mockDockerProvider{},
notifier: &mockDockerLabelsUpdate{},
logger: logger,
configPollInterval: cfg.ConfigPollInterval,
useEvents: cfg.UseEvents,
dockerPollInterval: cfg.DockerPollInterval,
pollingFallback: cfg.PollingFallback,
}
return handler, nil
}
Expand Down
Loading