If you simply take the official HAProxy docker image, you’ll quickly see that your logs will not show.

That’s because by default, HAProxy won’t log to stdout - you need to have a facility (rsyslog) that will take those logs and ship it to somewhere.

Location of the Docker-for-mac preferences menu

A Dockerfile with RSYSLOG for HAProxy logging

Although adding rsyslog is straightforward with the alpine-based image, we can go further with linking the generated haproxy.log from rsyslog to /dev/stdout such that whenever rsyslog writes HAProxy logs, it goes directly to stdout (such that any log aggregation driver from Docker can pick).

Given that we need to have some configuration files and a custom entrypoint, I created the following file structure:

.
├── Dockerfile          # image description
├── entrypoint.sh       # executable script to launch both
|                       # haproxy and rsyslog.
└── etc                 # configuration files
    ├── haproxy.cfg     # haproxy config
    └── rsyslog.conf    # rsyslog config

1 directory, 5 files

The Dockerfile doesn’t do much:

  • starts from the official HAProxy image;
  • adds rsyslog;
  • creates the file that rsyslog will log contents to;
  • links the haproxy log with the stdout device.

It looks like this:

FROM haproxy:1.8-alpine

RUN set -x	&& \
	apk upgrade --update					&&  \
	apk add bash ca-certificates rsyslog	                &&  \

	mkdir -p /etc/rsyslog.d/				&&  \

	touch /var/log/haproxy.log				&&  \

        # here's the catch: by creating a soft-link that 
        # links /var/log/haproxy.log to /dev/stdout whatever 
        # rsyslogd writes to the file will endup being
        # propagated to the standard output of the container
	ln -sf /dev/stdout /var/log/haproxy.log

# Include our configurations (`./etc` contains the files that we'd
# need to have in the `/etc` of the container).
ADD ./etc/ /etc/

# Include our custom entrypoint that will the the job of lifting
# rsyslog alongside haproxy.
ADD ./entrypoint.sh /usr/local/bin/entrypoint

# Set our custom entrypoint as the image's default entrypoint
ENTRYPOINT [ "entrypoint" ]

# Make haproxy use the default configuration file
CMD [ "-f", "/etc/haproxy.cfg" ]

Now looking at the configuration files, we have two: the rsyslog configuration and the haproxy configuration.

The rsyslog is tailored with a facility (local0) to be used solely by haproxy.

# Loads the imudp into rsyslog address space
# and activates it.
# IMUDP provides the ability to receive syslog
# messages via UDP.
$ModLoad imudp

# Address to listen for syslog messages to be 
# received.
$UDPServerAddress 0.0.0.0

# Port to listen for the messages
$UDPServerRun 514

# Take the messages of any priority sent to the
# local0 facility (which we reference in the haproxy
# configuration) and send to the haproxy.log 
# file.
local0.* -/var/log/haproxy.log

# Discard the rest
& ~

The HAProxy configuration then, with local0 as the target for the logs:

# Setting `log` here with the address of 127.0.0.1 will have the effect
# of haproxy sending the udp log messages to its own rsyslog instance
# (which sits at `127.0.0.1`) at the `local0` facility including all
# logs that have a priority greater or equal than debug
global
                maxconn                   2046
                log                       127.0.0.1       local0  debug


# By default we want to use the same logging parameters as defined
# in the global section.
defaults
                log                       global


# Simple frontend that will take some HTTP requests from port :80
# and then always pick the `backend_default` default backend.
#
# Naturally, this configuration you'd replace by whatever makes more
# sense to your application.
frontend        http
                bind                      :80
                default_backend           backend_default

# A non-existent backend that would never return - again, this would
# be replaced by something that makes sense to your application, maybe
# something that gets generated whenever a new container goes up or
# it could be picked via DNS (as HAProxy now supports SRV records).
backend         backend_default
                server                    local-server 127.0.0.1:8080

We can then raise those two components (HAProxy and RSYSLOG) by making the entrypoint do so:

#!/bin/bash

set -o errexit
set -o nounset

readonly RSYSLOG_PID="/var/run/rsyslogd.pid"

main() {
  start_rsyslogd
  start_lb "$@"
}

# make sure we have rsyslogd's pid file not
# created before
start_rsyslogd() {
  rm -f $RSYSLOG_PID
  rsyslogd -n
}

# Starts the load-balancer (haproxy) with 
# whatever arguments we pass to it ("$@")
start_lb() {
  exec haproxy "$@"
}

main "$@"

Create the image and you’re good to go!

Closing thoughts

Redirecting all the logs from rsyslog to the standard out device makes haproxy logs play nice with docker default logging. It also has the upside of allowing us to not be concerned about log rotation from within the load-balancer container - either the log aggregation platform that takes the HAProxy logs would take care of it or even the standard json-file driver would do the log rotation.

This configuration is fairly simplistic - it doesn’t account the eventual failure of rsyslog that might occur. In that case, I’d say the best would be to kill the container and let it be recreated (although I never saw rsyslog diying like that).

Please let me know if you have any questions or if you have any suggestions.

I’m @cirowrc and would appreciate!

Have a good one!

finis