Docker and HAProxy a match made in heaven

Once upon a time I setup my home network, I could access stuff from outside and all was well in the world. Over time this home network grew and I exposed more and I forwarded more ports from the router to the backend server apps. A few of which were as follows, each had their own port and own domain.

NameDomainInternal PortExternal Port
Gogs (git)g.example.com6000444
Gogs (git) (ssh)g.example.com60012222

There was more than just that but this will do for the purpose of this post, this meant anytime I did any work on my router it was a pain as I had to re-forward all the ports.

Like any enterprise I didnt design in this tech debt nor was it planned, it evolved one app at time and then all of a sudden it was a mess, and I never cleaned up because “its working”, “its too much hassle”,”i’ll do that when I am doing x, actually thats extra work i’ll leave it”, etc etc. Most of you reading this blog will understand, and will have likely used or heard one of those excuses at least once in your lifetime.

So now onto the interesting part, enter docker and haproxy. I couldn’t take the above any more and I was deploying other stuff in docker elsewhere so I began with taking each of the apps and getting them into docker on the host. This was easier than expected with docker hub providing all the images I could want for. I did rebuild the jenkins one though as I did want a few tweaks to how I was using it, but that was just an extra layer on top of the base jenkins image.

Once all the apps were dockerized and exposed on the same internal ports as before, with the exception of owncloud as it was blocking port 443. It got moved to an http port (8080) instead of being directly exposed as ssl.

The next step was haproxy, thanks to this post I was able to get the basics of haproxy working with letsencypt and multiple domains.

My docker startup for haproxy started to look like this:

docker run -d --name haproxy --restart=always \
-p 2222:2222 \
-p 80:80 \
-p 443:443 \
-p 8998:8998 \
-v /srv/haproxy:/usr/local/etc/haproxy:ro \
-v /etc/letsencrypt/live:/certs/live:ro \
-v /etc/letsencrypt/archive:/certs/archive:ro \

Port 2222: Will be the ssh port for git
Port 443: SSL/TLS Port for all webapps
Port 8998: Internal network only, stats for haproxy
port 80: Used for letsencrypt


All these bind on addr which is fine as this is the interface for the docker container not the docker host. Remembering this is important as although the apps on are on the docker host too, we cannot use as that would localhost for the *container* not the docker host.

My haproxy config became something like this:
tune.ssl.default-dh-param 2048
maxconn 2048

log local0
option tcplog
timeout connect 5000ms
timeout check 5000ms
timeout client 30000ms
timeout server 30000ms

listen stats
mode http
stats enable
stats realm Haproxy\ Statistics
stats refresh 20s
stats uri /

frontend letsencypt
bind *:80

default_backend letsencrypt-backend

frontend main
mode http

bind ssl crt /certs/live/ crt /certs/live/ crt /certs/live/ crt /certs/live/

reqadd X-Forwarded-Proto:\ https

acl letsencrypt-acl path_beg /.well-known/acme-challenge/
use_backend letsencrypt-backend if letsencrypt-acl

use_backend bk_o if { ssl_fc_sni }
use_backend bk_h if { ssl_fc_sni }
use_backend bk_j if { ssl_fc_sni }
use_backend bk_g if { ssl_fc_sni }

default_backend bk_c

frontend g_ssh
mode tcp

default_backend bk_g_ssh
timeout client 1h

backend bk_g_ssh
mode tcp
server g

backend bk_h
mode http
option forwardfor
server h

backend bk_o
mode http
option forwardfor
server o

backend bk_j
mode http
option forwardfor
server j

http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }

reqrep ^([^\ :]*)\ /(.*) \1\ /\2
acl response-is-redirect res.hdr(Location) -m found
rspirep ^Location:\ (http|https)://*) Location:\ \1://\2 if response-is-redirect

backend bk_g
mode http
option forwardfor
server git

backend letsencrypt-backend
mode http
option forwardfor
server letsencrypt


The sharp eyed will notice the ssl certs mentions combined.pem, yet letsencypt does produce combined certs well to achieve that a quick bash script creates those.
#!/bin/bash -xe

cd /etc/letsencrypt/live
for i in `ls`;
for x in `ls $i/fullchain*`;
cat $x $(echo $x | sed 's/fullchain/privkey/') > $(echo $x | sed 's/fullchain/combined/')


I then added a crontab entry to renew all my certs, the post-hook here is important as it first of calls the combine script to generate our combined ssl certs then it tells docker to restart haproxy so that it picks up the new certs.

certbot renew --tls-sni-01-port=8888 --noninteractive --force-renewal --post-hook "sh /data/scripts/util/; docker restart haproxy"


My new port forwarding became:

NameDomainInternal PortExternal Port
Gogs (git) (web)g.example.com6000
Gogs (git) (ssh)g.example.com6001
haproxy (web)
haproxy (git ssh)g.example.com22222222
haproxy (letsencrypt)

Leave a Reply

Your email address will not be published. Required fields are marked *


This blog is kept spam free by WP-SpamFree.