The Why

The Docker composition on one of my side projects has recently got to 6 containers: SMTP-in, SMTP-out, app, subscription, website, and certbot. Since I have deployed a 0.1.0 version to DigitalOcean a couple of weeks ago, I caught myself feeling increasingly anxious about losing logs every time I deployed a new version of the container.

Every container is streaming its logs to STDOUT. I wanted to have a persistent log file for each container, which Docker almost does out-of-the-box, with the “little” caveat that those files are removed every time the container is rebuilt. 🙃

One of the things that I’ve challenged myself to do on this side project is to have it as self-contained as possible. — This is why I’ve set up my own SMTP server instead of buying a transactional email subscription. 😎

This is the kind of simplicity I was referring to when I said “simple log aggregation” in the title: no third-party services. I wanted it to be a Docker composition that I can start on my laptop as easily and as cleanly as on a VM in some cloud.

At the end of the day, all I needed is to have those logs — plain text or JSON — stored somewhere so that I can grep or jq them whenever I want to do see what happened when.

After googling around for a few mornings, I ended up deploying an additional logger container that runs Syslog and then instructed all the other containers to stream their logs into it. I’ve seen this called “the sidecar pattern” in other places. The logger container’s files live in a mounded directory, so they can have a life outside Docker, and across the container life cycle.

Some dirty details

The logger

I’ve started with the mumblepins/syslog-ng-alpine for the proof of concept, but then I saw a warning about old log format, or something. On a closer inspection, I realized that it hasn’t been updated in 4 years. Hm… Looking at its Dockerfile on GitHub I found that it was manually compiling Syslog from the source. Umm… OK. On the next day, inspired by that I’ve created my own Dockerfile, which used Alpine’s built-in package of Syslog.

Extracted a copy of its config file from the running container, added the bits from mumblepins that made it listen on the network, and with that I had it both up to date and working. Open Source is awesome. 😎

The docker-compose config

To tell a container to stream its logs to a Syslog is well documented, and quite straightforward:

app:
  # ...more settings...
  depends_on: [logger]
  logging:
    driver: syslog
    options:
      syslog-address: tcp://127.0.0.1:514
      syslog-format: rfc3164
      tag: subscription

This worked, but having this for 6 services looked too non-DRY for my taste, so I googled around for a morning or two and found about YAML merge type which is described under the Extension fields section of Docker Compose references, and which gave me this:

x-logging: &logging
  depends_on: [logger]
  logging:
    driver: syslog
    options:
      syslog-address: tcp://127.0.0.1:514
      syslog-format: rfc3164
      tag: "{​{.Name}}"

I added the tag: option to have the symbolic container name in logs instead of its hex ID. And so this snippet now allows me to include it in every service’s config like this:

smtp-in:
  container_name: smtp-in
  # ...
  <<: *logging

This is neat. 🤓