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
-
gitinstalled - 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
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
bitcoin-node/
├── Dockerfile # Choose one of the two versions below
├── docker-compose.yml
└── entrypoint.sh
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"]
Why
debian:bookworminstead ofdebian: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"]
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 SHA256SUMSand note the output. Then intentionally corrupt one byte inSHA256SUMSand 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"
Make it executable:
chmod +x entrypoint.sh
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
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"]
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
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
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
Run in detached mode
docker compose up -d
docker compose logs -f
Interacting with the Node via RPC
Inside the container
docker exec -it bitcoin-regtest bitcoin-cli -regtest getblockchaininfo
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
Then from the host:
bitcoin-cli -regtest \
-rpcconnect=127.0.0.1 \
-rpcport=18443 \
-rpcuser=devuser \
-rpcpassword=devpass123 \
getblockchaininfo
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
Create a wallet
docker exec -it bitcoin-regtest bitcoin-cli -regtest createwallet "my-wallet"
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)
Check your balance
docker exec -it bitcoin-regtest bitcoin-cli -regtest getbalance
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
Inspect the mempool
docker exec -it bitcoin-regtest bitcoin-cli -regtest getrawmempool
Mine one block to confirm:
docker exec -it bitcoin-regtest bitcoin-cli -regtest \
generatetoaddress 1 $(docker exec bitcoin-regtest bitcoin-cli -regtest getnewaddress)
Decode a raw transaction
docker exec -it bitcoin-regtest bitcoin-cli -regtest \
getrawtransaction <txid> true
Troubleshooting
Container exits immediately
docker compose logs bitcoin-node
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"
Rebuilding after changing the Dockerfile
docker compose build --no-cache
docker compose up
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)