feat: docker image for Bitcoin Core, configured by environment
Replaces lncm/bitcoind, which pins Core 26 — a version that still has legacy wallets, while the code that talks to it (bt's BitcoindClient) is written for the descriptor-only behaviour of 29 and later. The binaries are the official release build, verified by SHA-256 in the same layer that downloads them; a version bump that forgets the checksum fails the build instead of shipping something unverified. uid 1000 and /data/.bitcoin are kept from the image this replaces, so an existing named volume survives the switch without a recursive chown of a synced chain. Verified on testnet2: regtest node healthy in ~12 s, descriptor wallet, 101 blocks mined, sendtoaddress accepted — the last one being the check for BITCOIND_FALLBACKFEE, without which a fresh chain refuses to send.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# A container for the Bitcoin Core daemon
|
||||
#
|
||||
# The binaries are the official release build, taken from bitcoincore.org and
|
||||
# checked against a SHA-256 pinned in this file. Building Core from source in
|
||||
# CI would cost twenty minutes per image for a binary that upstream already
|
||||
# publishes reproducibly; verifying the checksum is what makes taking it safe.
|
||||
#
|
||||
# Bumping BITCOIN_VERSION means bumping both checksums below. Get them from
|
||||
# https://bitcoincore.org/bin/bitcoin-core-<version>/SHA256SUMS -- if you bump
|
||||
# the version alone the build fails at `sha256sum -c`, which is the intent.
|
||||
|
||||
FROM debian:trixie-slim AS fetch
|
||||
|
||||
ARG BITCOIN_VERSION=31.1
|
||||
ARG BITCOIN_SHA256_X86_64=b80d9c3e04da78fb6f0569685673418cf686fadba9042d926d13fb87ff503f9e
|
||||
ARG BITCOIN_SHA256_AARCH64=dcf1873f2208ba4f962f3398d47e154c39c0084be8f4553e05c940d0ace3d004
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -yq --no-install-suggests --no-install-recommends \
|
||||
ca-certificates wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# One RUN, because the checksum must be verified in the same layer that
|
||||
# downloads: a cached "download" layer paired with a later check would verify
|
||||
# an artefact nobody fetched in this build.
|
||||
RUN set -eu; \
|
||||
arch="$(uname -m)"; \
|
||||
case "$arch" in \
|
||||
x86_64) sha="$BITCOIN_SHA256_X86_64" ;; \
|
||||
aarch64) sha="$BITCOIN_SHA256_AARCH64" ;; \
|
||||
*) echo "unsupported architecture: $arch" >&2; exit 1 ;; \
|
||||
esac; \
|
||||
tarball="bitcoin-${BITCOIN_VERSION}-${arch}-linux-gnu.tar.gz"; \
|
||||
wget -q "https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/${tarball}"; \
|
||||
echo "${sha} ${tarball}" | sha256sum -c -; \
|
||||
tar -xzf "$tarball" -C /tmp; \
|
||||
mkdir -p /opt/bitcoin/bin; \
|
||||
cp "/tmp/bitcoin-${BITCOIN_VERSION}/bin/bitcoind" \
|
||||
"/tmp/bitcoin-${BITCOIN_VERSION}/bin/bitcoin-cli" /opt/bitcoin/bin/; \
|
||||
/opt/bitcoin/bin/bitcoind -version | head -n 1
|
||||
|
||||
|
||||
FROM debian:trixie-slim
|
||||
|
||||
# RPC. The daemon binds what BITCOIND_RPCBIND says (0.0.0.0 by default, because
|
||||
# a container's peers reach it by service name); what you publish decides who
|
||||
# outside can reach it, and the answer should stay "loopback only" -- the RPC
|
||||
# controls the wallet and has no TLS.
|
||||
EXPOSE 8332/tcp
|
||||
# P2P. Publish it to accept inbound peers; without it the node still connects
|
||||
# out. The port follows the chain: 8333 main, 18333 test, 18444 regtest.
|
||||
EXPOSE 8333/tcp
|
||||
|
||||
# uid/gid 1000 and /data/.bitcoin are not arbitrary: they are what the image
|
||||
# this one replaces (lncm/bitcoind) used, so an existing named volume keeps
|
||||
# working across the switch without a recursive chown of a synced chain.
|
||||
ENV USER_UID=1000
|
||||
ENV USER_GID=1000
|
||||
ENV HOME=/data
|
||||
ENV BITCOIN_DATA=/data/.bitcoin
|
||||
|
||||
COPY --from=fetch /opt/bitcoin/bin/ /usr/local/bin/
|
||||
COPY ./docker/run.sh /usr/local/bin/
|
||||
COPY ./docker/healthy_check.sh /usr/local/bin/
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -yq --no-install-suggests --no-install-recommends gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Both are ENTRYPOINT/HEALTHCHECK targets in exec form, so the bit has to be
|
||||
# set here: a clone on a filesystem that does not carry it would otherwise
|
||||
# build an image that cannot start.
|
||||
RUN chmod +x /usr/local/bin/run.sh /usr/local/bin/healthy_check.sh
|
||||
|
||||
# groupadd, not addgroup: the slim images dropped the adduser package.
|
||||
RUN groupadd --gid $USER_GID bitcoin \
|
||||
&& useradd --uid $USER_UID --gid $USER_GID --skel /dev/null --create-home --home-dir $HOME bitcoin
|
||||
|
||||
VOLUME ${BITCOIN_DATA}
|
||||
WORKDIR ${HOME}
|
||||
|
||||
# ENTRYPOINT, not CMD: everything after the image name is appended to bitcoind's
|
||||
# own arguments, so `docker run … -reindex` does what it looks like.
|
||||
ENTRYPOINT ["/usr/local/bin/run.sh"]
|
||||
|
||||
# The daemon answers RPC long before the chain is synced, so this reports "can I
|
||||
# be talked to", not "am I caught up". On a fresh mainnet datadir the first
|
||||
# answer still waits for the block index to load, hence the start period.
|
||||
HEALTHCHECK --interval=15s --timeout=10s --start-period=120s --retries=3 \
|
||||
CMD ["/usr/local/bin/healthy_check.sh"]
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Healthy = the RPC answers. Not "the chain is synced": an initial block
|
||||
# download takes days on mainnet, and a container that reports unhealthy for all
|
||||
# of it would be restarted by every orchestrator that watches health.
|
||||
#
|
||||
# The credentials come from cli.conf, written by run.sh, so they stay out of the
|
||||
# process list -- unlike the daemon's own arguments, which cannot avoid them.
|
||||
|
||||
set -eu
|
||||
|
||||
exec gosu bitcoin bitcoin-cli \
|
||||
-datadir="${BITCOIN_DATA:-/data/.bitcoin}" \
|
||||
-conf="${BITCOIN_DATA:-/data/.bitcoin}/cli.conf" \
|
||||
getblockchaininfo > /dev/null
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
export BITCOIND_CHAIN="${BITCOIND_CHAIN:-main}"
|
||||
export BITCOIND_USER="${BITCOIND_USER:-user}"
|
||||
export BITCOIND_PASSWORD="${BITCOIND_PASSWORD:-pass}"
|
||||
export BITCOIND_FALLBACKFEE="${BITCOIND_FALLBACKFEE:-0}"
|
||||
export BITCOIND_RPCBIND="${BITCOIND_RPCBIND:-0.0.0.0}"
|
||||
export BITCOIND_RPCALLOWIP="${BITCOIND_RPCALLOWIP:-0.0.0.0/0}"
|
||||
export BITCOIND_TXINDEX="${BITCOIND_TXINDEX:-0}"
|
||||
export BITCOIND_TXOSPENDERINDEX="${BITCOIND_TXOSPENDERINDEX:-0}"
|
||||
export BITCOIND_EXTRA_ARGS="${BITCOIND_EXTRA_ARGS:-}"
|
||||
|
||||
# The chain name decides the default RPC port, so an unknown value must stop the
|
||||
# container rather than fall through to a default: "-chain=testnet" (the name
|
||||
# Core does not use -- it wants "test") would otherwise be a mainnet node
|
||||
# holding a wallet the caller believes is worthless.
|
||||
case "$BITCOIND_CHAIN" in
|
||||
main) default_port=8332 ;;
|
||||
test) default_port=18332 ;;
|
||||
signet) default_port=38332 ;;
|
||||
regtest) default_port=18443 ;;
|
||||
*)
|
||||
echo "BITCOIND_CHAIN must be one of main, test, signet, regtest (got '$BITCOIND_CHAIN')" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
export BITCOIND_PORT="${BITCOIND_PORT:-$default_port}"
|
||||
case "$BITCOIND_PORT" in
|
||||
'' | *[!0-9]*)
|
||||
echo "BITCOIND_PORT must be a positive integer" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# A named volume starts out owned by root. Non-recursive on purpose: the only
|
||||
# case that needs fixing is the empty datadir, and a synced mainnet chain is
|
||||
# hundreds of gigabytes -- walking it on every start would add minutes to each
|
||||
# restart. A datadir moved here from an image with another uid must be chowned
|
||||
# by hand, once (see README, "Notes").
|
||||
mkdir -p "$BITCOIN_DATA"
|
||||
if [ "$(stat -c %u "$BITCOIN_DATA")" != "$USER_UID" ]
|
||||
then
|
||||
chown "$USER_UID:$USER_GID" "$BITCOIN_DATA"
|
||||
fi
|
||||
|
||||
# Credentials for bitcoin-cli, so the health check does not have to repeat them
|
||||
# on a command line. Written before the daemon starts and readable only by the
|
||||
# daemon's user. rpcport is deliberately at the top level: bitcoin-cli is
|
||||
# invoked without -chain and therefore reads the mainnet section, whatever chain
|
||||
# the daemon runs -- what matters is that the number matches.
|
||||
cli_conf="${BITCOIN_DATA}/cli.conf"
|
||||
umask 077
|
||||
cat > "$cli_conf" <<EOF
|
||||
rpcconnect=127.0.0.1
|
||||
rpcport=${BITCOIND_PORT}
|
||||
rpcuser=${BITCOIND_USER}
|
||||
rpcpassword=${BITCOIND_PASSWORD}
|
||||
EOF
|
||||
chown "$USER_UID:$USER_GID" "$cli_conf"
|
||||
umask 022
|
||||
|
||||
# Everything goes on the command line rather than into bitcoin.conf. Core treats
|
||||
# rpcport, port and bind as network-specific options: in a config file they only
|
||||
# apply inside the matching [main]/[test]/[regtest] section, while on the
|
||||
# command line they apply to the chain in force. One list that behaves the same
|
||||
# on every network is worth more here than a config file nobody edits.
|
||||
set -- \
|
||||
-chain="$BITCOIND_CHAIN" \
|
||||
-datadir="$BITCOIN_DATA" \
|
||||
-server=1 \
|
||||
-printtoconsole=1 \
|
||||
-rpcport="$BITCOIND_PORT" \
|
||||
-rpcuser="$BITCOIND_USER" \
|
||||
-rpcpassword="$BITCOIND_PASSWORD" \
|
||||
-rpcbind="$BITCOIND_RPCBIND" \
|
||||
-rpcallowip="$BITCOIND_RPCALLOWIP" \
|
||||
-txindex="$BITCOIND_TXINDEX" \
|
||||
-txospenderindex="$BITCOIND_TXOSPENDERINDEX" \
|
||||
-fallbackfee="$BITCOIND_FALLBACKFEE" \
|
||||
"$@"
|
||||
|
||||
# Unquoted on purpose: this is the one variable whose whole point is to be split
|
||||
# into separate arguments.
|
||||
# shellcheck disable=SC2086
|
||||
set -- "$@" $BITCOIND_EXTRA_ARGS
|
||||
|
||||
exec gosu bitcoin bitcoind "$@"
|
||||
Reference in New Issue
Block a user