DEV Community

Salifu Yakubu
Salifu Yakubu

Posted on

Running a Bitcoin Core Node with Docker — Compile from Source vs Pre-built Binaries

This guide walks you through two approaches to setting up a local Bitcoin Core v28.0 node using Docker, targeting both regtest (a fully local, isolated chain you control) and testnet (a public test network with real peer connections). By the end you will have a running node, understand what every line of configuration does, and be ready to interact with it over RPC.

Prerequisites

  • Docker Desktop (or Docker Engine + Compose plugin) installed and running
  • git installed
  • Basic comfort with the terminal
  • ~4 GB free disk space (more for testnet sync)

Understanding the Two Approaches

Before writing a single line of code, you need to make a decision that shapes everything else: do you compile Bitcoin Core from source, or do you download official pre-built binaries?

Compile from Source

You download the raw C++ source code from the Bitcoin Core repository, tell the compiler exactly what features to include, and produce binaries yourself. The ./configure step is where you make decisions:

./configure \
    --with-sqlite=yes \   # Use SQLite for wallet storage (the v28 default)
    --without-bdb \       # Drop BerkeleyDB — the old wallet backend, now legacy
    --disable-tests \     # Skip test suite binaries — not needed at runtime
    --disable-bench \     # Skip benchmarking binaries
    --without-gui \       # No Qt desktop GUI — this is a headless server node
    --disable-man         # Skip generating man pages
Enter fullscreen mode Exit fullscreen mode

Each flag is a question: what is this node for, and what should it include? That process of asking and answering is most of the educational value.

Trade-offs:

Compile from Source
Build time 20–40 minutes
Final image size ~250 MB
Reproducibility Sensitive to compiler and library versions
Educational value High — exposes dependencies, wallet architecture, build flags
Best for Understanding how Bitcoin Core is structured

Pre-built Binaries

The Bitcoin Core project publishes official binaries for every release. You download the tarball, verify its checksum, and copy the binaries into your image. No compiler required.

Trade-offs:

Pre-built Binaries
Build time ~1 minute
Final image size ~80–100 MB
Reproducibility Identical binary every time
Educational value Lower — the build process is hidden
Best for Fast iteration when the focus is RPC and wallet exercises

Which should you choose?
If the exercise is about how Bitcoin Core is built — its dependencies, wallet backends, compile-time options — use source. If the exercise is about what the node does (mining blocks in regtest, reading the mempool, broadcasting transactions), pre-built is less friction. This guide covers both in full.


Project Structure

Create a working directory:

mkdir bitcoin-node && cd bitcoin-node
Enter fullscreen mode Exit fullscreen mode
bitcoin-node/
├── Dockerfile              # Choose one of the two versions below
├── docker-compose.yml
└── entrypoint.sh
Enter fullscreen mode Exit fullscreen mode

Option A: Compile from Source

The Dockerfile

# ============================================================
# Stage 1 — builder
# Compiles Bitcoin Core v28.0 from source on Debian Bookworm.
# BerkeleyDB is dropped — Bitcoin Core v28 uses SQLite by default.
# Nothing from this stage leaks into the final image.
# ============================================================
FROM debian:bookworm AS builder

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates \
        git \
        build-essential \
        libtool \
        autotools-dev \
        automake \
        pkg-config \
        python3 \
        libssl-dev \
        libevent-dev \
        libboost-system-dev \
        libboost-filesystem-dev \
        libboost-thread-dev \
        libboost-chrono-dev \
        libsqlite3-dev && \
    rm -rf /var/lib/apt/lists/*

# Clone Bitcoin Core v28.0 at the exact tagged commit
RUN git clone --depth 1 --branch v28.0 \
        https://github.com/bitcoin/bitcoin.git /bitcoin

# Compile Bitcoin Core with SQLite wallet, no GUI, no tests, no bench
RUN cd /bitcoin && \
    ./autogen.sh && \
    ./configure \
        --with-sqlite=yes \
        --without-bdb \
        --disable-tests \
        --disable-bench \
        --without-gui \
        --disable-man && \
    make -j$(nproc) && \
    make install

# ============================================================
# Stage 2 — runtime
# Copies only the compiled binaries. No build tools, no source.
# ============================================================
FROM debian:bookworm

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates \
        libssl3 \
        libevent-2.1-7 \
        libevent-pthreads-2.1-7 \
        libboost-system1.74.0 \
        libboost-filesystem1.74.0 \
        libboost-thread1.74.0 \
        libboost-chrono1.74.0 \
        libsqlite3-0 && \
    rm -rf /var/lib/apt/lists/*

COPY --from=builder /usr/local/bin/bitcoind    /usr/local/bin/bitcoind
COPY --from=builder /usr/local/bin/bitcoin-cli /usr/local/bin/bitcoin-cli

VOLUME ["/root/.bitcoin"]
EXPOSE 8333

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Enter fullscreen mode Exit fullscreen mode

Why debian:bookworm instead of debian:13?
Debian 13 ("trixie") is still in testing. Library package names and versions shift without notice, which makes the runtime layer fragile. bookworm (Debian 12) is the current stable release — the library names above are reliable.

What to pay attention to

./autogen.sh generates the configure script from configure.ac. Bitcoin Core does not ship a pre-generated configure script in its repository, so this step is always required when building from source.

make -j$(nproc) parallelises the build across all available CPU cores. On a 4-core machine this is make -j4. Removing -j$(nproc) makes the build sequential — useful for debugging build failures but much slower.

The two-stage build is deliberate. The builder stage installs compilers, headers, and source — easily 600 MB of tooling. The runtime stage copies only the two binaries and their shared library dependencies. This is why the final image is ~250 MB rather than ~800 MB.


Option B: Pre-built Binaries

The Dockerfile

# ============================================================
# Stage 1 — verifier
# Downloads Bitcoin Core v28.0 official binaries and verifies them.
# ============================================================
FROM debian:bookworm AS builder

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates \
        wget \
        gpg \
        gpg-agent && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /build

# Download binaries, checksums, and the detached signature
RUN wget -q https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz && \
    wget -q https://bitcoincore.org/bin/bitcoin-core-28.0/SHA256SUMS && \
    wget -q https://bitcoincore.org/bin/bitcoin-core-28.0/SHA256SUMS.asc

# Import release signing keys from the Bitcoin Core guix.sigs repository
RUN gpg --keyserver hkps://keys.openpgp.org \
        --recv-keys \
          152812300785C96444D3334D17565732E08E5E41 \
          0CCBAAFD76A2ECE2CCD3141DE2FFD5B1D88CA97D

# Verify the GPG signature on the checksums file
RUN gpg --verify SHA256SUMS.asc SHA256SUMS

# Verify the tarball matches the signed checksum
RUN grep "bitcoin-28.0-x86_64-linux-gnu.tar.gz" SHA256SUMS | sha256sum -c -

RUN tar -xzf bitcoin-28.0-x86_64-linux-gnu.tar.gz

# ============================================================
# Stage 2 — runtime
# ============================================================
FROM debian:bookworm

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        ca-certificates && \
    rm -rf /var/lib/apt/lists/*

COPY --from=builder /build/bitcoin-28.0/bin/bitcoind    /usr/local/bin/bitcoind
COPY --from=builder /build/bitcoin-28.0/bin/bitcoin-cli /usr/local/bin/bitcoin-cli

VOLUME ["/root/.bitcoin"]
EXPOSE 8333

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Enter fullscreen mode Exit fullscreen mode

Why the GPG step matters

The SHA256SUMS file contains a hash of the tarball. A checksum alone tells you the download arrived intact — it does not tell you whether bitcoincore.org itself was compromised, or whether the file was replaced in transit.

SHA256SUMS.asc is a GPG detached signature over SHA256SUMS, produced by a Bitcoin Core maintainer using their private key. When you run gpg --verify SHA256SUMS.asc SHA256SUMS, you are confirming that a known, trusted key holder signed off on those checksums. This is the same trust model that Bitcoin Core users are expected to follow when installing on any machine — skipping it in a learning environment teaches a bad habit.

Exercise: Run gpg --verify SHA256SUMS.asc SHA256SUMS and note the output. Then intentionally corrupt one byte in SHA256SUMS and run it again. What does GPG report?


The Entrypoint Script

This file is identical for both approaches. Save it as entrypoint.sh:

#!/usr/bin/env bash
set -e

MODE="${1:-regtest}"

case "$MODE" in
  regtest)
    echo "=== Starting Bitcoin Core v28.0 (regtest) ==="
    bitcoind -regtest -daemon
    LOG=/root/.bitcoin/regtest/debug.log
    ;;
  testnet)
    echo "=== Starting Bitcoin Core v28.0 (testnet) ==="
    bitcoind -testnet -daemon
    LOG=/root/.bitcoin/testnet3/debug.log
    ;;
  *)
    echo "ERROR: Unknown mode '$MODE'. Use 'regtest' or 'testnet'."
    exit 1
    ;;
esac

echo "=== Waiting for debug.log ==="
until [ -f "$LOG" ]; do
  sleep 1
done

echo "=== Node running in $MODE mode. Tailing debug log (Ctrl+C to detach) ==="
tail -f "$LOG"
Enter fullscreen mode Exit fullscreen mode

Make it executable:

chmod +x entrypoint.sh
Enter fullscreen mode Exit fullscreen mode

What this script does

set -e causes the script to exit immediately if any command fails. Without this, a failed bitcoind launch would silently fall through to tail -f on a log file that does not exist yet.

-daemon starts bitcoind as a background process. The script then polls for the debug log file — this is how it knows bitcoind has actually initialised rather than just been spawned.

The two log paths reflect Bitcoin Core's directory layout:

Mode Data directory
regtest /root/.bitcoin/regtest/
testnet /root/.bitcoin/testnet3/
mainnet /root/.bitcoin/

Docker Compose

Save as docker-compose.yml:

services:
  bitcoin-node:
    build:
      context: .
      dockerfile: Dockerfile
    image: bitcoin-core:v28.0
    container_name: bitcoin-regtest

    ports:
      - "18444:18444"   # P2P  (regtest)
      - "18443:18443"   # RPC  (regtest)

    volumes:
      - ./bitcoin-data:/root/.bitcoin

    entrypoint: ["/entrypoint.sh", "regtest"]

    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Port reference

Port Protocol Purpose
8333 P2P Mainnet peer connections
18333 P2P Testnet peer connections
18444 P2P Regtest peer connections
8332 RPC Mainnet RPC server
18332 RPC Testnet RPC server
18443 RPC Regtest RPC server

Switching to testnet

Change the entrypoint line and update the port mappings in docker-compose.yml:

    ports:
      - "18333:18333"   # P2P  (testnet)
      - "18332:18332"   # RPC  (testnet)

    entrypoint: ["/entrypoint.sh", "testnet"]
Enter fullscreen mode Exit fullscreen mode

Note on testnet: A testnet node needs to sync with the public testnet chain. For most CLI exercises in a development environment, regtest is preferred — you control the chain entirely, block generation is instant, and there is no sync wait.


Building and Running

Build the image

docker compose build
Enter fullscreen mode Exit fullscreen mode

For the source build this will take 20–40 minutes the first time. Docker caches layers, so subsequent builds are fast unless you change the Dockerfile.

Start the node

docker compose up
Enter fullscreen mode Exit fullscreen mode

A healthy regtest start looks like:

=== Starting Bitcoin Core v28.0 (regtest) ===
=== Waiting for debug.log ===
=== Node running in regtest mode. Tailing debug log (Ctrl+C to detach) ===
2024-01-15T10:23:01Z Bitcoin Core version v28.0.0
...
2024-01-15T10:23:02Z init message: Done loading
Enter fullscreen mode Exit fullscreen mode

Run in detached mode

docker compose up -d
docker compose logs -f
Enter fullscreen mode Exit fullscreen mode

Interacting with the Node via RPC

Inside the container

docker exec -it bitcoin-regtest bitcoin-cli -regtest getblockchaininfo
Enter fullscreen mode Exit fullscreen mode

From the host (requires RPC auth)

Create bitcoin-data/bitcoin.conf:

rpcuser=devuser
rpcpassword=devpass123
rpcallowip=0.0.0.0/0
rpcbind=0.0.0.0
Enter fullscreen mode Exit fullscreen mode

Then from the host:

bitcoin-cli -regtest \
    -rpcconnect=127.0.0.1 \
    -rpcport=18443 \
    -rpcuser=devuser \
    -rpcpassword=devpass123 \
    getblockchaininfo
Enter fullscreen mode Exit fullscreen mode

Essential CLI Exercises (Regtest)

Regtest gives you a blank chain — nothing exists until you create it.

Check the chain state

docker exec -it bitcoin-regtest bitcoin-cli -regtest getblockchaininfo
Enter fullscreen mode Exit fullscreen mode

Create a wallet

docker exec -it bitcoin-regtest bitcoin-cli -regtest createwallet "my-wallet"
Enter fullscreen mode Exit fullscreen mode

Mine blocks

Mining in regtest is instant. Generate 101 blocks to make the coinbase of the first block spendable (coinbase outputs require 100 confirmations):

docker exec -it bitcoin-regtest bitcoin-cli -regtest \
    generatetoaddress 101 $(docker exec bitcoin-regtest bitcoin-cli -regtest getnewaddress)
Enter fullscreen mode Exit fullscreen mode

Check your balance

docker exec -it bitcoin-regtest bitcoin-cli -regtest getbalance
Enter fullscreen mode Exit fullscreen mode

You should see 50.00000000 BTC.

Send a transaction

ADDR=$(docker exec bitcoin-regtest bitcoin-cli -regtest getnewaddress)
docker exec -it bitcoin-regtest bitcoin-cli -regtest sendtoaddress "$ADDR" 1.5
Enter fullscreen mode Exit fullscreen mode

Inspect the mempool

docker exec -it bitcoin-regtest bitcoin-cli -regtest getrawmempool
Enter fullscreen mode Exit fullscreen mode

Mine one block to confirm:

docker exec -it bitcoin-regtest bitcoin-cli -regtest \
    generatetoaddress 1 $(docker exec bitcoin-regtest bitcoin-cli -regtest getnewaddress)
Enter fullscreen mode Exit fullscreen mode

Decode a raw transaction

docker exec -it bitcoin-regtest bitcoin-cli -regtest \
    getrawtransaction <txid> true
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

Container exits immediately

docker compose logs bitcoin-node
Enter fullscreen mode Exit fullscreen mode

The most common cause is a malformed bitcoin.conf or a port already in use.

bitcoin-cli returns "Connection refused"
bitcoind may still be initialising. Follow the logs and wait for "Done loading".

Testnet sync is very slow
Add prune=550 to bitcoin.conf to limit disk usage to ~550 MB.

"Wallet file not found" on RPC calls
Create a wallet explicitly — v28 no longer creates a default wallet on startup:

docker exec -it bitcoin-regtest bitcoin-cli -regtest createwallet "default"
Enter fullscreen mode Exit fullscreen mode

Rebuilding after changing the Dockerfile

docker compose build --no-cache
docker compose up
Enter fullscreen mode Exit fullscreen mode

Summary

Source Build Pre-built
Dockerfile complexity High Low
Build time 20–40 min ~1 min
GPG verification git verify-tag v28.0 gpg --verify SHA256SUMS.asc
Best for Understanding Bitcoin Core internals RPC / wallet exercises
Regtest ready
Testnet ready

Both setups use the same entrypoint.sh and docker-compose.yml. The only difference is what arrives at /usr/local/bin/bitcoind, whether you compiled it or downloaded it, it is the same software.

The next step is working with wallets, descriptor addresses, and raw transactions. All of those exercises work against either setup with no changes to the Compose file.


Written by Samuel Kodie (Discord: skodie0) and Salifu Yakubu (Discord: salifu_88984)

Top comments (0)