Self-host
The whole Secondlayer stack is MIT-licensed. docker compose up runs the indexer, API, and subgraph processor on your own hardware, with every surface: Index, Streams, Subgraphs, Subscriptions.
Docker + Docker Compose (v2) is the only hard requirement. You also need a Stacks node for the event feed — run one yourself, or point the indexer at an existing node's event observer.
Hardware — app services only (external Stacks node):
- 8 GB RAM, 100 GB SSD, any modern CPU
- The database grows ~1 GB per 100K blocks; mainnet at ~7M+ blocks is 70+ GB today
Hardware — full stack (bitcoind + stacks-node bundled):
- 128 GB RAM (bitcoind 32 GB, stacks-node 64 GB, headroom for PG/indexer)
- 2 TB NVMe SSD (Bitcoin IBD ~700 GB, Stacks ~200 GB, plus subgraph data)
- 8+ cores, 1 Gbps network recommended (IBD downloads 700+ GB)
OSS runs on a single Postgres instance; the source/target DB split in docker/SCHEMA_SPLIT.md is a hosted-scale concern you don't need.
No light mode
There's no light mode that fetches from Hiro's REST API; it's too slow to index anything useful. Run the full chain, point at an external node, or use hosted.
Local contract development: sl devnet connect wires up a Clarinet devnet in one step — no mainnet node.
git clone https://github.com/ryanwaits/secondlayer.git
cd secondlayer/docker/oss
cp .env.example .env
# Edit .env — set POSTGRES_PASSWORD at minimum.
docker compose up -d postgres migrate api indexer subgraph-processorThe API is now at http://localhost:3800. Health check:
curl http://localhost:3800/healthThe API is open by default. For a Bearer token, set API_KEY in .env and uncomment the matching line in docker-compose.yml.
Point an existing Stacks node at the indexer by adding this to its Config.toml:
[[events_observer]]
endpoint = "your-server:3700"
events_keys = ["*"]
timeout_ms = 30000To bundle bitcoind + stacks-node:
cp .env.example .env
# Set POSTGRES_PASSWORD and BITCOIN_RPC_PASSWORD (strong random).
# Update bitcoin.conf and Config.toml to match the RPC password.
# Stage bitcoin.conf before first start:
mkdir -p ./data/bitcoin
cp bitcoin.conf ./data/bitcoin/bitcoin.conf
sudo chown -R 1000:1000 ./data/bitcoin
# 1. Start bitcoind — IBD takes 1–3 days.
docker compose --profile node up -d bitcoind
# 2. Wait for IBD past Stacks genesis (~block 666050):
docker compose exec bitcoind bitcoin-cli \
-rpcuser=stacks -rpcpassword=$BITCOIN_RPC_PASSWORD getblockcount
# 3. Start stacks-node once bitcoind is past 666050:
docker compose --profile node up -d stacks-node
# 4. Start app services:
docker compose up -d postgres migrate api indexer subgraph-processorOSS mode sets INSTANCE_MODE=oss, which disables the platform-only layer. Single-tenant; the operator owns access control.
Core env vars (.env):
| Variable | Default | Notes |
|---|---|---|
POSTGRES_PASSWORD | changeme_postgres_password | Change before exposing publicly |
POSTGRES_USER / POSTGRES_DB | secondlayer | |
POSTGRES_PORT | 127.0.0.1:5432 | Remove 127.0.0.1: prefix to expose |
API_PORT | 3800 | |
API_KEY | (unset) | Set to require a Bearer token on every request |
INDEXER_PORT | 127.0.0.1:3700 | Localhost-only; stacks-node uses docker network |
NETWORKS | mainnet | testnet or comma-separated for multi-network |
LOG_LEVEL | info |
Indexer-specific (for advanced tuning):
| Variable | Default | Notes |
|---|---|---|
TIP_FOLLOWER_ENABLED | true | Disable during genesis sync (see below) |
TIP_FOLLOWER_TIMEOUT | 60 | Seconds of node silence before polling |
HIRO_API_URL | https://api.mainnet.hiro.so | Gap-fill fallback only |
HIRO_API_KEY | (unset) | Optional — improves gap-fill rate limits |
Subgraph handlers that read the chain (ctx.client.contract(...).read) need a
Nakamoto node — the same one you already run, not a new service:
| Variable | Default | Notes |
|---|---|---|
STACKS_NODE_RPC_URL | (unset) | Unset → a handler that reads throws naming this var; handlers that don't read are unaffected |
SUBGRAPH_CHAIN_READ_CONCURRENCY | 4 | Max concurrent node reads across all handlers in the process |
Results are memoized in the chain_read_cache table on the chain database — no
Redis, no extra container.
# ghcr.io/ryanwaits/secondlayer-{api,indexer,worker,decoder,subgraph-processor,subscription-processor}
docker pull ghcr.io/ryanwaits/secondlayer-api:latest
docker pull ghcr.io/ryanwaits/secondlayer-indexer:latest
docker pull ghcr.io/ryanwaits/secondlayer-subgraph-processor:latestImages publish on every v* git tag. Pin v1.2.3 in production, not :latest. The compose file builds from source by default; swap its build: block for image: to use the pulled images.
Use a tag after the read-parity fix
Use a tag cut after commit 169b0967 (the OSS read-parity fix); older images returned 402 UPGRADE_REQUIRED on reads.
# Start with the tip follower off so it doesn't interfere with IBD:
TIP_FOLLOWER_ENABLED=false docker compose up -d postgres migrate api indexer subgraph-processor
# Check sync progress (compare to chain tip):
curl http://localhost:3700/public/status | jq .chainTip
# Once caught up, re-enable:
TIP_FOLLOWER_ENABLED=true docker compose up -d --force-recreate indexerThe indexer integrity loop runs every 5 min and auto-fills gaps from the local DB or Hiro's API as a fallback.
Upgrade:
git pull
docker compose build
docker compose up -d # the migrate service runs automatically and applies new migrationsHealth checks:
curl http://localhost:3800/health | jq # API
curl http://localhost:3700/health | jq # IndexerSecurity checklist:
- Change
POSTGRES_PASSWORDandBITCOIN_RPC_PASSWORDbefore any public exposure POSTGRES_PORTandINDEXER_PORTdefault to127.0.0.1:...; keep it that way unless you know what you're opening- Set
API_KEYif the API is reachable from untrusted networks - Never publish port 8332 (bitcoind RPC) to the internet
Deploy a subgraph against your local instance:
export SL_API_URL=http://localhost:3800
export SL_API_KEY=<your-key> # only if API_KEY is set
sl subgraphs deploy ./my-subgraph.ts- Auth/keys:
API_KEYis one shared bearer token; layer your own reverse proxy for finer control. - Billing, credits, x402: no metering; reads are unbounded single-tenant.
- Multi-tenant: one operator, one Postgres, one set of subgraphs.
- Explore: secondlayer.tools/subgraphs/explore is managed-only.
Support is community/best-effort — file issues at github.com/ryanwaits/secondlayer. For managed hosting with SLAs and priority support, see pricing.