1
0
Fork 0

Add FastCGI support for PHP-FPM and backend server protocol configuration

- Introduced `proto` and `socket` options for backend communication, supporting protocols like FastCGI (fcgi) and HTTP/2 (h2).
- Updated HAProxy configuration templates to handle Unix socket paths and protocol-specific settings.
- Enhanced documentation with examples for FastCGI using Unix sockets and TCP connections.
- Added new tests to validate FastCGI support, including expected configuration generation.
This commit is contained in:
Joao Gilberto Magalhaes 2025-11-30 13:23:56 -05:00
parent 51f8cd4659
commit a993025718
4 changed files with 126 additions and 15 deletions

View file

@ -6,20 +6,22 @@ sidebar_position: 11
## Container (Docker or Swarm) labels ## Container (Docker or Swarm) labels
| Label | Description | Default | Example | | Label | Description | Default | Example |
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------------------------------| |---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------------------------------|
| easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com | | easyhaproxy.[definition].host | Host(s) HAProxy is listening. More than one host use comma as delimiter | **required** | somehost.com OR host1.com,host2.com |
| easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp | | easyhaproxy.[definition].mode | (Optional) Is this `http` or `tcp` mode in HAProxy. | http | http or tcp |
| easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 | | easyhaproxy.[definition].port | (Optional) Port HAProxy will listen for the host. | 80 | 3000 |
| easyhaproxy.[definition].localport | (Optional) Port container is listening. | 80 | 8080 | | easyhaproxy.[definition].localport | (Optional) Port container is listening. | 80 | 8080 |
| easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | *empty* | \{"foo.com":"https://bla.com", "bar.com":"https://bar.org"} | | easyhaproxy.[definition].redirect | (Optional) JSON containing key/value pair from host/to URL redirect. | *empty* | \{"foo.com":"https://bla.com", "bar.com":"https://bar.org"} |
| easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `certbot` is enabled. | *empty* | base64 cert + key | | easyhaproxy.[definition].sslcert | (Optional) Cert PEM Base64 encoded. Do not use this if `certbot` is enabled. | *empty* | base64 cert + key |
| easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false | | easyhaproxy.[definition].ssl | (Optional) If `true` you need to provide certificate as a file. See below. Do not use with `sslcert`. | false | true or false |
| easyhaproxy.[definition].ssl-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl | | easyhaproxy.[definition].ssl-check | (Optional) `ssl`, enable health check via SSL in `mode tcp` | *empty* | ssl |
| easyhaproxy.[definition].certbot | (Optional) Generate certificate with certbot. Do not use with `sslcert` parameter. More info [here](acme.md). | false | true OR false | | easyhaproxy.[definition].certbot | (Optional) Generate certificate with certbot. Do not use with `sslcert` parameter. More info [here](acme.md). | false | true OR false |
| easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false | | easyhaproxy.[definition].redirect_ssl | (Optional) Redirect all requests to https | false | true OR false |
| easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use** this with `ssl` or `certbot` parameters | false | true OR false | | easyhaproxy.[definition].clone_to_ssl | (Optional) It copies the configuration to HTTPS(443) and disable SSL from the current config. **Do not use** this with `ssl` or `certbot` parameters | false | true OR false |
| easyhaproxy.[definition].balance | (Optional) HAProxy balance algorithm. See [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance) | roundrobin | roundrobin, source, uri, url_param, hdr, rdp-cookie, leastconn, first, static-rr, rdp-cookie, hdr_dom, map-based | | easyhaproxy.[definition].balance | (Optional) HAProxy balance algorithm. See [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance) | roundrobin | roundrobin, source, uri, url_param, hdr, rdp-cookie, leastconn, first, static-rr, rdp-cookie, hdr_dom, map-based |
| easyhaproxy.[definition].proto | (Optional) Backend server protocol (e.g., fcgi for PHP-FPM, h2 for HTTP/2) | *empty* | fcgi, h2 |
| easyhaproxy.[definition].socket | (Optional) Unix socket path for backend connection (alternative to host:port) | *empty* | /run/php/php-fpm.sock |
:::info Understanding Definitions :::info Understanding Definitions
The `[definition]` is a string identifier that groups related configuration labels together. Different definitions create separate HAProxy configurations. The `[definition]` is a string identifier that groups related configuration labels together. Different definitions create separate HAProxy configurations.
@ -93,6 +95,54 @@ docker run \
some/tcp-service some/tcp-service
``` ```
### FastCGI (PHP-FPM) Support
EasyHAProxy supports FastCGI protocol for PHP-FPM and other FastCGI applications.
#### Using Unix Socket
```yaml title="PHP-FPM with Unix socket"
version: "3"
services:
php-fpm:
image: php:8.2-fpm
labels:
easyhaproxy.fcgi.host: phpapp.local
easyhaproxy.fcgi.port: 80
easyhaproxy.fcgi.socket: /run/php/php-fpm.sock
easyhaproxy.fcgi.proto: fcgi
volumes:
- /run/php:/run/php
```
#### Using TCP Connection
```yaml title="PHP-FPM with TCP connection"
version: "3"
services:
php-fpm:
image: php:8.2-fpm
labels:
easyhaproxy.fcgi.host: phpapp.local
easyhaproxy.fcgi.port: 80
easyhaproxy.fcgi.localport: 9000
easyhaproxy.fcgi.proto: fcgi
```
**Generated HAProxy Configuration:**
```
backend srv_phpapp_local_80
balance roundrobin
mode http
option forwardfor
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server srv-0 /run/php/php-fpm.sock check weight 1 proto fcgi
```
### Redirect Domains ### Redirect Domains
```bash title="Domain redirect configuration" ```bash title="Domain redirect configuration"

View file

@ -175,13 +175,33 @@ class HaproxyConfigGenerator:
"" ""
) )
# Protocol for backend server communication (e.g., fcgi, h2)
proto = self.label.get(
self.label.create([definition, "proto"]),
""
)
# Unix socket path (alternative to host:port)
socket_path = self.label.get(
self.label.create([definition, "socket"]),
""
)
for hostname in sorted(d[host_label].split(",")): for hostname in sorted(d[host_label].split(",")):
hostname = hostname.strip() hostname = hostname.strip()
self.serving_hosts.append("%s:%s" % (hostname, port)) self.serving_hosts.append("%s:%s" % (hostname, port))
easymapping[port]["hosts"].setdefault(hostname, {}) easymapping[port]["hosts"].setdefault(hostname, {})
easymapping[port]["hosts"][hostname].setdefault("containers", []) easymapping[port]["hosts"][hostname].setdefault("containers", [])
easymapping[port]["hosts"][hostname].setdefault("certbot", False) easymapping[port]["hosts"][hostname].setdefault("certbot", False)
easymapping[port]["hosts"][hostname]["containers"] += ["{}:{}".format(container, ct_port)] easymapping[port]["hosts"][hostname].setdefault("proto", proto)
# Determine server address: Unix socket or TCP host:port
if socket_path:
server_address = socket_path
else:
server_address = "{}:{}".format(container, ct_port)
easymapping[port]["hosts"][hostname]["containers"] += [server_address]
easymapping[port]["hosts"][hostname]["certbot"] = certbot easymapping[port]["hosts"][hostname]["certbot"] = certbot
easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool( easymapping[port]["hosts"][hostname]["redirect_ssl"] = self.label.get_bool(
self.label.create([definition, "redirect_ssl"]) self.label.create([definition, "redirect_ssl"])

View file

@ -97,7 +97,7 @@ backend srv_{{ host }}
tcp-check connect{{ " ssl" if o["ssl-check"] == "ssl" }} tcp-check connect{{ " ssl" if o["ssl-check"] == "ssl" }}
{% endif %} {% endif %}
{% for c in o["hosts"][k]["containers"] %} {% for c in o["hosts"][k]["containers"] %}
server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["ssl-check"] == "ssl" }} server srv-{{ loop.index0 }} {{ c }} check weight 1{{ " verify none" if o["ssl-check"] == "ssl" }}{{ " proto " + o["hosts"][k]["proto"] if o["hosts"][k].get("proto") }}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View file

@ -124,6 +124,7 @@ def test_parser_finds_services_raw():
"my-stack_agent:9001" "my-stack_agent:9001"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
} }
@ -143,6 +144,7 @@ def test_parser_finds_services_raw():
"my-stack_cadvisor:8080" "my-stack_cadvisor:8080"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
}, },
@ -152,6 +154,7 @@ def test_parser_finds_services_raw():
"my-stack_node-exporter:9100" "my-stack_node-exporter:9100"
], ],
"certbot": True, "certbot": True,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
} }
@ -171,6 +174,7 @@ def test_parser_finds_services_raw():
"my-stack_node-exporter:9100" "my-stack_node-exporter:9100"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
}, },
@ -180,6 +184,7 @@ def test_parser_finds_services_raw():
"some-service:80" "some-service:80"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
} }
@ -204,6 +209,7 @@ def test_parser_finds_services_raw():
"some-service:80" "some-service:80"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
} }
@ -471,6 +477,7 @@ def test_parser_finds_services_clone_to_ssl_raw():
"10.152.183.215:8080" "10.152.183.215:8080"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
}, },
@ -480,6 +487,7 @@ def test_parser_finds_services_clone_to_ssl_raw():
"10.152.183.62:8080" "10.152.183.62:8080"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
}, },
@ -489,6 +497,7 @@ def test_parser_finds_services_clone_to_ssl_raw():
"10.152.183.62:8080" "10.152.183.62:8080"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
} }
@ -508,6 +517,7 @@ def test_parser_finds_services_clone_to_ssl_raw():
"10.152.183.215:8080" "10.152.183.215:8080"
], ],
"certbot": False, "certbot": False,
"proto": "",
"redirect_ssl": False, "redirect_ssl": False,
"plugin_configs": [] "plugin_configs": []
} }
@ -525,6 +535,37 @@ def test_parser_finds_services_clone_to_ssl_raw():
assert parsed_object == processed assert parsed_object == processed
assert [] == cfg.certbot_hosts assert [] == cfg.certbot_hosts
def test_parser_fcgi():
"""Test FastCGI support with proto and socket parameters"""
line_list = load_fixture("services-fcgi")
result = {
"customerrors": False,
"stats": {
"port": 0
}
}
cfg = easymapping.HaproxyConfigGenerator(result)
haproxy_config = cfg.generate(line_list)
assert len(haproxy_config) > 0
# Verify proto fcgi is in the output
assert "proto fcgi" in haproxy_config
# Verify Unix socket path is used
assert "/run/php/php-fpm.sock" in haproxy_config
# Verify TCP connection is also present
assert "172.17.0.3:9000" in haproxy_config
path = os.path.dirname(os.path.realpath(__file__))
with open(path + "/expected/services-fcgi.txt", 'r') as expected_file:
assert expected_file.read() == haproxy_config
assert [] == cfg.certbot_hosts
# test_parser_finds_services_raw() # test_parser_finds_services_raw()
# test_parser_tcp() # test_parser_tcp()
# test_parser_multiple_hosts() # test_parser_multiple_hosts()