{"id":22933496,"url":"https://github.com/bluebrown/moby-ingress","last_synced_at":"2025-08-12T16:32:51.733Z","repository":{"id":44108848,"uuid":"385762655","full_name":"bluebrown/moby-ingress","owner":"bluebrown","description":"Label based HAProxy ingress-controller for docker and docker swarm","archived":true,"fork":false,"pushed_at":"2022-02-13T21:32:12.000Z","size":403,"stargazers_count":7,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-18T18:28:30.548Z","etag":null,"topics":["docker","docker-compose","docker-swarm","haproxy","ingress","labels"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bluebrown.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-07-13T23:49:04.000Z","updated_at":"2024-09-03T20:51:03.000Z","dependencies_parsed_at":"2022-09-03T11:51:00.110Z","dependency_job_id":null,"html_url":"https://github.com/bluebrown/moby-ingress","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bluebrown/moby-ingress","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fmoby-ingress","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fmoby-ingress/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fmoby-ingress/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fmoby-ingress/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bluebrown","download_url":"https://codeload.github.com/bluebrown/moby-ingress/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bluebrown%2Fmoby-ingress/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270097295,"owners_count":24526643,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-12T02:00:09.011Z","response_time":80,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","docker-compose","docker-swarm","haproxy","ingress","labels"],"created_at":"2024-12-14T11:30:00.214Z","updated_at":"2025-08-12T16:32:51.452Z","avatar_url":"https://github.com/bluebrown.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Docker Compose/Swarm Haproxy Ingress Controller\n\nThe aim of the project is it to create dynamic ingress rules for swarm services through labels. This allows to create new services and change the haproxy configuration without any downtime or container rebuild.\n\nThe manager service is responsible for generating a valid haproxy configuration file from the labels. The loadbalancer instances scrape the configuration periodically and reload the worker \"hitless\" if the content has changed.\n\n## Synopsis\n\n```yml\nversion: \"3.9\"\n\nservices:\n  ingress-loadbalancer:\n    image: swarm-haproxy-loadbalancer\n    environment:\n      MANAGER_ENDPOINT: http://ingress-manager:6789/\n      SCRAPE_INTERVAL: \"25\"\n      STARTUP_DELAY: \"5\"\n    ports:\n      - 3000:80 # ingress port\n      - 8765:8765 # stats page\n      - 9876:9876 # socket cli\n    depends_on:\n      - ingress-manager\n\n  ingress-manager:\n    image: swarm-haproxy-manager\n    # this is the default template path. the flag is only set here\n    # to show how to override the default template path\n    command: --template templates/haproxy.cfg.template --log-level debug\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock:rw\n    ports:\n      - 6789:6789\n    deploy:\n      # needs to be on a manager node to read the services\n      placement: { constraints: [\"node.role == manager\"] }\n    # manager labels are added to the container\n    # instead of the services under the deploy key\n    labels:\n      # if the ingress class is provided the services are\n      # filtered by the ingress class otherwise all services are checked\n      ingress.class: haproxy\n      ingress.global: |\n        spread-checks 15\n      ingress.defaults: |\n        timeout connect 5s\n        timeout check 5s\n        timeout client 2m\n        timeout server 2m\n        retries 1\n        retry-on all-retryable-errors\n        option redispatch 1\n        default-server check inter 30s\n      ingress.frontend.default: |\n        bind *:80\n        option forwardfor except 127.0.0.1\n        option forwardfor header X-Real-IP\n        http-request disable-l7-retry unless METH_GET\n\n  app:\n    image: nginx\n    deploy:\n      replicas: 2\n      # service labels are added under the deploy key\n      labels:\n        # the ingress class of the manager\n        ingress.class: haproxy\n        # the application port inside the container (default: 80)\n        ingress.port: \"80\"\n        # rules are merged with corresponding frontend\n        # the service name is used available in go template format\n        ingress.frontend.default: |\n          default_backend {{ .Name }}\n        # backend snippet are added to the backend created from\n        # this service definition\n        ingress.backend: |\n          balance leastconn\n          option httpchk GET /\n          acl allowed_method method HEAD GET POST\n          http-request deny unless allowed_method\n```\n\nSee the [official haproxy documentation](https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/) to learn more about haproxy configuration. The settings are identical to the official haproxy version.\n\nCurrently it only works when deploying the *backend* services with swarm. The manager can be deployed with a normal container. This is because the labels for the manager are provided on container level while the backends are created from service definitions and their labels.\n\n\u003e Note\n\u003e *Be careful which ports you publish in production*\n\n## Template\n\nThe labels are parsed and passed into a haproxy.cfg template. The default template looks like the below.\n\n```go\nresolvers docker\n    nameserver dns1 127.0.0.11:53\n    resolve_retries 3\n    timeout resolve 1s\n    timeout retry   1s\n    hold other      10s\n    hold refused    10s\n    hold nx         10s\n    hold timeout    10s\n    hold valid      10s\n    hold obsolete   10s\n\nglobal\n    log          fd@2 local2\n    stats timeout 2m\n    {{ .Global | indent 4 | trim }}\n\ndefaults\n    log global\n    mode http\n    option httplog\n    {{ .Defaults | indent 4 | trim }}\n\nlisten stats\n    bind *:8765\n    stats enable\n    stats uri /\n    stats refresh 15s\n    stats show-legends\n    stats show-node {{ println \"\" }}\n\n{{- range $frontend, $config := .Frontend }}\nfrontend {{$frontend}}\n    {{$config | indent 4 | trim }}\n{{ end }}\n\n{{- range $backend, $config := .Backend }}\nbackend {{$backend}}\n    {{ $config.Backend | indent 4 | trim }}\n    server-template {{ $backend }}- {{ $config.Replicas }} {{ if ne .EndpointMode \"dnsrr\" }}tasks.{{ end }}{{ $backend }}:{{ default \"80\" $config.Port }} resolvers docker init-addr libc,none\n{{ end }}\n\n{{ println \"\"}}\n```\n\nThe data types passed into the template have the following format. The ingressClass is used to filter services. That way it is possible to run the separate stacks with separate controllers and a different ingress class if required.\n\nFrontend snippets in the backend struct are executed as template and merged with the frontend config from the ConfigData struct. That is why it is not returned as json and not directly used in the template. The endpoint mode is used to determine what dns pattern should be used to query the docker dns resolver for the service.\n\n```go\ntype ConfigData struct {\n IngressClass string             `json:\"-\" mapstructure:\"class\"`\n Global       string             `json:\"global,omitempty\"`\n Defaults     string             `json:\"defaults,omitempty\"`\n Frontend     map[string]string  `json:\"frontend,omitempty\"`\n Backend      map[string]Backend `json:\"backend,omitempty\"`\n}\n\ntype Backend struct {\n EndpointMode swarm.ResolutionMode `json:\"endpoint_mode,omitempty\"`\n Port         string               `json:\"port,omitempty\"`\n Replicas     uint64               `json:\"replicas,omitempty\"`\n Frontend     map[string]string    `json:\"-\"`\n Backend      string               `json:\"backend,omitempty\"`\n}\n```\n\n## Manager API\n\nExample HTTP requests can be found in the assets folder.\n\nThe configuration can be fetched via the root endpoint of the manager service. The manager will return the current config immediately if the `Config-Hash` header has not been set or its value is different from the the current configs hash.\n\nThe content hash is a md5sum of the current config. It is used to communicate to the server if the client has the current config already. If send hash matches the current config, the manager will respond in a long-polling fashion. That means, it will leave the connection open and not respond at all until the haproxy config file in memory hash changed and a new hash has been computed. This mechanism is meant to avoid sending data across the network that the client already has.\n\n```shell\n# immediate response\ncurl -i localhost:8080\n# immediate response if hash does not match, otherwise long polling\ncurl -H 'Content-Hash: \u003cmd5sum-of-local-config\u003e' localhost:8080\n```\n\nThe raw data to populate the template is also available in json format. Since the content hash is the hash of the rendered haproxy config, it is not really meant to be used for the json response. If you still want to use it, you need to read the hash from the response header `Content-Hash` as computing the md5sum of the json content will be always different.\n\n```shell\ncurl -i -H 'Accept: application/json' localhost:8080\n```\n\n### Example Config Response\n\n```c\nresolvers docker\n    nameserver dns1 127.0.0.11:53\n    resolve_retries 3\n    timeout resolve 1s\n    timeout retry   1s\n    hold other      10s\n    hold refused    10s\n    hold nx         10s\n    hold timeout    10s\n    hold valid      10s\n    hold obsolete   10s\n\nglobal\n    log          fd@2 local2\n    stats timeout 2m\n    spread-checks 15\n\ndefaults\n    log global\n    mode http\n    option httplog\n    timeout connect 5s\n    timeout check 5s\n    timeout client 2m\n    timeout server 2m\n    retries 1\n    retry-on all-retryable-errors\n    option redispatch 1\n    default-server check inter 30s\n\nlisten stats\n    bind *:8765\n    stats enable\n    stats uri /\n    stats refresh 15s\n    stats show-legends\n    stats show-node\n\nfrontend default\n    bind *:80\n    option forwardfor except 127.0.0.1\n    option forwardfor header X-Real-IP\n    http-request disable-l7-retry unless METH_GET\n    default_backend app\n\nbackend app\n    balance leastconn\n    option httpchk GET /\n    acl allowed_method method HEAD GET POST\n    http-request deny unless allowed_method\n    server-template app- 2 app:80 resolvers docker init-addr libc,none\n```\n\n### Example JSON response\n\n```json\n{\n  \"global\": \"spread-checks 15\\n\",\n  \"defaults\": \"timeout connect 5s\\ntimeout check 5s\\ntimeout client 2m\\ntimeout server 2m\\nretries 1\\nretry-on all-retryable-errors\\noption redispatch 1\\ndefault-server check inter 30s\\n\",\n  \"frontend\": {\n    \"default\": \"bind *:80\\noption forwardfor except 127.0.0.1\\noption forwardfor header X-Real-IP\\nhttp-request disable-l7-retry unless METH_GET\\ndefault_backend app\\n\"\n  },\n  \"backend\": {\n    \"app\": {\n      \"endpoint_mode\": \"dnsrr\",\n      \"port\": \"80\",\n      \"replicas\": 2,\n      \"backend\": \"balance leastconn\\noption httpchk GET /\\nacl allowed_method method HEAD GET POST\\nhttp-request deny unless allowed_method\\n\"\n    }\n  }\n}\n```\n\n### Update the template at runtime\n\nIt is possible to change the template at runtime via PUT request.\n\n```shell\ncurl -i -X PUT localhost:8080 --data-binary @path/to/template\n```\n\n## Local Development\n\nIf you have the repository locally, you can use the `Makefile` to run a example deployment.\n\n```shell\nmake build \u0026\u0026 make stack \u0026\u0026 make cli-service # build the images deploy a stack and a service\ncurl -i \"localhost:3000\" # backend my-stack_app\ncurl -i \"localhost:3000/test/\" # backend test\ncurl -i \"localhost:8765\" # haproxy stats page\ncurl -i \"localhost:6789\" # rendered template\ncurl -i -H 'Accept: application/json' \"localhost:6789\" # json data\nmake clean # remove the stack and service\n```\n\n## Haproxy Socket\n\nIf you publish the port 9876 on the loadbalancer you can use `socat` to connect to the socket cli.\n\n```bash\n$ socat tcp-connect:127.0.0.1:9876 -\n$ prompt\n$ master\u003e help\n help\nThe following commands are valid at this level:\n  @!\u003cpid\u003e                                 : send a command to the \u003cpid\u003e process\n  @\u003crelative pid\u003e                         : send a command to the \u003crelative pid\u003e process\n  @master                                 : send a command to the master process\n  operator                                : lower the level of the current CLI session to operator\n  reload                                  : reload haproxy\n  show cli level                          : display the level of the current CLI session\n  show cli sockets                        : dump list of cli sockets\n  show proc                               : show processes status\n  show version                            : show version of the current process\n  user                                    : lower the level of the current CLI session to user\n  help [\u003ccommand\u003e]                        : list matching or all commands\n  prompt                                  : toggle interactive mode with prompt\n  quit                                    : disconnect\n```\n\n## Usage with Compose\n\nCompose is supported, note that you must add the labels to the service instead of the deploy key.\n\n```yaml\napp:\n  image: nginx\n  deploy:\n    replicas: 2\n  labels:\n    ingress.class: haproxy\n    ingress.port: \"80\"\n    ingress.frontend.default: |\n      default_backend {{ .Name }}\n    ingress.backend: |\n      balance leastconn\n      option httpchk GET /\n      acl allowed_method method HEAD GET POST\n      http-request deny unless allowed_method\n```\n\nThe controller will search independently from the compose project for services, that means the manager does not have to be deployed as part of the same compose project, the same way it doest have to be part of the same stack when using swarm.\n\n### Limitation\n\nSince the loadbalancer will use network aliases to discover the service via dockers dns resolver, you need to ensure that when loadbalancer across projects, the service names are unique as they will be merged into a single service otherwise. This is not a direct limitation of the controller. It is due to the way dockers networking is implemented and the way compose uses network aliases. The same problem would occur if you were to join 2 compose projects on the same docker network and used the same service name in each project. Dockers dns resolver would give records for all services under the given alias. This is because the project name is not part of the alias.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluebrown%2Fmoby-ingress","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbluebrown%2Fmoby-ingress","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbluebrown%2Fmoby-ingress/lists"}