Building Cardano Node 1.18.0 using Dockerfiles

2020-08-03 · Computing

In last week’s announcement that I’m running a highly-available Cardano stake pool, CRAB, I mentioned that I built my own container images for Cardano Node 1.18.0. Whilst this process is similar to my post about building Haskell 8.8.3 programs using Dockerfiles, there are some Cardano-specific differences. In this post, I explain the basics of how to compile and run both relay and block-producing nodes, such as might be used to build either a single-node or highly-available Ada cryptocurrency stake pool.

Dockerfile

Cardano Node 1.18.0 is based on Haskell 8.6.5. Similar to in my related post, I used a custom FROMFREEZE instruction to freeze the exact version of the base image being used, using a SHA-256 checksum. This then gets upgraded semi-automatically by my CI servers. As usual, I both build and run as an unprivileged user (x) where possible.

As usual, I use the builder pattern to separate a dependencies and development container image from a packaged image such as might be deployed. If your requirements are more simple, you might like to consider using multi-stage builds instead. Note that Haskell 8.6.5 is based on Debian 9 (not Debian 10, as for Haskell 8.8.3). Creating a packaged image is usually as simple as copying the compiled binaries, and ensuring the library dependencies are present. Cardano Node requires a custom version of Sodium; I’ve seen some confusion in Cardano channels about whether this is just a compile-time or run-time dependency; at least using defaults, it is a compile-time dependency, meaning those dependencies also need to be copied and in LD_LIBRARY_PATH.

# FROMFREEZE docker.io/library/haskell:8.6.5
FROM docker.io/library/haskell@sha256:64352f810b8cbbc79ec55150cc88105586a280c0f0ea0cedfa0ed2e9f1454de1

ARG USER=x
ARG HOME=/home/x

Next, I install various dependencies, both of Cardano Node, and for compilation. daemontools is used according to my usual pattern of dropping privileges where possible. Some of these dependencies are likely optional (tmux? really?), but I include them because they’re listed as dependencies in the Cardano Node documentation. I also create a user for compiling and running the programs.

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        autoconf \
        automake \
        build-essential \
        daemontools \
        g++ \
        git \
        jq \
        libffi-dev \
        libgmp-dev \
        libncursesw5 \
        libssl-dev \
        libsystemd-dev \
        libtinfo-dev \
        libtool \
        make \
        pkg-config \
        tmux \
        wget \
        zlib1g-dev && \
    rm -rf /var/lib/apt/lists/*

RUN useradd ${USER} -d ${HOME} && \
    mkdir -p ${HOME}/repo && \
    chown -R ${USER}:${USER} ${HOME}

I include the Cardano Node source as a Git submodule at lib/cardano-node (remote: git@github.com:input-output-hk/cardano-node.git). This enables me to checkout the 1.18.0 tag, and upgrade easily in the future. Similarly, Sodium is added at lib/libsodium (remote: git@github.com:input-output-hk/libsodium.git). Rather than lock dependencies separately using cabal.config or similar, I use the out-the-box Cardano Node setup, which makes it easier to update. Note also that Cardano Node uses Cabal v2 syntax, with multiple project files.

WORKDIR ${HOME}/repo

COPY --chown=x:x lib/ ./lib/

Next, switch user and compile Sodium.

USER ${USER}

WORKDIR ${HOME}/repo/lib/libsodium

RUN ./autogen.sh && \
    ./configure && \
    make

Switching to root, I install Sodium. This results in the libraries being at /usr/local/lib.

USER root

RUN make install

Switching back to the unprivileged user, I compile Cardano Node. LD_LIBRARY_PATH is set in the environment for Sodium. Normally, I’d only run, rather than install, but I’m not actively developing Cardano Node. -O2 is used to apply maximum compiler optimisations, at the expense of taking longer to build. I haven’t measured if this leads to a noticeable speed-up for Cardano Node or not, but it’s pretty standard practice when compiling Haskell programs for maximum efficiency. v2- prefix is added, to silence warnings about Cabal v1 vs v2. Both cardano-cli and cardano-node are included, in order to allow interacting with the blockchain directly using this image.

USER ${USER}

WORKDIR ${HOME}/repo/lib/cardano-node

ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH

RUN cabal v2-update && \
    cabal v2-install -O2 -j \
        cardano-cli \
        cardano-node

Finally, I run a Cardano Node script for booting the node. However, I don’t use this for packaged images that are deployed, instead using my custom scripts as commands as detailed below.

ENV PATH=${HOME}/.cabal/bin:$PATH

CMD scripts/mainnet.sh --verbose

If building a packaged image, you’ll probably want to do something like the following instead. I set the port (which can be anything you like, even if joining Cardano Mainnet), the data directory (/var/lib/cardano, but you can change as desired), and a socket path (optional, but required if you want to use cardano-cli, which I recommend).

USER ${USER}

WORKDIR ${HOME}

ENV PORT=3001 \
    LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH \
    PATH=${HOME}/bin:$PATH \
    CRD_DATA=/var/lib/cardano

ENV CARDANO_NODE_SOCKET_PATH=${CRD_DATA}/socket/cardano-node.socket

CMD cardano-node-relay

Script: cardano-node-relay

For a stake pool relay node, I use a custom cardano-node-relay script. ADDRESS is made an optional parameter, to allow for binding to all interfaces (default), or restricting as desired. I did originally set this to localhost, but that didn’t play nicely with running a dual-stack IPv4 and IPv6 node, so I made it optional instead. Note that for block-producing nodes, you’ll definitely want to lock them down somehow, potentially by using private interfaces, but certainly by having adequate firewalls to protect them from the outside world. Relay nodes, however, need to be publicly accessible. CRD_DATA is used to point to the data directory. I’ve taken the PostgreSQL approach of putting both the database and configuration files within that path, since that makes high-availability failover much easier.

#!/bin/bash -eu

ADDRESS=${ADDRESS:-}
#-------------------------------------------------------------------------------
if [ -z "$ADDRESS" ]; then
    host_addr_=()
else
    host_addr_=(--host-addr "$ADDRESS")
fi

cardano-node run \
    --config "$CRD_DATA/etc/config.json" \
    --database-path "$CRD_DATA/db" \
    "${host_addr_[@]}" \
    --port "$PORT" \
    --socket-path "$CRD_DATA/socket/cardano-node.socket" \
    --topology "$CRD_DATA/etc/topology.json"

Script: cardano-node-block

For a block-producing node, you’ll also need to point to the various keys needed for operating a Cardano stake pool. Since this post is specifically about building using Dockerfiles, I won’t go into detail here. Using my cardano-node-block script, however, I’m able to simply set CMD to it in my container orchestrator, and everything is pointed to consistent locations.

#!/bin/bash -eu

ADDRESS=${ADDRESS:-}
#-------------------------------------------------------------------------------
if [ -z "$ADDRESS" ]; then
    host_addr_=()
else
    host_addr_=(--host-addr "$ADDRESS")
fi

cardano-node run \
    --config "$CRD_DATA/etc/config.json" \
    --database-path "$CRD_DATA/db" \
    "${host_addr_[@]}" \
    --port "$PORT" \
    --socket-path "$CRD_DATA/socket/cardano-node.socket" \
    --topology "$CRD_DATA/etc/topology.json" \
    --shelley-kes-key "$CRD_DATA/keys/kes.skey" \
    --shelley-vrf-key "$CRD_DATA/keys/vrf.skey" \
    --shelley-operational-certificate "$CRD_DATA/keys/node.cert"

CRAB stake pool

Whilst this post will help you to build Cardano Node using Dockerfiles, this is in fact only part of the journey. I myself took days researching and experimenting with Cardano testnets prior to the Shelley Mainnet launch, and registering my stake pool. If you’re only interested in finding somewhere to delegate your Ada cryptocurrency to, in order to participate in this innovative technology and have the chance of gaining rewards, you might like to consider delegating to my CRAB stake pool instead: the pool margin is just 1%, the pool cost is 340 ADA (the minimum allowed), and I’ve pledged 10K ADA of my own money to the pool (~ 1196 EUR).