Running a Bitcoin node in Docker
A war story about packaging bitcoind in a container: the initial block download that humbles you, why the blockchain must live in a volume, locking down the RPC interface, and what a full node teaches you about long-running stateful services.
We wanted a Bitcoin full node — our own, not a third-party API — to query the
blockchain directly and to understand the network from the inside rather than
through someone else’s abstraction. Docker had just hit 1.0, we were containerising
everything else, so the plan was obvious: package bitcoind in an image and run it
like any other service. The plan was sound. The execution taught us a great deal
about what containers are good at, what they are not, and how long-running stateful
software really behaves. Here is the war story.
Why a full node, and why bitcoind
A full node downloads and validates the entire blockchain and enforces every
consensus rule itself. The alternative — trusting a hosted API — is convenient and
exactly the dependency we wanted to remove: a node that verifies everything locally
owes its answers to no one. bitcoind (Bitcoin Core’s daemon) is the reference
implementation, it exposes a JSON-RPC interface we could call from Ruby, and it is
a single long-running process — which on paper is the ideal Docker citizen.
A first Dockerfile, and a false sense of progress
The image itself is unremarkable: a base OS, the bitcoind binary, a config file, expose the ports, run the daemon.
FROM ubuntu:14.04
RUN apt-get update && apt-get install -y bitcoind && rm -rf /var/lib/apt/lists/*
COPY bitcoin.conf /root/.bitcoin/bitcoin.conf
EXPOSE 8333 8332
CMD ["bitcoind", "-printtoconsole"]
docker build -t btc-node .
docker run -d --name btc btc-node
It built, it started, the logs scrolled with peer connections and incoming blocks. It looked finished. It was not even close — the image was the easy 10%, and every hard lesson was still ahead.
The initial block download humbles you
The first reality check is the initial block download (IBD): a brand-new node must fetch and validate the entire chain from genesis before it is useful. In 2014 that is roughly twenty gigabytes that arrives slowly, because your node is not just downloading — it is verifying every block and every signature, which is CPU- and disk-bound work. On a modest cloud instance this takes not minutes but the better part of a day, sometimes more.
Two failures fell out of this immediately. First, the sync is I/O-heavy enough to starve everything else on a small box — the node will happily peg the disk. Second, and more painful: we killed and restarted the container a few times during setup (as you do with containers), and because of how we had it configured, the partial chain did not survive. Re-downloading twenty gigabytes because you restarted a container is the kind of mistake you make exactly once. It pointed straight at the real lesson.
State must not live in the container
This is the heart of it. A container’s writable layer is ephemeral by design — remove the container and it is gone. That is a feature for stateless app servers and a catastrophe for a node whose entire value is a twenty-gigabyte dataset it spent a day building. The blockchain must live in a volume, on the host, outside the container’s lifecycle:
docker run -d --name btc \
-v /data/bitcoin:/root/.bitcoin \
-p 8333:8333 \
btc-node
Now the chain data lives in /data/bitcoin on the host. The container becomes
genuinely disposable: kill it, rebuild the image, upgrade bitcoind, start a fresh
container pointed at the same volume, and it picks up exactly where it left off. The
discipline that crystallised here is one we now apply everywhere — the container
is the process; the volume is the state, and they have completely different
lifecycles. Conflating them is the single most common way people get burned
running stateful software in Docker. The data also wants to be on a fast,
appropriately-sized disk, because the node will keep growing forever; “20 GB” is a
floor, not a ceiling.
Locking down the RPC interface
The reason we built the node was its JSON-RPC interface, and it is also the thing
most likely to get you robbed if you are careless. bitcoind will happily accept
RPC commands — including, on a wallet-enabled node, ones that move funds — so the
interface must never be exposed to the open internet.
# bitcoin.conf
server=1
rpcuser=a_real_username
rpcpassword=a_long_random_password
rpcallowip=127.0.0.1 # or a specific internal subnet — never 0.0.0.0
The container-specific subtlety bites here: inside the container, rpcallowip and
binding interact with Docker’s network namespace, so it is easy to think you have
bound RPC to localhost while actually exposing it to anything that can reach the
container’s network. The rule we settled on: publish the P2P port (8333) so the
node can talk to peers, but never publish the RPC port (8332) to the host or the
world. Reach RPC only from within the Docker network — from your application
container on the same private network — so the money-moving interface is never
routable from outside. Treat the RPC credentials as the secrets they are.
What a full node teaches you about stateful services
Strip away the Bitcoin specifics and this project was a concentrated lesson in running any long-lived stateful service in a container:
- Separate the process from its data, ruthlessly. Volumes are not an afterthought; for stateful software they are the design. Decide what is ephemeral and what is precious before you run anything.
- Account for the cold-start cost. A service that needs a day to become useful changes how you do everything — deploys, failover, scaling. You do not casually restart a node mid-sync, and any automation has to know that.
- The container does not change your security posture. It is easy to feel that “it’s in a container” is a form of isolation; for a network-exposed, money-handling daemon it is not. The RPC interface needed exactly the same care it would on a bare server.
- Disposable container, durable data is the goal. The win, once we got there, was real: upgrading bitcoind became “build new image, swap the container, keep the volume” — a thirty-second operation with no resync, on infrastructure that reviews and reproduces like code.
The node has been running happily since, surviving restarts, image rebuilds, and a bitcoind upgrade without re-downloading a single block. Getting there cost us one accidental twenty-gigabyte re-sync and a hard look at where our state actually lived — a cheap price for a lesson we have applied to every database and stateful service we have containerised since. Docker did not make running a Bitcoin node trivial. It made it reproducible, which on a long-lived piece of infrastructure turns out to be the thing that matters.