diff --git a/.gitignore b/.gitignore index 5a9bc1631..32c7474d9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ _* # Logs .log +# Environment variables/configs +.env + # Profiling profile.json.gz flamegraph.svg diff --git a/Cargo.lock b/Cargo.lock index c4f460519..ca61ac343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,6 +1046,7 @@ dependencies = [ "log", "minreq", "serde", + "serde_json", "tokio", "tower-http", "tracing", diff --git a/Cargo.toml b/Cargo.toml index a36cb50ee..4013b4726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ package.edition = "2024" package.version = "0.0.80" package.homepage = "https://bitcoinresearchkit.org" package.repository = "https://github.com/bitcoinresearchkit/brk" +package.readme = "README.md" [profile.release] lto = "fat" diff --git a/crates/brk_cli/src/config.rs b/crates/brk_cli/src/config.rs index a0851c456..d6190d2e9 100644 --- a/crates/brk_cli/src/config.rs +++ b/crates/brk_cli/src/config.rs @@ -13,7 +13,6 @@ use clap_derive::Parser; use color_eyre::eyre::eyre; use serde::{Deserialize, Serialize}; -use crate::services::Services; #[derive(Parser, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[command(version, about)] @@ -33,10 +32,6 @@ pub struct Config { #[arg(long, value_name = "PATH")] brkdir: Option, - /// Activated services, default: all, saved - #[serde(default, deserialize_with = "default_on_error")] - #[arg(short, long)] - services: Option, /// Computation of computed datasets, `lazy` computes data whenever requested without saving it, `eager` computes the data once and saves it to disk, default: `lazy`, saved #[serde(default, deserialize_with = "default_on_error")] @@ -129,9 +124,6 @@ impl Config { config_saved.brkdir = Some(brkdir); } - if let Some(services) = config_args.services.take() { - config_saved.services = Some(services); - } if let Some(computation) = config_args.computation.take() { config_saved.computation = Some(computation); @@ -306,15 +298,6 @@ impl Config { self.outputsdir().join("hars") } - pub fn process(&self) -> bool { - self.services - .is_none_or(|m| m == Services::All || m == Services::Processor) - } - - pub fn serve(&self) -> bool { - self.services - .is_none_or(|m| m == Services::All || m == Services::Server) - } fn path_cookiefile(&self) -> PathBuf { self.rpccookiefile.as_ref().map_or_else( diff --git a/crates/brk_cli/src/lib.rs b/crates/brk_cli/src/lib.rs index ed14a5406..2b5ce02ea 100644 --- a/crates/brk_cli/src/lib.rs +++ b/crates/brk_cli/src/lib.rs @@ -4,7 +4,6 @@ use brk_core::{dot_brk_log_path, dot_brk_path}; mod config; mod run; -mod services; use run::*; diff --git a/crates/brk_cli/src/run.rs b/crates/brk_cli/src/run.rs index 398153356..bdd90f512 100644 --- a/crates/brk_cli/src/run.rs +++ b/crates/brk_cli/src/run.rs @@ -13,18 +13,20 @@ pub fn run() -> color_eyre::Result<()> { let config = Config::import()?; let rpc = config.rpc()?; - let exit = Exit::new(); - - let parser = brk_parser::Parser::new(config.blocksdir(), rpc); + let parser = brk_parser::Parser::new( + config.blocksdir(), + config.brkdir(), + rpc, + ); let format = config.format(); let mut indexer = Indexer::forced_import(&config.outputsdir())?; - let wait_for_synced_node = || -> color_eyre::Result<()> { + let wait_for_synced_node = |rpc_client: &bitcoincore_rpc::Client| -> color_eyre::Result<()> { let is_synced = || -> color_eyre::Result { - let info = rpc.get_blockchain_info()?; + let info = rpc_client.get_blockchain_info()?; Ok(info.headers == info.blocks) }; @@ -50,54 +52,40 @@ pub fn run() -> color_eyre::Result<()> { .enable_all() .build()? .block_on(async { - let server = if config.serve() { - let served_indexer = indexer.clone(); - let served_computer = computer.clone(); + let served_indexer = indexer.clone(); + let served_computer = computer.clone(); - let server = Server::new(served_indexer, served_computer, config.website())?; + let server = Server::new(served_indexer, served_computer, config.website())?; - let watch = config.watch(); - let mcp = config.mcp(); - let opt = Some(tokio::spawn(async move { - server.serve(watch, mcp).await.unwrap(); - })); + let watch = config.watch(); + let mcp = config.mcp(); + let server_handle = tokio::spawn(async move { + server.serve(watch, mcp).await.unwrap(); + }); - sleep(Duration::from_secs(1)); + sleep(Duration::from_secs(1)); - opt - } else { - None - }; + loop { + wait_for_synced_node(rpc)?; - if config.process() { - loop { - wait_for_synced_node()?; + let block_count = rpc.get_block_count()?; - let block_count = rpc.get_block_count()?; + info!("{} blocks found.", block_count + 1); - info!("{} blocks found.", block_count + 1); + let starting_indexes = + indexer.index(&parser, rpc, &exit, config.check_collisions())?; - let starting_indexes = - indexer.index(&parser, rpc, &exit, config.check_collisions())?; + computer.compute(&mut indexer, starting_indexes, &exit)?; - computer.compute(&mut indexer, starting_indexes, &exit)?; + if let Some(delay) = config.delay() { + sleep(Duration::from_secs(delay)) + } - if let Some(delay) = config.delay() { - sleep(Duration::from_secs(delay)) - } + info!("Waiting for new blocks..."); - info!("Waiting for new blocks..."); - - while block_count == rpc.get_block_count()? { - sleep(Duration::from_secs(1)) - } + while block_count == rpc.get_block_count()? { + sleep(Duration::from_secs(1)) } } - - if let Some(handle) = server { - handle.await.unwrap(); - } - - Ok(()) }) } diff --git a/crates/brk_cli/src/services.rs b/crates/brk_cli/src/services.rs deleted file mode 100644 index d06597a4b..000000000 --- a/crates/brk_cli/src/services.rs +++ /dev/null @@ -1,23 +0,0 @@ -use clap_derive::{Parser, ValueEnum}; -use serde::{Deserialize, Serialize}; - -#[derive( - Default, - Debug, - Clone, - Copy, - Parser, - ValueEnum, - Serialize, - Deserialize, - PartialEq, - Eq, - PartialOrd, - Ord, -)] -pub enum Services { - #[default] - All, - Processor, - Server, -} diff --git a/crates/brk_computer/examples/main.rs b/crates/brk_computer/examples/main.rs index 6b921befa..ee69e3f57 100644 --- a/crates/brk_computer/examples/main.rs +++ b/crates/brk_computer/examples/main.rs @@ -26,7 +26,7 @@ pub fn main() -> color_eyre::Result<()> { thread::Builder::new() .stack_size(256 * 1024 * 1024) .spawn(move || -> color_eyre::Result<()> { - let parser = Parser::new(bitcoin_dir.join("blocks"), rpc); + let parser = Parser::new(bitcoin_dir.join("blocks"), default_brk_path(), rpc); let _outputs_dir = Path::new("/Volumes/WD_BLACK/brk").join("outputs"); let outputs_dir = _outputs_dir.as_path(); diff --git a/crates/brk_indexer/examples/main.rs b/crates/brk_indexer/examples/main.rs index 5fa9c2a40..f4b18234f 100644 --- a/crates/brk_indexer/examples/main.rs +++ b/crates/brk_indexer/examples/main.rs @@ -1,6 +1,6 @@ use std::{path::Path, time::Instant}; -use brk_core::default_bitcoin_path; +use brk_core::{default_bitcoin_path, default_brk_path}; use brk_exit::Exit; use brk_indexer::Indexer; use brk_parser::Parser; @@ -13,6 +13,7 @@ fn main() -> color_eyre::Result<()> { brk_logger::init(Some(Path::new(".log"))); let bitcoin_dir = default_bitcoin_path(); + let brk_dir = default_brk_path(); let rpc = Box::leak(Box::new(bitcoincore_rpc::Client::new( "http://localhost:8332", @@ -20,7 +21,7 @@ fn main() -> color_eyre::Result<()> { )?)); let exit = Exit::new(); - let parser = Parser::new(bitcoin_dir.join("blocks"), rpc); + let parser = Parser::new(bitcoin_dir.join("blocks"), brk_dir, rpc); let outputs = Path::new("../../_outputs"); diff --git a/crates/brk_parser/examples/main.rs b/crates/brk_parser/examples/main.rs index 0a56f9a2d..019807203 100644 --- a/crates/brk_parser/examples/main.rs +++ b/crates/brk_parser/examples/main.rs @@ -1,11 +1,12 @@ use bitcoincore_rpc::{Auth, Client}; -use brk_core::{Height, default_bitcoin_path}; +use brk_core::{Height, default_bitcoin_path, default_brk_path}; use brk_parser::Parser; fn main() { let i = std::time::Instant::now(); let bitcoin_dir = default_bitcoin_path(); + let brk_dir = default_brk_path(); let rpc = Box::leak(Box::new( Client::new( @@ -18,7 +19,7 @@ fn main() { let start = None; let end = None; - let parser = Parser::new(bitcoin_dir.join("blocks"), rpc); + let parser = Parser::new(bitcoin_dir.join("blocks"), brk_dir, rpc); parser .parse(start, end) diff --git a/crates/brk_parser/examples/p2a.rs b/crates/brk_parser/examples/p2a.rs index c858c7800..5fc6d89a7 100644 --- a/crates/brk_parser/examples/p2a.rs +++ b/crates/brk_parser/examples/p2a.rs @@ -1,11 +1,12 @@ use bitcoincore_rpc::{Auth, Client}; -use brk_core::{Height, OutputType, default_bitcoin_path}; +use brk_core::{Height, OutputType, default_bitcoin_path, default_brk_path}; use brk_parser::Parser; fn main() { let i = std::time::Instant::now(); let bitcoin_dir = default_bitcoin_path(); + let brk_dir = default_brk_path(); let rpc = Box::leak(Box::new( Client::new( @@ -18,7 +19,7 @@ fn main() { // let start = None; // let end = None; - let parser = Parser::new(bitcoin_dir.join("blocks"), rpc); + let parser = Parser::new(bitcoin_dir.join("blocks"), brk_dir, rpc); // parser // .parse(start, end) diff --git a/crates/brk_parser/src/blk_index_to_blk_recap.rs b/crates/brk_parser/src/blk_index_to_blk_recap.rs index e4e619467..95e6c6db7 100644 --- a/crates/brk_parser/src/blk_index_to_blk_recap.rs +++ b/crates/brk_parser/src/blk_index_to_blk_recap.rs @@ -5,7 +5,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{BlkIndexToBlkPath, Height, blk_recap::BlkRecap}; +use crate::{blk_recap::BlkRecap, BlkIndexToBlkPath, Height}; #[derive(Debug)] pub struct BlkIndexToBlkRecap { @@ -15,11 +15,11 @@ pub struct BlkIndexToBlkRecap { impl BlkIndexToBlkRecap { pub fn import( - bitcoin_dir: &Path, + outputs_dir: &Path, blk_index_to_blk_path: &BlkIndexToBlkPath, start: Option, ) -> (Self, u16) { - let path = bitcoin_dir.join("blk_index_to_blk_recap.json"); + let path = outputs_dir.join("blk_index_to_blk_recap.json"); let tree = { if let Ok(file) = File::open(&path) { diff --git a/crates/brk_parser/src/lib.rs b/crates/brk_parser/src/lib.rs index 59b80bdb7..ff2247067 100644 --- a/crates/brk_parser/src/lib.rs +++ b/crates/brk_parser/src/lib.rs @@ -38,12 +38,17 @@ const BOUND_CAP: usize = 50; pub struct Parser { blocks_dir: PathBuf, + outputs_dir: PathBuf, rpc: &'static bitcoincore_rpc::Client, } impl Parser { - pub fn new(blocks_dir: PathBuf, rpc: &'static bitcoincore_rpc::Client) -> Self { - Self { blocks_dir, rpc } + pub fn new(blocks_dir: PathBuf, outputs_dir: PathBuf, rpc: &'static bitcoincore_rpc::Client) -> Self { + Self { + blocks_dir, + outputs_dir: outputs_dir, + rpc + } } pub fn get(&self, height: Height) -> Block { @@ -74,7 +79,7 @@ impl Parser { let blk_index_to_blk_path = BlkIndexToBlkPath::scan(blocks_dir); let (mut blk_index_to_blk_recap, blk_index) = - BlkIndexToBlkRecap::import(blocks_dir, &blk_index_to_blk_path, start); + BlkIndexToBlkRecap::import(&self.outputs_dir, &blk_index_to_blk_path, start); let xor_bytes = XORBytes::from(blocks_dir); diff --git a/crates/brk_server/Cargo.toml b/crates/brk_server/Cargo.toml index 14789f38b..50907bd44 100644 --- a/crates/brk_server/Cargo.toml +++ b/crates/brk_server/Cargo.toml @@ -28,6 +28,7 @@ jiff = { workspace = true } log = { workspace = true } minreq = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true } tower-http = { version = "0.6.6", features = ["compression-full", "trace"] } tracing = "0.1.41" diff --git a/crates/brk_server/examples/main.rs b/crates/brk_server/examples/main.rs index c92854022..fd7c6e710 100644 --- a/crates/brk_server/examples/main.rs +++ b/crates/brk_server/examples/main.rs @@ -2,7 +2,7 @@ use std::{path::Path, thread::sleep, time::Duration}; use bitcoincore_rpc::RpcApi; use brk_computer::Computer; -use brk_core::default_bitcoin_path; +use brk_core::{default_bitcoin_path, default_brk_path}; use brk_exit::Exit; use brk_fetcher::Fetcher; use brk_indexer::Indexer; @@ -18,6 +18,7 @@ pub fn main() -> color_eyre::Result<()> { let process = true; let bitcoin_dir = default_bitcoin_path(); + let brk_dir = default_brk_path(); let rpc = Box::leak(Box::new(bitcoincore_rpc::Client::new( "http://localhost:8332", @@ -25,7 +26,7 @@ pub fn main() -> color_eyre::Result<()> { )?)); let exit = Exit::new(); - let parser = Parser::new(bitcoin_dir.join("blocks"), rpc); + let parser = Parser::new(bitcoin_dir.join("blocks"), brk_dir, rpc); let outputs_dir = Path::new("../../_outputs"); diff --git a/crates/brk_server/src/api/mod.rs b/crates/brk_server/src/api/mod.rs index 99631973c..78c54e522 100644 --- a/crates/brk_server/src/api/mod.rs +++ b/crates/brk_server/src/api/mod.rs @@ -107,6 +107,16 @@ impl ApiRoutes for Router { }, ), ) + .route( + "/health", + get(|| async { + Json(serde_json::json!({ + "status": "healthy", + "service": "brk-server", + "timestamp": jiff::Timestamp::now().to_string() + })) + }), + ) .route( "/api", get(|| async { diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 000000000..494e86bb3 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,68 @@ +# Git +.git +.gitignore + +# Build artifacts +target/ + +# Development files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Docker files +Dockerfile +docker-compose.yml +.dockerignore +docker-build.sh + +# Documentation +docs/ +LICENSE +# Keep README.md for build process +!README.md + +# CI/CD +.github/ + +# Logs and temporary files +*.log +tmp/ +temp/ + +# BRK runtime data (should be in volumes) +.brk/ + +# Example and test data +examples/ +tests/ +*.test +*.example + +# Node modules (if any frontend deps) +node_modules/ + +# Python cache (if any) +__pycache__/ +*.pyc +*.pyo + +# Rust workspace cache +**/*.rs.bk + +# macOS +.AppleDouble +.LSOverride + +# Windows +Desktop.ini +ehthumbs.db + +# Linux +.directory \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 000000000..8454693c4 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,42 @@ +# Bitcoin Core data directory +# This should point to your Bitcoin Core data directory +BITCOIN_DATA_DIR=/path/to/bitcoin + +# Bitcoin Core RPC configuration +# If running Bitcoin Core on the same host (not in Docker), use host.docker.internal on macOS/Windows +# or the host's IP address on Linux +BTC_RPC_HOST=localhost +BTC_RPC_PORT=8332 + +# Use either cookie file authentication (recommended) or username/password +# Cookie file is automatically created by Bitcoin Core +# If using username/password, comment out RPCCOOKIEFILE in docker-compose.yml +# BTC_RPC_USER=your_rpc_username +# BTC_RPC_PASSWORD=your_rpc_password + +# BRK configuration +# Services to run: all, processor, or server +BRK_SERVICES=all + +# Computation mode: lazy (compute on demand) or eager (precompute and save) +BRK_COMPUTATION=lazy + +# Data format: raw (faster) or compressed (saves disk space) +BRK_FORMAT=raw + +# Enable price fetching from exchanges +BRK_FETCH=true + +# Enable Model Context Protocol (MCP) for AI/LLM integration +BRK_MCP=true + +# BRK data storage options +# Option 1: Use a Docker named volume (default, recommended) +# This is the default configuration - no changes needed. +# Leave this commented to use the default named volume +# BRK_DATA_VOLUME=brk-data + +# Option 2: Use a bind mount to a local directory +# Uncomment and set this to use a specific directory on your host +# Also uncomment the corresponding line in docker-compose.yml +# BRK_DATA_DIR=/path/to/brk/data \ No newline at end of file diff --git a/docker/DOCKER.md b/docker/DOCKER.md new file mode 100644 index 000000000..68259fdbd --- /dev/null +++ b/docker/DOCKER.md @@ -0,0 +1,263 @@ +# Docker Setup for BRK + +This guide explains how to run BRK using Docker and Docker Compose. + +## Prerequisites + +- Docker Engine (with buildx support) +- Docker Compose v2 +- A running Bitcoin Core node with RPC enabled +- Access to Bitcoin Core's blocks directory + +## Quick Start + +1. **Create environment file** + ```bash + cp docker/.env.example docker/.env + ``` + Edit `docker/.env` and set `BITCOIN_DATA_DIR` to your Bitcoin Core data directory. + +2. **Run with Docker Compose** + ```bash + docker compose -f docker/docker-compose.yml up -d + ``` + + Or from the docker directory: + ```bash + cd docker && docker compose up -d + ``` + +3. **Access BRK** + - Web interface: http://localhost:7070 + - API: http://localhost:7070/api + - Health check: http://localhost:7070/health + +## Architecture + +BRK runs as a single container that includes both the blockchain processor and API server. This simplified architecture: +- Ensures processor and server are always in sync +- Simplifies deployment and monitoring +- Uses a single shared data directory + +```bash +# Start BRK +docker compose -f docker/docker-compose.yml up + +# Or run in background +docker compose -f docker/docker-compose.yml up -d + +# Alternative: from docker directory +cd docker && docker compose up -d +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `BITCOIN_DATA_DIR` | Path to Bitcoin Core data directory | - | +| `BTC_RPC_HOST` | Bitcoin Core RPC host | `localhost` | +| `BTC_RPC_PORT` | Bitcoin Core RPC port | `8332` | +| `BTC_RPC_USER` | Bitcoin RPC username | - | +| `BTC_RPC_PASSWORD` | Bitcoin RPC password | - | +| `BRK_DATA_VOLUME` | Docker volume name for BRK data | `brk-data` | +| `BRK_COMPUTATION` | Computation mode (`lazy`, `eager`) | `lazy` | +| `BRK_FORMAT` | Data format (`raw`, `compressed`) | `raw` | +| `BRK_FETCH` | Enable price fetching | `true` | +| `BRK_MCP` | Enable MCP for AI/LLM | `true` | + +### Example .env File + +```env +# Bitcoin Core paths +BITCOIN_DATA_DIR=/path/to/bitcoin/data +BRK_DATA_VOLUME=brk-data + +# Bitcoin RPC configuration +BTC_RPC_HOST=localhost +BTC_RPC_PORT=8332 +BTC_RPC_USER=your_username +BTC_RPC_PASSWORD=your_password + +# BRK settings +BRK_COMPUTATION=lazy +BRK_FORMAT=raw +BRK_FETCH=true +BRK_MCP=true +``` + +### Connecting to Bitcoin Core + +#### Option 1: Cookie File Authentication (Recommended) +BRK will automatically use the `.cookie` file from your Bitcoin Core directory. + +#### Option 2: Username/Password +Set `BTC_RPC_USER` and `BTC_RPC_PASSWORD` in your `docker/.env` file. + +#### Network Connectivity +- **Same host**: + - If Bitcoin Core is running natively (not in Docker): Use `host.docker.internal` on macOS/Windows or `172.17.0.1` on Linux + - If Bitcoin Core is also in Docker: Use the service name or container IP +- **Remote host**: Use the actual IP address or hostname + +## Building the Image + +### Using Docker Compose +```bash +docker compose -f docker/docker-compose.yml build +``` + +### or ... Using Docker Build Script +```bash +# Build with default settings +./docker/docker-build.sh + +# Build with custom tag +./docker/docker-build.sh --tag v1.0.0 +``` + +## Volumes and Data Storage + +BRK supports two options for storing its data: + +### Option 1: Docker Named Volume (Default) +Uses a Docker-managed named volume called `brk-data`. This is the recommended approach for most users. + +### Option 2: Bind Mount +Maps a specific directory on your host to the container's data directory. +This may be desirable if you want to use a specific storage location for BRK data (e.g. a different disk). + +1. Set `BRK_DATA_DIR` in your `docker/.env` file to your desired host directory +2. In `docker/docker-compose.yml`, comment out the named volume line and uncomment the bind mount line + +```bash +# In docker/.env file +BRK_DATA_DIR=/home/user/brk-data +``` + +```bash +# In docker/docker-compose.yml +# Comment out: + - ${BRK_DATA_VOLUME:-brk-data}:/home/brk/.brk + +# Uncomment: + # - ${BRK_DATA_DIR:-./brk-data}:/home/brk/.brk +``` + +Can also remove or comment out the `volumes` section from the docker/docker-compose.yml file (right at the bottom): +```bash +# Comment out: +volumes: + brk-data: + driver: local +``` + +## Health Checks + +The container includes a combined health check that verifies: +- The BRK process is running +- The API server is responding on port 3110 + +## Monitoring + +### Check Container Status +```bash +# View running container +docker compose -f docker/docker-compose.yml ps + +# Check health status +docker compose -f docker/docker-compose.yml ps --format \"table {{.Service}}\\t{{.Status}}\\t{{.Health}}\" +``` + +### View Logs +```bash +# View logs +docker compose -f docker/docker-compose.yml logs + +# Follow logs in real-time +docker compose -f docker/docker-compose.yml logs -f +``` + +## Troubleshooting + +### Server Issues + +#### Server returns empty data +- This is normal if the processor hasn't indexed any blocks yet +- The server component will serve data as the processor indexes blocks + +#### Server won't start +- Check Docker Compose logs: `docker compose -f docker/docker-compose.yml logs` +- Verify health endpoint: `curl http://localhost:7070/health` +- Ensure no port conflicts on 7070 + +### Processor Issues + +#### Cannot connect to Bitcoin Core +1. Ensure Bitcoin Core is running with `-server=1` +2. Check RPC credentials are correct +3. Verify network connectivity from container +4. Test RPC connection: `docker compose -f docker/docker-compose.yml exec brk brk --help` + +#### Processor fails to start +- Verify Bitcoin RPC credentials in `docker/.env` +- Ensure Bitcoin Core is running and accessible +- Check Bitcoin data directory permissions (should be readable by UID 1000) + +### Performance Issues + +#### Slow indexing +- Ensure adequate disk space for indexed data - a minimum of 3GB/s is recommended +- Monitor memory usage during initial indexing +- Use `BRK_COMPUTATION=lazy` to reduce memory usage + +#### Out of memory +- Increase Docker's memory limit +- Use `BRK_COMPUTATION=lazy` mode +- Monitor container resource usage: `docker stats` + +### Permission Issues + +#### Permission denied errors +- Ensure the Bitcoin data directory is readable by the container user (UID 1000) +- Check that volumes are properly mounted +- Verify file ownership: `ls -la $BITCOIN_DATA_DIR` + +### Network Issues + +#### Cannot access web interface +- Verify port mapping: `docker compose -f docker/docker-compose.yml ps` +- Check firewall settings +- Ensure no other services are using port 7070 + +## Security Considerations + +- Bitcoin data is mounted read-only for safety +- BRK runs as non-root user inside container +- Only necessary ports are exposed + +## Backup and Recovery + +### Backing Up BRK Data + +```bash +# Create backup of named volume +docker run --rm -v brk_brk-data:/source -v \"$(pwd)\":/backup alpine tar czf /backup/brk-backup.tar.gz -C /source . + +# Or if using bind mount +tar czf brk-backup.tar.gz -C \"$BRK_DATA_DIR\" . +``` + +### Restoring BRK Data + +```bash +# Stop container +docker compose -f docker/docker-compose.yml down + +# Restore from backup (named volume) +docker run --rm -v brk_brk-data:/target -v \"$(pwd)\":/backup alpine tar xzf /backup/brk-backup.tar.gz -C /target + +# Start container +docker compose -f docker/docker-compose.yml up -d +``` \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..ca1d387f4 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,60 @@ +# ************* +# Builder +# ************* +FROM rustlang/rust:nightly AS builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libssl-dev \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy all source files +COPY . . + +# Build the application +RUN cargo build --release --locked + +# ************* +# Runtime +# ************* +FROM debian:bookworm-slim + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN useradd -m -s /bin/bash brk + +# Copy binary from builder +COPY --from=builder /app/target/release/brk /usr/local/bin/brk + +# Copy websites directory +COPY --from=builder /app/websites /app/websites + +# Set ownership +RUN chown -R brk:brk /app + +# Switch to non-root user +USER brk + +# Create directories for BRK data +RUN mkdir -p /home/brk/.brk + +# Expose API port +EXPOSE 3110 + +# Set working directory +WORKDIR /home/brk + +# Default entrypoint +ENTRYPOINT ["brk"] + +# Default command (can be overridden) +CMD ["--services", "all"] \ No newline at end of file diff --git a/docker/docker-build.sh b/docker/docker-build.sh new file mode 100755 index 000000000..f64f279a6 --- /dev/null +++ b/docker/docker-build.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +IMAGE_NAME="brk" +TAG="latest" + +# Function to print colored output +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -t|--tag) + TAG="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -t, --tag TAG Tag for the image (default: latest)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $1" + exit 1 + ;; + esac +done + +# Build the image +print_info "Building BRK Docker image..." +print_info "Image: ${IMAGE_NAME}:${TAG}" + +# Detect script location and set paths accordingly +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Determine if we're running from project root or docker directory +if [[ "$(basename "$PWD")" == "docker" ]]; then + # Running from docker directory + DOCKERFILE_PATH="./Dockerfile" + BUILD_CONTEXT=".." + print_info "Running from docker directory" +else + # Running from project root or elsewhere + DOCKERFILE_PATH="docker/Dockerfile" + BUILD_CONTEXT="." + print_info "Running from project root" +fi + +# Execute the build +if docker build -f "$DOCKERFILE_PATH" -t "${IMAGE_NAME}:${TAG}" "$BUILD_CONTEXT"; then + print_info "Build completed successfully!" + print_info "Image built as ${IMAGE_NAME}:${TAG}" +else + print_error "Build failed!" + exit 1 +fi \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..aa2b01633 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,64 @@ +# BRK single-container Docker Compose configuration + +name: brk + +services: + brk: + build: + context: .. + dockerfile: docker/Dockerfile + image: brk:latest + container_name: brk + restart: unless-stopped + ports: + - 7070:3110 # Map host port 7070 to container port 3110 + volumes: + # Bitcoin Core data directory (read-only) + # For access to raw block data + - ${BITCOIN_DATA_DIR:-/path/to/bitcoin}:/bitcoin:ro + # BRK data directory for outputs and state + # Option 1: Use a named volume (default) + - ${BRK_DATA_VOLUME:-brk-data}:/home/brk/.brk + # Option 2: Use a bind mount (uncomment and set BRK_DATA_DIR in .env) + # - ${BRK_DATA_DIR:-./brk-data}:/home/brk/.brk + environment: + # Bitcoin Core configuration + - BITCOINDIR=/bitcoin + - BLOCKSDIR=/bitcoin/blocks + + # RPC configuration (required for processor) + - RPCCONNECT=${BTC_RPC_HOST:-localhost} + - RPCPORT=${BTC_RPC_PORT:-8332} + # - RPCCOOKIEFILE=/bitcoin/.cookie + + # Username/password authentication + - RPCUSER=${BTC_RPC_USER} + - RPCPASSWORD=${BTC_RPC_PASSWORD} + + # BRK configuration + - BRKDIR=/home/brk/.brk + - COMPUTATION=${BRK_COMPUTATION:-lazy} + - FORMAT=${BRK_FORMAT:-raw} + - FETCH=${BRK_FETCH:-true} + - MCP=${BRK_MCP:-true} + command: + - --bitcoindir + - /bitcoin + - --brkdir + - /home/brk/.brk + - --rpcconnect + - "${BTC_RPC_HOST:-localhost}" + - --rpcuser + - "${BTC_RPC_USER:-bitcoin}" + - --rpcpassword + - "${BTC_RPC_PASSWORD:-bitcoin}" + healthcheck: + test: ["CMD", "sh", "-c", "pgrep -f brk && nc -z localhost 3110"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + +volumes: + brk-data: + driver: local \ No newline at end of file