- Introduced `IpWhitelistPlugin` (DOMAIN): Restricts access to specific IP addresses or CIDR ranges. - Added configuration options: `enabled`, `allowed_ips`, and `status_code`. - Updated `plugins.md` and `plugin-development.md` to document `IpWhitelistPlugin`. - Created fixtures for `services-with-ip-whitelist`. - Added extensive test cases for `IpWhitelistPlugin` to validate configuration and HAProxy output generation. - Ensured seamless integration into the plugin framework alongside other domain plugins.
30 KiB
| sidebar_position |
|---|
| 17 |
Plugin Developer Guide
This guide explains how to create custom plugins for EasyHAProxy. For information on using existing plugins, see Using Plugins.
Architecture Overview
Plugin Lifecycle
1. Discovery Cycle Starts
│
2. PluginManager loads plugins
├─ Load builtin plugins from src/plugins/builtin/
└─ Load external plugins from /etc/haproxy/plugins/
│
3. PluginManager configures plugins
├─ Read global config (YAML/env vars)
└─ Call plugin.configure(config)
│
4. Parse container metadata → easymapping
│
5. Execute GLOBAL plugins (once)
├─ Create PluginContext (no domain info)
├─ Call plugin.process(context)
└─ Collect PluginResult
│
6. For each discovered domain:
├─ Extract plugin list from labels
├─ Extract plugin configs from labels
├─ Call plugin.configure(label_config)
├─ Execute DOMAIN plugins
│ ├─ Create PluginContext (with domain info)
│ ├─ Call plugin.process(context)
│ └─ Collect PluginResult
└─ Store plugin HAProxy configs
│
7. Render Jinja2 template
├─ Inject global plugin configs
└─ Inject domain plugin configs per backend
│
8. Generate final HAProxy config
Built-in Plugins Reference
EasyHAProxy includes four built-in plugins that serve as both functional tools and reference implementations for plugin development.
CloudflarePlugin (DOMAIN)
Purpose: Restore original visitor IP addresses when using Cloudflare CDN
Type: DOMAIN - Executes once per domain
Source: src/plugins/builtin/cloudflare.py
Configuration Options:
enabled(bool) - Enable/disable plugin (default:true)ip_list_path(str) - Path to Cloudflare IP list file (default:/etc/haproxy/cloudflare_ips.lst)
What it does:
- Reads Cloudflare IP ranges from a file
- Checks if request comes from Cloudflare IP
- Restores original visitor IP from
CF-Connecting-IPheader
HAProxy config generated:
# Cloudflare - Restore original visitor IP
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare
Usage: See Cloudflare Plugin documentation
CleanupPlugin (GLOBAL)
Purpose: Perform cleanup tasks during discovery cycle
Type: GLOBAL - Executes once per discovery cycle
Source: src/plugins/builtin/cleanup.py
Configuration Options:
enabled(bool) - Enable/disable plugin (default:true)max_idle_time(int) - Max file age in seconds before deletion (default:300)cleanup_temp_files(bool) - Enable temp file cleanup (default:true)
What it does:
- Scans
/tmpfor files prefixed witheasyhaproxy_ - Removes files older than
max_idle_timeseconds - Logs cleanup actions to metadata
HAProxy config generated: None (performs cleanup only)
Usage: See Cleanup Plugin documentation
DenyPagesPlugin (DOMAIN)
Purpose: Block access to specific paths for a domain
Type: DOMAIN - Executes once per domain
Source: src/plugins/builtin/deny_pages.py
Configuration Options:
enabled(bool) - Enable/disable plugin (default:true)paths(str) - Comma-separated list of paths to block (e.g.,/admin,/private)status_code(int) - HTTP status code to return (default:403)
What it does:
- Parses comma-separated list of paths
- Creates HAProxy ACL matching those paths
- Returns specified HTTP status code for matching requests
HAProxy config generated:
# Deny Pages - Block specific paths
acl denied_path path_beg /admin /private
http-request deny deny_status 403 if denied_path
Usage: See Deny Pages Plugin documentation
IpWhitelistPlugin (DOMAIN)
Purpose: Restrict domain access to specific IP addresses or CIDR ranges
Type: DOMAIN - Executes once per domain
Source: src/plugins/builtin/ip_whitelist.py
Configuration Options:
enabled(bool) - Enable/disable plugin (default:true)allowed_ips(str) - Comma-separated list of IPs/CIDR ranges to allow (e.g.,192.168.1.0/24,10.0.0.1)status_code(int) - HTTP status code to return for blocked IPs (default:403)
What it does:
- Parses comma-separated list of IPs and CIDR ranges
- Creates HAProxy ACL matching whitelisted IPs
- Denies all requests NOT from whitelisted IPs
HAProxy config generated:
# IP Whitelist - Only allow specific IPs
acl whitelisted_ip src 192.168.1.0/24 10.0.0.5
http-request deny deny_status 403 if !whitelisted_ip
Usage: See IP Whitelist Plugin documentation
Summary Table
| Plugin | Type | Purpose | Config Generated |
|---|---|---|---|
cloudflare |
DOMAIN | Restore original visitor IPs | ✅ Yes |
cleanup |
GLOBAL | Clean up temp files | ❌ No |
deny_pages |
DOMAIN | Block specific paths | ✅ Yes |
ip_whitelist |
DOMAIN | Restrict to specific IPs | ✅ Yes |
Learning from Built-in Plugins
Best practices demonstrated:
-
CloudflarePlugin shows:
- Reading external files (IP list)
- Conditional HAProxy ACLs
- Header manipulation
-
CleanupPlugin shows:
- GLOBAL plugin pattern
- File system operations
- Metadata-only results (no HAProxy config)
-
DenyPagesPlugin shows:
- Parsing comma-separated config values
- Configurable status codes
- Path-based ACLs
-
IpWhitelistPlugin shows:
- IP-based access control
- Negated ACLs (
if !whitelisted_ip) - CIDR range support
View the source code:
Plugin API Reference
PluginInterface (Abstract Base Class)
All plugins must inherit from PluginInterface and implement these abstract methods:
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
class MyPlugin(PluginInterface):
@property
@abstractmethod
def name(self) -> str:
"""
Return unique plugin identifier.
Used for:
- Configuration lookups
- Enable/disable via labels
- Logging
Must be:
- Unique across all plugins
- Lowercase with underscores
- Match filename (e.g., my_plugin.py → "my_plugin")
"""
pass
@property
@abstractmethod
def plugin_type(self) -> PluginType:
"""
Return plugin execution type.
Returns:
PluginType.GLOBAL - Execute once per discovery cycle
PluginType.DOMAIN - Execute once per domain/host
"""
pass
@abstractmethod
def configure(self, config: dict) -> None:
"""
Configure plugin with settings from YAML/env/labels.
Called:
- Once at startup with global config
- Before each execution with label-specific config (domain plugins)
Args:
config: Dictionary with plugin configuration
Keys are configuration option names
Values are strings from YAML/env/labels
Common pattern:
if "enabled" in config:
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
if "my_option" in config:
self.my_option = config["my_option"]
"""
pass
@abstractmethod
def process(self, context: PluginContext) -> PluginResult:
"""
Execute plugin logic.
Args:
context: PluginContext with all execution data
Returns:
PluginResult with HAProxy config snippets and/or metadata
Common pattern:
if not self.enabled:
return PluginResult() # Empty result when disabled
# Generate config
haproxy_config = "..."
return PluginResult(
haproxy_config=haproxy_config,
metadata={"info": "value"}
)
"""
pass
PluginType (Enum)
Defines when plugins execute:
class PluginType(Enum):
GLOBAL = "global" # Execute once per discovery cycle
DOMAIN = "domain" # Execute once per domain/host
GLOBAL plugins:
- Execute once regardless of how many domains exist
- Receive empty domain/port/host_config in context
- Use cases: cleanup, monitoring, DNS updates
DOMAIN plugins:
- Execute for each discovered domain
- Receive domain-specific data in context
- Use cases: per-domain config, headers, path blocking
PluginContext (Data Class)
Contains all data available to plugins during execution:
@dataclass
class PluginContext:
parsed_object: dict # Discovery data: {IP: labels}
easymapping: list # HAProxy mapping structure
container_env: dict # Global environment config
domain: Optional[str] = None # Domain name (DOMAIN plugins only)
port: Optional[str] = None # Port (DOMAIN plugins only)
host_config: Optional[dict] = None # Host config (DOMAIN plugins only)
Fields explained:
parsed_object: dict
Container discovery data mapping IP addresses to labels.
Example:
{
"192.168.1.10": {
"easyhaproxy.http.host": "example.com",
"easyhaproxy.http.port": "80",
"easyhaproxy.http.localport": "8080",
"easyhaproxy.http.plugins": "cloudflare,deny_pages",
"easyhaproxy.http.plugin.deny_pages.paths": "/admin"
},
"192.168.1.20": {
"easyhaproxy.http.host": "other.com",
"easyhaproxy.http.port": "80"
}
}
Use cases:
- Iterate over all discovered containers
- Access container labels directly
- Global plugins analyzing all containers
easymapping: list
Parsed HAProxy configuration structure before template rendering.
Example:
[
{
"port": "80",
"mode": "http",
"ssl-check": "",
"hosts": {
"example.com": {
"containers": ["192.168.1.10:8080"],
"balance": "roundrobin",
"certbot": False,
"redirect_ssl": False,
"plugin_configs": []
}
},
"redirect": {}
}
]
Use cases:
- Analyze discovered hosts
- Modify discovery structure (advanced)
container_env: dict
Global configuration from YAML and environment variables.
Example:
{
"customerrors": False,
"ssl_mode": "default",
"certbot": {
"email": "admin@example.com"
},
"stats": {
"port": 1936,
"username": "admin"
},
"plugins": {
"enabled": ["cleanup"],
"abort_on_error": False,
"config": {
"cleanup": {
"max_idle_time": "300"
}
}
}
}
Use cases:
- Access global settings
- Check certbot configuration
- Read global plugin config
domain: Optional[str]
Domain name for DOMAIN plugins. None for GLOBAL plugins.
Example: "example.com"
port: Optional[str]
Port for DOMAIN plugins. None for GLOBAL plugins.
Example: "80", "443"
host_config: Optional[dict]
Host-specific configuration for DOMAIN plugins. None for GLOBAL plugins.
Example:
{
"containers": ["192.168.1.10:8080"],
"balance": "roundrobin",
"certbot": False,
"redirect_ssl": False,
"plugin_configs": []
}
PluginResult (Data Class)
Plugin.process(context) → PluginResult
├─ haproxy_config (what to add to HAProxy config)
├─ modified_easymapping (optional: modify discovery data)
└─ metadata (optional: debug/logging info)
What is PluginResult?
PluginResult is a container object that your plugin returns from its process() method. It holds everything the plugin produced during execution.
Why does it exist?
Plugins need to return multiple pieces of information:
- HAProxy configuration snippets to inject
- Optional modifications to the discovery data
- Metadata for logging and debugging
Instead of returning multiple values, plugins return one PluginResult object containing all this information.
How do you use it?
Every process() method must return a PluginResult:
def process(self, context: PluginContext) -> PluginResult:
# Plugin is disabled - return empty result
if not self.enabled:
return PluginResult()
# Plugin is enabled - return config
return PluginResult(
haproxy_config="http-request set-header X-Custom-Header 'value'",
metadata={"info": "some debug info"}
)
PluginResult structure:
@dataclass
class PluginResult:
haproxy_config: str = "" # HAProxy config to inject
modified_easymapping: Optional[list] = None # Modified discovery data (advanced)
metadata: Dict[str, Any] = field(default_factory=dict) # Debug/logging info
Common usage patterns:
# Empty result (plugin disabled or nothing to do)
return PluginResult()
# Config only (most common)
return PluginResult(
haproxy_config="# My config\n directive value"
)
# Config + metadata (recommended)
return PluginResult(
haproxy_config="# My config\n directive value",
metadata={"domain": context.domain, "option": self.my_option}
)
# All fields (advanced)
return PluginResult(
haproxy_config="# My config\n directive value",
modified_easymapping=modified_data,
metadata={"info": "value"}
)
Fields explained:
haproxy_config: str
HAProxy configuration snippet to inject into the generated config.
For DOMAIN plugins: Injected into the backend section for that domain.
Example:
haproxy_config = """# Cloudflare IP Restoration
acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst
http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare"""
Important:
- Include leading spaces/tabs for proper indentation
- Add comment describing what the config does
- Use HAProxy directives that make sense for backend context (domain plugins)
- Return empty string
""when plugin is disabled or has nothing to add
modified_easymapping: Optional[list]
Modified easymapping structure. Advanced feature, rarely used.
When to use:
- Modify discovery data before template rendering
- Add/remove hosts dynamically
- Change port mappings
Default: None (don't modify easymapping)
Example:
# Add a new host to port 80
modified = context.easymapping.copy()
modified[0]["hosts"]["new-host.com"] = {
"containers": ["192.168.1.30:8080"],
"balance": "roundrobin",
"certbot": False,
"redirect_ssl": False,
"plugin_configs": []
}
return PluginResult(modified_easymapping=modified)
metadata: Dict[str, Any]
Metadata for logging and debugging. Not used in HAProxy config.
Example:
metadata = {
"domain": context.domain,
"blocked_paths": ["/admin", "/private"],
"status_code": 403,
"files_cleaned": 5
}
Use cases:
- Debug information
- Statistics
- Audit trail
Logged at DEBUG level:
DEBUG: Plugin deny_pages metadata: {'domain': 'example.com', 'blocked_paths': ['/admin'], 'status_code': 403}
Creating a Plugin: Step-by-Step
Step 1: Create Plugin File
Create /etc/haproxy/plugins/my_plugin.py:
import os
import sys
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
from functions import loggerEasyHaproxy # Optional: for logging
Step 2: Define Plugin Class
class MyPlugin(PluginInterface):
"""
Brief description of what your plugin does.
Configuration:
- enabled: Enable/disable plugin (default: true)
- my_option: Description of option
Example:
easyhaproxy.http.plugins: my_plugin
easyhaproxy.http.plugin.my_plugin.my_option: value
"""
def __init__(self):
"""Initialize plugin with default values"""
self.enabled = True
self.my_option = "default_value"
Step 3: Implement Required Methods
@property
def name(self) -> str:
return "my_plugin" # Must match filename
@property
def plugin_type(self) -> PluginType:
return PluginType.DOMAIN # or PluginType.GLOBAL
def configure(self, config: dict) -> None:
"""Parse configuration from YAML/env/labels"""
if "enabled" in config:
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
if "my_option" in config:
self.my_option = config["my_option"]
loggerEasyHaproxy.debug(f"Configured {self.name}: enabled={self.enabled}, my_option={self.my_option}")
def process(self, context: PluginContext) -> PluginResult:
"""Execute plugin logic"""
# Return empty result if disabled
if not self.enabled:
return PluginResult()
# Generate HAProxy config snippet
haproxy_config = f"""# My Plugin - Description
http-request set-header X-My-Header "{self.my_option}\""""
# Return result
return PluginResult(
haproxy_config=haproxy_config,
metadata={
"domain": context.domain,
"my_option": self.my_option
}
)
Step 4: Test Your Plugin
Enable debug logging:
EASYHAPROXY_LOG_LEVEL=DEBUG
Enable plugin via label:
services:
test:
labels:
easyhaproxy.http.host: test.example.com
easyhaproxy.http.plugins: my_plugin
easyhaproxy.http.plugin.my_plugin.my_option: custom_value
Check logs for:
INFO: Loaded external plugin: my_plugin (domain)
DEBUG: Configured my_plugin: enabled=True, my_option=custom_value
DEBUG: Executing domain plugin: my_plugin for domain: test.example.com
DEBUG: Plugin my_plugin metadata: {'domain': 'test.example.com', 'my_option': 'custom_value'}
Complete Examples
Example 1: Custom Header Plugin (DOMAIN)
Add custom headers to specific domains:
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
class CustomHeaderPlugin(PluginInterface):
"""Add custom HTTP headers to requests"""
def __init__(self):
self.enabled = True
self.headers = {} # {header_name: header_value}
@property
def name(self) -> str:
return "custom_header"
@property
def plugin_type(self) -> PluginType:
return PluginType.DOMAIN
def configure(self, config: dict) -> None:
if "enabled" in config:
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
# Parse headers from config
# Format: header1:value1,header2:value2
if "headers" in config:
for header_pair in config["headers"].split(","):
if ":" in header_pair:
name, value = header_pair.split(":", 1)
self.headers[name.strip()] = value.strip()
def process(self, context: PluginContext) -> PluginResult:
if not self.enabled or not self.headers:
return PluginResult()
# Generate HAProxy directives for each header
lines = ["# Custom Headers"]
for name, value in self.headers.items():
lines.append(f' http-request set-header {name} "{value}"')
return PluginResult(
haproxy_config="\n".join(lines),
metadata={"domain": context.domain, "headers": self.headers}
)
Usage:
labels:
easyhaproxy.http.plugins: custom_header
easyhaproxy.http.plugin.custom_header.headers: X-App-Name:MyApp,X-Environment:Production
Example 2: Rate Limiting Plugin (DOMAIN)
Add HAProxy rate limiting per domain:
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
class RateLimitPlugin(PluginInterface):
"""Rate limit requests per domain"""
def __init__(self):
self.enabled = True
self.requests_per_second = 100
self.burst = 200
@property
def name(self) -> str:
return "rate_limit"
@property
def plugin_type(self) -> PluginType:
return PluginType.DOMAIN
def configure(self, config: dict) -> None:
if "enabled" in config:
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
if "requests_per_second" in config:
try:
self.requests_per_second = int(config["requests_per_second"])
except ValueError:
pass
if "burst" in config:
try:
self.burst = int(config["burst"])
except ValueError:
pass
def process(self, context: PluginContext) -> PluginResult:
if not self.enabled:
return PluginResult()
# Use HAProxy stick tables for rate limiting
domain_safe = context.domain.replace(".", "_")
haproxy_config = f"""# Rate Limiting - {self.requests_per_second} req/s
stick-table type ip size 100k expire 30s store http_req_rate({self.requests_per_second}s)
http-request track-sc0 src
http-request deny deny_status 429 if {{ sc_http_req_rate(0) gt {self.burst} }}"""
return PluginResult(
haproxy_config=haproxy_config,
metadata={
"domain": context.domain,
"rate_limit": self.requests_per_second,
"burst": self.burst
}
)
Usage:
labels:
easyhaproxy.http.plugins: rate_limit
easyhaproxy.http.plugin.rate_limit.requests_per_second: 50
easyhaproxy.http.plugin.rate_limit.burst: 100
Example 3: Maintenance Mode Plugin (GLOBAL)
Put all sites in maintenance mode:
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from plugins import PluginInterface, PluginType, PluginContext, PluginResult
class MaintenanceModePlugin(PluginInterface):
"""Enable/disable maintenance mode globally"""
def __init__(self):
self.enabled = False # Disabled by default
self.message = "Site is under maintenance"
@property
def name(self) -> str:
return "maintenance_mode"
@property
def plugin_type(self) -> PluginType:
return PluginType.GLOBAL
def configure(self, config: dict) -> None:
if "enabled" in config:
self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"]
if "message" in config:
self.message = config["message"]
def process(self, context: PluginContext) -> PluginResult:
if not self.enabled:
return PluginResult()
# When enabled, return maintenance page for all requests
# This would need custom error pages configured in HAProxy
return PluginResult(
metadata={
"maintenance_mode": True,
"message": self.message
}
)
Best Practices
1. Error Handling
Always handle errors gracefully:
def process(self, context: PluginContext) -> PluginResult:
try:
# Your plugin logic
result = do_something()
return PluginResult(haproxy_config=result)
except Exception as e:
loggerEasyHaproxy.error(f"Plugin {self.name} failed: {e}")
return PluginResult() # Return empty result on error
2. Logging
Use structured logging:
from functions import loggerEasyHaproxy
# Info level for important events
loggerEasyHaproxy.info(f"Plugin {self.name} executed successfully")
# Debug level for detailed info
loggerEasyHaproxy.debug(f"Plugin {self.name} config: {self.my_option}")
# Warning for non-critical issues
loggerEasyHaproxy.warning(f"Plugin {self.name}: config missing, using default")
# Error for failures
loggerEasyHaproxy.error(f"Plugin {self.name} failed: {error}")
3. Configuration Validation
Validate configuration values:
def configure(self, config: dict) -> None:
if "timeout" in config:
try:
timeout = int(config["timeout"])
if timeout < 0:
loggerEasyHaproxy.warning(f"{self.name}: timeout must be positive, using default")
self.timeout = 30
else:
self.timeout = timeout
except ValueError:
loggerEasyHaproxy.warning(f"{self.name}: invalid timeout value, using default")
self.timeout = 30
4. Documentation
Document your plugin thoroughly:
class MyPlugin(PluginInterface):
"""
One-line description.
Detailed description of what the plugin does and why you'd use it.
Configuration Options:
enabled (bool): Enable/disable plugin (default: true)
option1 (str): Description of option1 (default: "value")
option2 (int): Description of option2 (default: 100)
Example YAML:
plugins:
my_plugin:
enabled: true
option1: custom_value
option2: 200
Example Container Label:
easyhaproxy.http.plugins: my_plugin
easyhaproxy.http.plugin.my_plugin.option1: custom_value
easyhaproxy.http.plugin.my_plugin.option2: 200
HAProxy Config Generated:
# My Plugin - Description
directive1 value
directive2 value
"""
5. Testing
Test your plugin with various configurations:
# Test 1: Plugin disabled
config = {"enabled": "false"}
plugin.configure(config)
result = plugin.process(context)
assert result.haproxy_config == ""
# Test 2: Plugin with default config
plugin = MyPlugin()
result = plugin.process(context)
assert "expected output" in result.haproxy_config
# Test 3: Plugin with custom config
config = {"my_option": "custom"}
plugin.configure(config)
result = plugin.process(context)
assert "custom" in result.haproxy_config
6. Performance
Keep plugins lightweight:
# DON'T: Make external API calls in domain plugins
def process(self, context: PluginContext) -> PluginResult:
# This runs for EVERY domain!
data = requests.get("https://api.example.com/data") # BAD
# DO: Cache data or use global plugins for external calls
def process(self, context: PluginContext) -> PluginResult:
# Use cached data
data = self.cached_data
Testing Plugins
Unit Testing
Create tests in src/tests/test_my_plugin.py:
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from plugins import PluginContext
from plugins.my_plugin import MyPlugin
def test_plugin_initialization():
plugin = MyPlugin()
assert plugin.name == "my_plugin"
assert plugin.enabled is True
def test_plugin_configuration():
plugin = MyPlugin()
plugin.configure({"enabled": "false", "my_option": "custom"})
assert plugin.enabled is False
assert plugin.my_option == "custom"
def test_plugin_generates_config():
plugin = MyPlugin()
context = PluginContext(
parsed_object={},
easymapping=[],
container_env={},
domain="example.com",
port="80",
host_config={}
)
result = plugin.process(context)
assert "X-My-Header" in result.haproxy_config
assert result.metadata["domain"] == "example.com"
def test_plugin_disabled():
plugin = MyPlugin()
plugin.configure({"enabled": "false"})
context = PluginContext(
parsed_object={},
easymapping=[],
container_env={},
domain="example.com"
)
result = plugin.process(context)
assert result.haproxy_config == ""
Run tests:
pytest src/tests/test_my_plugin.py -v
Troubleshooting
Plugin Not Loading
Check logs:
ERROR: Failed to load plugin from /etc/haproxy/plugins/my_plugin.py: <error>
Common causes:
- Syntax errors in Python code
- Missing imports
- Class doesn't inherit from
PluginInterface __init__.pyin plugins directory (remove it)
Plugin Not Executing
Check logs:
DEBUG: Executing domain plugin: my_plugin for domain: example.com
If missing:
- Plugin not enabled in labels/YAML/env
- Plugin name mismatch
- Plugin returned by
nameproperty doesn't match
Configuration Not Working
Enable debug logging:
EASYHAPROXY_LOG_LEVEL=DEBUG
Check:
DEBUG: Configured my_plugin: enabled=True, option=value
Verify precedence:
- Container labels (highest)
- YAML config
- Environment variables (lowest)
Further Reading
- Using Plugins - How to use existing plugins
- Built-in Plugin Source Code - Examples to learn from
- HAProxy Configuration Manual - HAProxy directive reference
Support
For issues or questions:
- GitHub Issues: https://github.com/byjg/docker-easy-haproxy/issues
- Documentation: https://byjg.github.io/docker-easy-haproxy