Sunday, September 14, 2025

How to install dockerized HAProxy with ACME and backed by NGINX with PHP

HAProxy (short for High Availability Proxy) is an open-source software that acts as a load balancer and proxy server for TCP and HTTP-based applications. It is widely used in both small and large-scale production environments to improve performance, reliability, and scalability of web and application services.

Any L7 load balancer (reverse http proxy) nowadays is used for SSL/TLS termination and very often with combination with ACME (Automatic Certificate Management Environment).  

How ACME works? Below is the simplified process ...

  1. Account Setup
    • Your ACME client (like Certbot, acme.sh, or HAProxy’s built-in ACME support) registers with the CA.
  2. Domain Validation
    • The CA challenges the client to prove it controls the domain (HTTP-01, DNS-01, or TLS-ALPN-01 challenge).
    • Example:
      • For HTTP-01, the client places a special token on your web server, and the CA checks it.
      • For DNS-01, the client places a special token on your DNS server, and the CA checks it. 
        • acme.sh creates a TXT record value that must be placed under
          • _acme-challenge.uw.cz
  3. Certificate Issuance
    • Once validated, the CA issues an SSL/TLS certificate automatically.
  4. Renewal
    • The client renews certificates before they expire, often without human involvement.

I use DNS-01 CA challenge, therefore integration with DNS provider is necessary. I use Active24.cz DNS provider. 

For my personal load-balancer I use VM with 2 vCPUs, 2 GB RAM, 10 GB vSSD, 1x vNIC, Linux OS - Debian 13.0

If you are interested how to install and configure above solution, keep reading.

HAproxy Basic Concept 

Before installation and configuration we should be familiar with HAproxy's basic concepts and terminology. Here's a breakdown of its core concepts.

HAProxy (High Availability Proxy) is an open-source software solution designed to provide high availability, load balancing, and proxying for TCP and HTTP-based applications. It acts as an intermediary, sitting between the clients and the backend servers, to efficiently distribute incoming network traffic.

Key Concepts

  • Frontends: These define how HAProxy listens for incoming client requests. A frontend specifies the IP addresses and ports that HAProxy binds to. It also includes rules for processing and routing requests, such as SSL termination and access control lists (ACLs).

  • Backends: A backend is a group of servers to which a frontend forwards requests. It defines the pool of servers, their IP addresses, and ports. The backend also specifies the load balancing algorithm that HAProxy will use to distribute traffic among the servers.

  • Load Balancing Algorithms: These are the rules HAProxy uses to decide which server in a backend should receive a new request. Common algorithms include:

    • Round Robin: Distributes requests sequentially to each server in the pool. This is the default algorithm and works well when all servers have similar capabilities.

    • Least Connections: Routes new connections to the server with the fewest active connections. This is useful when the time it takes to process a request varies between servers.

    • Source: Ensures a client is always directed to the same server based on their source IP address. This is crucial for applications that require session persistence.

Core Features

  • High Availability: As its name suggests, HAProxy is built to ensure web services are always accessible. It achieves this by distributing traffic across multiple servers, preventing a single point of failure. If one server goes down, HAProxy automatically redirects traffic to the remaining healthy servers.

  • Health Checks: HAProxy continuously monitors the health of the backend servers. If a server becomes unresponsive, HAProxy automatically removes it from the pool and stops sending traffic to it. It will add the server back once it's healthy again.

  • SSL Offloading: This feature allows HAProxy to handle the SSL/TLS encryption and decryption process. By offloading this computationally intensive task from the backend servers, HAProxy improves overall performance and simplifies SSL certificate management by centralizing it.

HAProxy Installation 

Official HAproxy documentation is available at https://docs.haproxy.org/

Install Debian

Debian installation is out of scope. After standard Debian minimal installation, login as root and update the system ...

# Apt sources are in file /etc/apt/sources.list  

apt update && apt upgrade -y 

When system and packages are up to date, install dependencies ...

apt install -y curl git apt-transport-https ca-certificates gnupg lsb-release

Install Docker

curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker 
apt install docker-ce docker-ce-cli containerd.io 

Check installed docker compose version

 root@mailcow:~# docker compose version  
 Docker Compose version v2.39.1  
 root@mailcow:~#   

Prepare Directories to store files

mkdir -p /opt/haproxy
mkdir -p /var/log/haproxy 

TLS Certificates with ZeroSSL and Active24 DNS provider

We will leverage acme.sh to automatically issue & renew the free certificates and we will use DNS-01 challenge, therefore integration with DNS provider is necessary.

Create DNS provider credentials 

Your DNS provider must be supported by acme.sh. See supported providers at https://github.com/acmesh-official/acme.sh/wiki/dnsapi. I use Active24 which is supported. 

Create an .env file with your Active24 credentials: 

cat > /opt/haproxy/.env <<EOF
Active24_ApiKey=your_api_identifier
Active24_ApiSecret=your_api_secret
EOF
 
chmod 600 /opt/haproxy/.env # file should be visible only for root user

Create docker volume

docker volume create haproxy_certs

This volume will be used for acme.sh and SSL/TLS certificate files are stored there.

Register and Generate Certificate Manualy (One Time just to be sure it works)

Since v3, acme.sh uses ZeroSSL as the default Certificate Authority (CA). See. https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA. Account registration (one-time) is required before one can issue new certs. This must be done on web https://zerossl.com/. Try to issue one certificate over web to be sure everything works. After registration, e-mail verification, and validation that certificates can be created, you can continue.

HAproxy docker-compose Deployment

Above section was just test that certificates can be successfully issued with neilpang/acme.sh container. In this section we will configure everything in docker compose file.

Create HAProxy config file ...

cat > /opt/haproxy/haproxy.cfg <<EOF
global
    log stdout format raw local0
    maxconn 2000
    tune.ssl.default-dh-param 2048

defaults
    log     global
    option  httplog
    option  dontlognull
    timeout connect 5s
    timeout client  50s
    timeout server  50s
    retries 3
 
# HTTP
frontend http-in
    bind *:80
    mode http
    redirect scheme https code 301 if !{ ssl_fc }
 
# HTTPS
frontend https-in
    bind *:443 ssl crt /usr/local/etc/haproxy/certs/uw.cz_ecc/uw.cz.pem
    mode http
    option forwardfor
    default_backend web-backend

backend web-backend
    mode http
    option httpchk GET /
    balance roundrobin
    server web1 10.200.2.3:443 verify none check
 
EOF

Create docker compose file

cat > /opt/haproxy/docker-compose.yml <<EOF
volumes:
  certs:
    driver: local
 
networks:
  proxy:
    external: false
 
services:
  haproxy:
    image: haproxy:latest
    container_name: haproxy
    user: root 
    networks:
      - proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - certs:/usr/local/etc/haproxy/certs:ro
      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    restart: unless-stopped

  acme:
    image: neilpang/acme.sh
    container_name: acme
    networks:
      - proxy
    volumes:
      - certs:/acme.sh
    environment:
      - 
Active24_ApiKey=\${Active24_ApiKey}                    # Active24 API Key
      - Active24_ApiSecret=\${Active24_ApiSecret}            # Active24 API Secret
    command: daemon --foreground    
    restart: unless-stopped
 
EOF

Run Docker Stack (HAProxy with ACME)

docker compose up -d

Register account at ZeroSSL - Initial setup (one-time)

# OPTIONAL STEP because latest acme.sh versions use ZeroSSL as default CA anyway
# Set default CA
docker exec -it acme acme.sh --set-default-ca --server zerossl

# Register account
docker exec -it acme acme.sh --register-account -m david.pasek@gmail.com --server zerossl


Issue cert with automated renewal and reload hook

# Issue cert with reload hook
docker exec -it acme acme.sh --issue \
  --dns dns_active24 \
  -d uw.cz -d '*.uw.cz' \
  --key-file       /acme.sh/uw.cz_ecc/uw.cz.key \
  --fullchain-file /acme.sh/uw.cz_ecc/fullchain.cer \
  --reloadcmd "sh -c 'cat /acme.sh/uw.cz_ecc/uw.cz.key /acme.sh/uw.cz_ecc/fullchain.cer > /acme.sh/uw.cz_ecc/uw.cz.pem && chmod 644 /acme.sh/uw.cz_ecc/uw.cz.pem && docker kill -s HUP haproxy'"

How renewal works automatically?

  • acme.sh daemon wakes up daily.
  • If a certificate is within 30 days of expiration, it renews it.
  • After renewal, it executes the --reloadcmd to update HAProxy without downtime.

Your HAProxy should be up and running now at (10.200.2.4)

Your backend HTTP server (10.200.2.3) should be accessible over HTTPS.

In case of troubles, use troubleshooting tips on section below. 

HAproxy docker-compose Troubleshooting

Troubleshoot acme.sh

you can login into acme container for troubleshooting ... 

docker logs acme  

docker exec -it acme sh -c "/bin/sh" 

docker run --rm -it -v haproxy_certs:/acme.sh busybox cat /acme.sh/uw.cz_ecc/uw.cz.pem 

docker run --rm -it -v haproxy_certs:/acme.sh busybox ls -l /acme.sh/uw.cz_ecc/uw.cz.pem

We can check generated files by following commands ...

docker run --rm -it -v haproxy_certs:/acme.sh busybox cat /acme.sh/uw.cz_ecc/uw.cz.pem 

docker run --rm -it -v haproxy_certs:/acme.sh busybox ls -l /acme.sh/uw.cz_ecc/uw.cz.pem

Certificate verification in haproxy_certs docker volume

We can verify that the cert is really ZeroSSL with following command ...

docker run --rm -v haproxy_certs:/acme.sh alpine \
  sh -c "apk add --no-cache openssl >/dev/null && \
  openssl x509 -in /acme.sh/uw.cz_ecc/fullchain.cer -noout -issuer -subject"

 root@lb:/opt/haproxy# docker run --rm -v haproxy_certs:/acme.sh alpine  sh -c "apk add --no-cache openssl >/dev/null && \  
  openssl x509 -in /acme.sh/uw.cz_ecc/fullchain.cer -noout -issuer -subject"  
 issuer=C=AT, O=ZeroSSL, CN=ZeroSSL ECC Domain Secure Site CA  
 subject=CN=uw.cz  
 root@lb:/opt/haproxy#   

List all certificates acme.sh manages

docker exec -it acme acme.sh --list

 root@lb:/opt/haproxy# docker exec -it acme acme.sh --list  
 Main_Domain     KeyLength     SAN_Domains     CA              Created                  Renew  
 uw.cz           "ec-256"      *.uw.cz         ZeroSSL.com     2025-09-13T18:06:21Z     2025-11-11T18:06:21Z  
 root@lb:/opt/haproxy#   

Troubleshoot haproxy

you can login into acme container for troubleshooting ... 

docker logs haproxy

docker exec -it haproxy sh -c "/bin/sh" 

Stop and start again HAProxy and ACME.SH

docker compose down
docker compose up -d
 

HAProxy acme.sh certificate renewal

Automatic certificate renewal

Docker compose and steps above provide automatic renewal

docker exec -it acme acme.sh --install-cert -d uw.cz \
  --cert-file /acme.sh/uw.cz_ecc/uw.cz.cer \
  --key-file /acme.sh/uw.cz_ecc/uw.cz.key \
  --fullchain-file /acme.sh/uw.cz_ecc/fullchain.cer \
  --reloadcmd "cat /acme.sh/uw.cz_ecc/uw.cz.key /acme.sh/uw.cz_ecc/fullchain.cer > /acme.sh/uw.cz_ecc/uw.cz.pem && chmod 644 /acme.sh/uw.cz_ecc/uw.cz.pem" 

Manual certificate renewal

docker exec -it acme acme.sh --renew -d uw.cz --force 
docker compose down
docker compose up -d 

Explanation:

  • --renew -d uw.cz → Attempts to renew the certificate for uw.cz
  • --force → Forces the renewal even if it’s not close to expiration
  •  docker compose down → Shutdown the stack
  •   docker compose up -d → Start the stack in background (as daemon)

Automated restart of haproxy container

Create a small script at /opt/haproxy/restart-weekly.sh

#!/bin/bash
# Weekly restart of selected HAProxy containers
 
# Timestamp to log file
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restarting HAProxy container..." >> /var/log/haproxy-weekly-restart.log 2>&1
 
cd /opt/haproxy || exit 1
docker compose restart haproxy >> /var/log/haproxy-weekly-restart.log 2>&1 

Make the script executable

chmod +x /opt/haproxy/restart-weekly.sh 

Schedule the script in cron

Edit the root crontab:

crontab -e

...and add following line to run it every Saturday at 3:00 AM:

0 3 * * 0 /opt/haproxy/restart-weekly.sh  

 

No comments:

Post a Comment

Rocky Linux - Basic Operational Procedures

Rocky Linux is an open-source, community-driven Linux distribution designed to be a bug-for-bug compatible downstream rebuild of Red Hat Ent...