A block explorer for the Dash network. Dashscan indexes the Dash blockchain into a PostgreSQL database and exposes it through a REST API and a web frontend.
| Component | Stack | Purpose |
|---|---|---|
indexer/ |
Rust (Tokio, tokio-postgres) | Syncs blocks from Dash Core via JSON-RPC and ZMQ |
api/ |
TypeScript (Fastify, Knex) | REST API backed by PostgreSQL |
frontend/ |
React (TanStack Router/Query, Tailwind) | Web UI |
- Block and transaction browsing
- Address balances and transaction history
- Masternode list and detail pages
- Special transactions (coinbase, superblocks, etc.)
- UTXO tracking
- DAO governance page
- Prometheus metrics (via API)
For a Docker Compose deployment, only Docker is needed — skip to Deploying with Docker Compose.
To run the components directly:
- Rust (2024 edition)
- Node.js 18+
- PostgreSQL 14+
- A running Dash Core node with RPC and ZMQ enabled
Create a PostgreSQL database:
CREATE USER dashscan WITH PASSWORD 'dashscan';
CREATE DATABASE dashscan OWNER dashscan;Migrations (Refinery) are applied by the indexer's migrate subcommand, which
exits when finished — a normal indexer start does not apply them:
cd indexer
cargo run --release -- migrateUnder Docker Compose this runs automatically as the one-shot migrate service,
which the indexer and api services wait on before starting.
cd indexer
cp .env.example .env # edit with your values
cargo run --release.env variables:
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
CORE_RPC_HOST / CORE_RPC_PORT |
Dash Core RPC address |
CORE_RPC_USER / CORE_RPC_PASSWORD |
Dash Core RPC credentials |
CORE_ZMQ_URL |
ZMQ endpoint for new-block notifications |
CORE_P2P_HOST / CORE_P2P_PORT |
Dash Core P2P address (block catch-up + peer crawler seed) |
REDIS_URL |
Redis connection string (governance + peer caches) |
NETWORK |
mainnet or testnet |
START_HEIGHT |
Block height to begin indexing from |
PEER_CRAWL_EVERY_BLOCKS |
Run a peer crawl every N live-sync blocks (default 25; 0 disables) |
PEER_CRAWL_MAX_PEERS |
Safety cap on addresses visited per crawl round (default 100000) |
PEER_CRAWL_CONCURRENCY |
Concurrent peer connections per crawl (default 512) |
PEER_CRAWL_DEADLINE_SECS |
Wall-clock ceiling for one crawl round (default 600) |
PEER_CRAWL_CONNECT_TIMEOUT_SECS |
Per-peer connect/handshake timeout (default 3) |
cd api
npm install
cp .env.example .env # edit with your values
npm start.env variables: DATABASE_URL, CORE_RPC_*, CORE_ZMQ_URL, NETWORK.
cd frontend
npm install
npm run dev # development (port 3000)
npm run build # production builddocker-compose.yml in the repo root runs the whole stack. Only Docker is
required — the Rust/Node/PostgreSQL prerequisites above are for local development.
Verified with Docker Compose v5.4.0. Requires a version supporting
depends_on[].required and service_completed_successfully.
cp .env.example .env # then edit it — see below
docker compose up -d --build
docker compose logs -fThe first run builds the images locally; the indexer is a Rust release build and takes several minutes.
api, indexer and migrate run published images from GHCR rather than
building on the server. Pin the release in .env:
VERSION=0.4.8 # git tag pushed by .github/workflows/publish.ymlThen on the server:
docker compose pull
docker compose up -d --no-build--no-build matters: api and indexer also carry a build: section for
local development, so without it a failed or skipped pull silently falls back to
building from source on the server.
Rolling back is the same flow with an earlier VERSION — but note that
migrations are not reversible, so an older image may not match a newer schema.
Image names are derived from the CI naming and can be overridden:
| Variable | Default | Resulting reference |
|---|---|---|
VERSION |
nightly |
tag — a git tag (0.4.8) or nightly from develop merges |
REGISTRY |
ghcr.io |
ghcr.io/pshenmic/dashscan-api:0.4.8 |
IMAGE_PREFIX |
pshenmic/dashscan |
ghcr.io/pshenmic/dashscan-indexer:0.4.8 |
migrate intentionally uses the same image and tag as the indexer, so the
schema always matches the binary that runs against it.
Two caveats:
- Published images are
linux/amd64only. They run on a typical x86 server, but pulling on Apple Silicon fails withno matching manifest for linux/arm64/v8. Build locally instead, or add multi-arch (platforms:) to the publish workflow. - CI does not publish a frontend image —
publish.ymlhas jobs for api and indexer only. The frontend is built locally fromfrontend/Dockerfile, which is also whyVERSIONdoesn't apply to it. BecauseDASHSCAN_API_URLis baked into the bundle at build time, a published frontend image would have to be built per environment rather than pulled by version.
| Service | Profile | Notes |
|---|---|---|
migrate |
always | One-shot; applies Refinery migrations, then exits |
indexer |
always | Waits for migrate to exit 0 |
api |
always | Runs API_REPLICAS replicas (default 4); no published port of its own |
haproxy |
always | Load balances the api replicas; publishes API_PORT |
postgres |
local-db |
|
redis |
local-redis |
|
dashd |
local-dashcore |
Dash Core node; mainnet needs ~40GB disk |
frontend |
local-frontend |
Publishes FRONTEND_PORT |
Infrastructure is opt-in via profiles, so the same file serves both a self-contained dev box and a production host that uses managed Postgres/Redis and an existing Dash Core node.
Set profiles once in .env rather than passing flags every time:
# Production: external DB, Redis and Core; frontend served elsewhere
# COMPOSE_PROFILES=
# Everything local
COMPOSE_PROFILES=local-db,local-redis,local-dashcore,local-frontendAnything omitted must be reachable at the URL you configure — DATABASE_URL,
REDIS_URL and the CORE_* variables. For a service running on the Docker
host itself, use host.docker.internal (already mapped for api and indexer).
.env next to docker-compose.yml. DATABASE_URL and REDIS_URL are
required and fail fast if missing; .env.example documents every value.
| Variable | Description |
|---|---|
VERSION |
Published image tag for api/indexer/migrate (default nightly) |
REGISTRY / IMAGE_PREFIX |
Override where images are pulled from |
COMPOSE_PROFILES |
Which optional services to run (see above) |
DATABASE_URL |
Required. Host postgres with the local-db profile |
REDIS_URL |
Required. Host redis with the local-redis profile |
NETWORK |
mainnet or testnet |
CORE_RPC_*, CORE_ZMQ_URL, CORE_P2P_* |
Dash Core connection |
CORE_CHAIN |
local-dashcore only — main/test/regtest; must agree with NETWORK |
POSTGRES_USER / _PASSWORD / _DB |
local-db only; must agree with DATABASE_URL |
API_PORT |
Host port published by haproxy (default 3005) |
API_REPLICAS |
Number of api containers (default 4) |
FRONTEND_PORT |
Host port for the frontend (default 3000) |
DASHSCAN_API_URL |
Build-time. See below |
DASHSCAN_API_URL is inlined into the frontend bundle at build time, so it must
be the URL a browser can reach — not an in-network name like
http://api:3005. Changing it requires a rebuild, not just a restart:
docker compose build frontend && docker compose up -d frontendThe local-dashcore profile runs dashpay/dashd. Its RPC port is deliberately
not published to the host (-rpcallowip is open, so it stays on the compose
network); only the P2P port is. Initial mainnet sync takes a long time and the
API returns 503 {"status":"syncing"} until the indexer catches up.
When pointing at your own node instead, it needs server=1, matching RPC
credentials, and these ZMQ publishers, which the indexer subscribes to:
zmqpubhashblock, zmqpubrawtx, zmqpubrawtxlock,
zmqpubrawtxlocksig, zmqpubrawchainlocksig
haproxy discovers api replicas through Docker DNS, so scaling needs no
restart or config change:
docker compose up -d --scale api=6 apiThe haproxy config is inlined in docker-compose.yml under the top-level
configs: key, so a deploy host needs no file besides docker-compose.yml and
.env. It pre-allocates 8 backend slots (server-template api 8);
raise that line before going beyond 8. There are no health checks — every
replica stays in rotation and its response (including 503 while syncing) is
passed through. A cleanly stopped container is dropped from DNS automatically,
but a hung one keeps receiving its share of traffic.
The stats UI is on port 8404, unpublished. To view it:
docker compose exec haproxy wget -qO- http://127.0.0.1:8404/The migrate service runs before indexer and api on every up, and is a
no-op once the schema is current. To run it by hand:
docker compose run --rm migratedocker compose ps # status
docker compose logs -f indexer # follow one service
docker compose pull && docker compose up -d --no-build # deploy pinned VERSION
docker compose down # stop, keep data
docker compose down -v # stop and DELETE all volumesdown -v destroys the Postgres, Redis and Dash Core volumes, including the
indexed chain.
- No TLS. haproxy serves plain HTTP on
API_PORT, and the frontend onFRONTEND_PORT. Terminate TLS in haproxy or put a reverse proxy in front. - Replace every
change-mecredential in.env. - Only publish the ports you actually need.
MIT