Skip to content
← All posts
6 min read Dawid Skłodowski

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.