Skip to main content

Automating HAProxy – Safe Deployment and Soft Restarts

In my previous post, I demonstrated how to automatically generate HAProxy configurations from human-readable documentation. In this follow-up, I’ll walk through safely applying those configurations in a live environment.

As outlined in the haproxy_config_automation_example README, once the generated configuration has been reviewed and validated, it can be safely written to the active HAProxy config file by redirecting output:

python haproxy_config_generator.py > haproxy_config.cfg

This approach was chosen deliberately. By requiring manual redirection, the script encourages visibility into what changes are being made and enforces a clear, intentional step before applying them—minimizing the risk of silent misconfigurations.


HAProxy’s Graceful Reloads

One of HAProxy’s most powerful features is its ability to reload configurations gracefully. If the new configuration contains errors but the previous version is still in memory, HAProxy continues running with the last known-good config while logging the failure.

To initiate a soft reload and take advantage of this behavior, I send the following signal to the HAProxy container:

kill -HUP 1

This tells HAProxy to reload the configuration without restarting the process. If the new config is invalid, HAProxy logs the issue but keeps serving existing traffic. Here’s what a failure might look like in the logs:

[NOTICE]   (1) : Reloading HAProxy
...
[ALERT]    (1) : config : Fatal errors found in configuration.
[WARNING]  (1) : Loading failure!

If this had been a hard reload, everything would go down. Instead, we can troubleshoot with confidence, knowing that any previously working connections are still being served. (It never hurts to double-check, of course.)

Here’s what a successful reload looks like:

[NOTICE]   (1) : Reloading HAProxy
[WARNING]  (44) : Proxy http_front stopped (cumulated conns: FE: 205, BE: 0).
[WARNING]  (44) : Proxy https_front stopped (cumulated conns: FE: 555, BE: 0).
[WARNING]  (44) : Proxy catchall_backend stopped (cumulated conns: FE: 0, BE: 267).
...
[WARNING]  (44) : Proxy <HTTPCLIENT> stopped (cumulated conns: FE: 0, BE: 0).
[NOTICE]   (1) : New worker (73) forked
[NOTICE]   (1) : Loading success.
[NOTICE]   (1) : haproxy version is 2.7.8-58c657f
[WARNING]  (1) : Former worker (44) exited with code 0 (Exit)

One of the most impressive aspects of HAProxy’s reload process is how seamless it is for external clients. Once the new configuration is validated and the new worker process is active, HAProxy immediately begins routing traffic through the updated settings—without interrupting existing sessions.

This makes it especially effective in containerized environments. Changes can be staged and tested in isolation, then applied with a single signal. New requests are routed using the new configuration instantly, while the old worker continues to handle any remaining active connections until it shuts down cleanly.

If the config fails, HAProxy logs the error and rolls back to the last valid state automatically—allowing for rapid recovery and confident iteration with near-zero risk.

Automating HAProxy – Configuration Management

Config management from human-readable documentation

Recently, my home lab grew to the point where manually updating HAProxy ACL rules became a chore. I needed to ensure my infrastructure could scale with minimal friction. Since HAProxy configurations can become unwieldy and difficult to manage, I designed and implemented a tool that lets me maintain a single, living document defining the business logic for HAProxy routing.

To tackle this, I chose Python as my weapon of choice. I needed something that could parse semi-structured data from a human-readable file format—something I could write quickly and clearly—and convert it into a format HAProxy could use.

I wanted HAProxy to interpret lines like the following:

https://www.foobar.com     -> http://foobarcom-internal-server-1:80  
https://foobar.com         -> http://foobarcom-internal-server-1:80  
https://stage.foobar.com   -> http://stage-foobarcom-internal-server-1:80  

My goal was to offload SSL termination to HAProxy so internal services wouldn’t need to manage HTTPS at all. I wanted to spin up new servers quickly, define a route, and go.


First Attempt: Functional Overkill?

My first draft used a set of functions: one to extract the config block from the README.md, two more to generate HAProxy config sections, and a final multi-line f-string to build the file contents, with a context manager to handle writing.

This worked, but a good friend (and fellow co-potato-battery-piloting-a-meatsuit-doomed-to-inevitable-decay) pointed out that my approach felt more like software engineering than systems administration. For sysadmins, readability and linear structure often trump modularity, especially in one-off scripts. It was a valid critique, and one I took to heart.


Refining the Approach: Simpler, More Explicit

I restructured the script around two clearly separated tasks:

  1. Parse the config block from README.md
  2. Generate the HAProxy configuration

Step 1: Extracting HAProxy Rules

with open('README.md', 'r') as file_handle:
    configuration_data = re.match(
        r'(?is).*# HAProxy Configuration.*`+\w*\n+(?P<conf>.*\w+)\n+`',
        file_handle.read()
    )

Then, I split the block and parsed each line:

for line in configuration_data.groupdict().get('conf').strip().split("\n"):
    if match := re.match(
        r'^\s+https://(?P<ingress_host>\S+)/?\s+->\s+http://(?P<destination_service>\S+)\s*$',
        line
    ):
        destination_service = match.group('destination_service')
        ingress_host = match.group('ingress_host')
        safe_ingress_host = re.sub(r'\W', '_', ingress_host)
        destination_service += ':80' if not re.match(r'.*:\d+$', destination_service) else ''

        configured_hosts[ingress_host] = {
            'safe_ingress_host': safe_ingress_host,
            'destination_service': destination_service
        }

Step 2: Generating the HAProxy Config

We begin by printing the static HAProxy preamble:

print("""
global
    # intermediate configuration
    ...
defaults
    timeout connect 5000
    timeout client 50000
    timeout server 50000
""")
Note: The preamble here is an excerpt for brevity.

For the full version please see the referenced GitHub repo: haproxy_config_automation_example

Then generate the ACLs:

for ingress_host in sorted(configured_hosts.keys()):
    print(f"""
    acl host_{configured_hosts[ingress_host]['safe_ingress_host']} hdr(host) -i {ingress_host}
    use_backend {configured_hosts[ingress_host]['safe_ingress_host']}_backend if host_{configured_hosts[ingress_host]['safe_ingress_host']}
""")

Set the default backend:

print("""
    default_backend catchall_backend

backend catchall_backend
    mode http
    http-request deny deny_status 404
""")

Finally, define each backend:

for ingress_host in sorted(configured_hosts.keys()):
    print(f"""
backend {configured_hosts[ingress_host]['safe_ingress_host']}_backend
    mode http
    server only_server {configured_hosts[ingress_host]['destination_service']} check
""")

This creates a clean, maintainable HAProxy config generated directly from documentation. Future improvements (like supporting load balancing across multiple destinations) can easily build on this foundation—but that’s a topic for another post.

Home [lab] is where the heart is..

This is my main site, used for hosting information on projects and hobbies, as well as allowing me to develop my skills further in my spare time.

This theme was built from scratch aimed at maintaining readable contrast and accessibility while mitigating migraine triggers using:

The OS / Software

  • Docker
  • HAProxy
    • Setup to handle my ssl terminations
  • Ubuntu
  • Certbot / LetsEncrypt
    • SSL certs and renewals

The Hardware:

The LAMP stack containers use the following:

V Rising Server Info

While not an official or officially sanctioned server (it’s a private server setup for people who want to explore the game while not necessarily playing on full difficulty) I aim on keeping this server open to all who want to play. I took over hosting from Crushmap when server hosting issues came up. I’ll keep the server up as long as I can and as long as people still play.

Server should be searchable by name from within the V Rising client:
[US-E] Weenie Hut General | No Resets

The wiki offers a wealth of information and resources—most questions about gameplay and related topics can be found there. The featured image is sourced from the official game website, playvrising.com via their mediakit, and all rights remain with the original author.
You can access the wiki home here: vrising.fandom.com.

Current Discord: https://discord.gg/qmsQVPBsjv
I’m also pm friendly and can be reached on discord at @binderlinc

Minecraft Server Info

The server hosting my favorite mod indicated it might be shutting down soon. So I did what any reasonable person with engineering experience would do… I analyzed what I enjoyed about the previous server, took note of what I wanted to change, and researched what I was missing to pull it off.

Thus the minecraft and wordpress servers came to be (more info on the technical in another post, coming soon(tm)) and the modpack is a curated experience meant to create a paced journey into Thaumcraft and magic without immediately throwing one in head first.

The following info should allow anyone interested in joining the mc.thornmire.com server to connect.

First one will need to install the 1.12.2 Forge latest release – Download here: https://files.minecraftforge.net/net/minecraftforge/forge/index_1.12.2.html

I use separate profiles with seperate directories as I play multiple versions of Minecraft so that it’s easier to switch in the launcher. If you want to do the same this can be done by going to Installations and clicking New Installation

Be sure to give it a meaningful name, such as Forge-1.12.2-<modpack_name>.

For the JVM arguments, I’ve had the best results with the following:Note: the `-javaagent:unsup-0.2.3.jar` must be included in order to have the mod loader run.

-XX:+UseG1GC -Xmx4G -Xms4G -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=20 -XX:G1ReservePercent=20 -XX:MaxGCPauseMillis=50 -XX:G1HeapRegionSize=64M -javaagent:unsup-0.2.3.jar


MODPACK INFO
A mod pack exists which was created using Packwiz, and can be reviewed here:
https://github.com/LincT/thornmire_modpack
The pack itself is here for convenience: Thornmire_CommunityV1-1.zip

Once your forge profile is setup, you’ll want the latest stable Unsup release directly from the source:
https://git.sleeping.town/unascribed/unsup/releases (look for the file ending .jar):

To make Unsup sync mods for you, you’ll need an unsup.ini file as well:
ini files can be inspected / edited by a text editor such as Vim, Nano, Notepad, or Notepad++

The unsup.ini should appear as follows:


version=1
preset=minecraft

source_format=packwiz
source=https://raw.githubusercontent.com/LincT/thornmire_modpack/refs/heads/main/pack.toml

[colors]
progress=E15817
button=E15817

If a minecraft.ini is needed as well, it can be added with the following content:

use_envs=true
recognize_nogui=true

[env.client]
marker=net.minecraft.client.main.Main

[env.server]
marker=*

[mmc-component-map]
minecraft=net.minecraft
minecraft=net.fabricmc.intermediary
minecraft=org.quiltmc.hashed
unsup=com.unascribed.sup
unsup=com.unascribed.unsup
fabric=net.fabricmc.fabric-loader
quilt=org.quiltmc.quilt-loader
liteloader=com.mumfrey.liteloader
forge=net.minecraftforge
neoforge=net.neoforged

the jar and ini files should live in the minecraft game directory (not the mods folder)

Once the above is complete, starting the game profile should setup and sync the mods to allow for immediate play.

This post will be updated further with changes.