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).