Deploy / Run the platform yourself

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-processor

The API is now at http://localhost:3800. Health check:

curl http://localhost:3800/health

The 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 = 30000

To 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-processor

OSS mode sets INSTANCE_MODE=oss, which disables the platform-only layer. Single-tenant; the operator owns access control.

Core env vars (.env):

VariableDefaultNotes
POSTGRES_PASSWORDchangeme_postgres_passwordChange before exposing publicly
POSTGRES_USER / POSTGRES_DBsecondlayer
POSTGRES_PORT127.0.0.1:5432Remove 127.0.0.1: prefix to expose
API_PORT3800
API_KEY(unset)Set to require a Bearer token on every request
INDEXER_PORT127.0.0.1:3700Localhost-only; stacks-node uses docker network
NETWORKSmainnettestnet or comma-separated for multi-network
LOG_LEVELinfo

Indexer-specific (for advanced tuning):

VariableDefaultNotes
TIP_FOLLOWER_ENABLEDtrueDisable during genesis sync (see below)
TIP_FOLLOWER_TIMEOUT60Seconds of node silence before polling
HIRO_API_URLhttps://api.mainnet.hiro.soGap-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:

VariableDefaultNotes
STACKS_NODE_RPC_URL(unset)Unset → a handler that reads throws naming this var; handlers that don't read are unaffected
SUBGRAPH_CHAIN_READ_CONCURRENCY4Max 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:latest

Images 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 indexer

The 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 migrations

Health checks:

curl http://localhost:3800/health | jq   # API
curl http://localhost:3700/health | jq   # Indexer

Security checklist:

  • Change POSTGRES_PASSWORD and BITCOIN_RPC_PASSWORD before any public exposure
  • POSTGRES_PORT and INDEXER_PORT default to 127.0.0.1:...; keep it that way unless you know what you're opening
  • Set API_KEY if 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_KEY is 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.