diff --git a/cmd/neofs-node/config.go b/cmd/neofs-node/config.go
index 17176a75c..36b166a7c 100644
--- a/cmd/neofs-node/config.go
+++ b/cmd/neofs-node/config.go
@@ -422,18 +422,22 @@ func initShardOptions(c *cfg) {
 		metabaseCfg := sc.Metabase()
 		gcCfg := sc.GC()
 
-		piloramaCfg := sc.Pilorama()
-		piloramaPath := piloramaCfg.Path()
-		if piloramaPath == "" {
-			piloramaPath = filepath.Join(blobStorCfg.Path(), "pilorama.db")
-		}
+		var piloramaOpts []pilorama.Option
 
-		piloramaOpts := []pilorama.Option{
-			pilorama.WithPath(piloramaPath),
-			pilorama.WithPerm(piloramaCfg.Perm()),
-			pilorama.WithNoSync(piloramaCfg.NoSync()),
-			pilorama.WithMaxBatchSize(piloramaCfg.MaxBatchSize()),
-			pilorama.WithMaxBatchDelay(piloramaCfg.MaxBatchDelay())}
+		piloramaCfg := sc.Pilorama()
+		if config.BoolSafe(c.appCfg.Sub("tree"), "enabled") {
+			piloramaPath := piloramaCfg.Path()
+			if piloramaPath == "" {
+				piloramaPath = filepath.Join(blobStorCfg.Path(), "pilorama.db")
+			}
+
+			piloramaOpts = []pilorama.Option{
+				pilorama.WithPath(piloramaPath),
+				pilorama.WithPerm(piloramaCfg.Perm()),
+				pilorama.WithNoSync(piloramaCfg.NoSync()),
+				pilorama.WithMaxBatchSize(piloramaCfg.MaxBatchSize()),
+				pilorama.WithMaxBatchDelay(piloramaCfg.MaxBatchDelay())}
+		}
 
 		metaPath := metabaseCfg.Path()
 		metaPerm := metabaseCfg.BoltDB().Perm()
diff --git a/cmd/neofs-node/tree.go b/cmd/neofs-node/tree.go
index 4be5ac093..12385a4f1 100644
--- a/cmd/neofs-node/tree.go
+++ b/cmd/neofs-node/tree.go
@@ -3,10 +3,16 @@ package main
 import (
 	"context"
 
+	"github.com/nspcc-dev/neofs-node/cmd/neofs-node/config"
 	"github.com/nspcc-dev/neofs-node/pkg/services/tree"
 )
 
 func initTreeService(c *cfg) {
+	if !config.BoolSafe(c.appCfg.Sub("tree"), "enabled") {
+		c.log.Info("tree service is not enabled, skip initialization")
+		return
+	}
+
 	c.treeService = tree.New(
 		tree.WithContainerSource(c.cfgObject.cnrSource),
 		tree.WithNetmapSource(c.netMapSource),
diff --git a/config/example/node.yaml b/config/example/node.yaml
index 07836381e..46f3a9673 100644
--- a/config/example/node.yaml
+++ b/config/example/node.yaml
@@ -99,6 +99,9 @@ object:
   put:
     pool_size_remote: 100  # number of async workers for remote PUT operations
 
+tree:
+  enable: false
+
 storage:
   # note: shard configuration can be omitted for relay node (see `node.relay`)
   shard_pool_size: 15 # size of per-shard worker pools used for PUT operations
diff --git a/pkg/local_object_storage/shard/control.go b/pkg/local_object_storage/shard/control.go
index 1faf44aa0..c4a1e5efd 100644
--- a/pkg/local_object_storage/shard/control.go
+++ b/pkg/local_object_storage/shard/control.go
@@ -15,7 +15,11 @@ import (
 // Open opens all Shard's components.
 func (s *Shard) Open() error {
 	components := []interface{ Open() error }{
-		s.blobStor, s.metaBase, s.pilorama,
+		s.blobStor, s.metaBase,
+	}
+
+	if s.pilorama != nil {
+		components = append(components, s.pilorama)
 	}
 
 	if s.hasWriteCache() {
@@ -41,7 +45,11 @@ func (s *Shard) Init() error {
 	}
 
 	components := []func() error{
-		s.blobStor.Init, fMetabase, s.pilorama.Init,
+		s.blobStor.Init, fMetabase,
+	}
+
+	if s.pilorama != nil {
+		components = append(components, s.pilorama.Init)
 	}
 
 	if s.hasWriteCache() {
@@ -154,7 +162,11 @@ func (s *Shard) Close() error {
 		components = append(components, s.writeCache)
 	}
 
-	components = append(components, s.pilorama, s.blobStor, s.metaBase)
+	if s.pilorama != nil {
+		components = append(components, s.pilorama)
+	}
+
+	components = append(components, s.blobStor, s.metaBase)
 
 	for _, component := range components {
 		if err := component.Close(); err != nil {
diff --git a/pkg/local_object_storage/shard/shard.go b/pkg/local_object_storage/shard/shard.go
index 48d7cc61c..28d03f19c 100644
--- a/pkg/local_object_storage/shard/shard.go
+++ b/pkg/local_object_storage/shard/shard.go
@@ -109,7 +109,10 @@ func New(opts ...Option) *Shard {
 		metaBase:   mb,
 		writeCache: writeCache,
 		tsSource:   c.tsSource,
-		pilorama:   pilorama.NewBoltForest(c.piloramaOpts...),
+	}
+
+	if s.piloramaOpts != nil {
+		s.pilorama = pilorama.NewBoltForest(c.piloramaOpts...)
 	}
 
 	s.fillInfo()
@@ -263,5 +266,7 @@ func (s *Shard) fillInfo() {
 	if s.cfg.useWriteCache {
 		s.cfg.info.WriteCacheInfo = s.writeCache.DumpInfo()
 	}
-	s.cfg.info.PiloramaInfo = s.pilorama.DumpInfo()
+	if s.pilorama != nil {
+		s.cfg.info.PiloramaInfo = s.pilorama.DumpInfo()
+	}
 }
diff --git a/pkg/local_object_storage/shard/tree.go b/pkg/local_object_storage/shard/tree.go
index 33f120a91..f42a7b2b7 100644
--- a/pkg/local_object_storage/shard/tree.go
+++ b/pkg/local_object_storage/shard/tree.go
@@ -1,14 +1,22 @@
 package shard
 
 import (
+	"errors"
+
 	"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama"
 	cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id"
 )
 
 var _ pilorama.Forest = (*Shard)(nil)
 
+// ErrPiloramaDisabled is returned when pilorama was disabled in the configuration.
+var ErrPiloramaDisabled = errors.New("plorama is disabled")
+
 // TreeMove implements the pilorama.Forest interface.
 func (s *Shard) TreeMove(d pilorama.CIDDescriptor, treeID string, m *pilorama.Move) (*pilorama.LogMove, error) {
+	if s.pilorama == nil {
+		return nil, ErrPiloramaDisabled
+	}
 	if s.GetMode() != ModeReadWrite {
 		return nil, ErrReadOnlyMode
 	}
@@ -17,6 +25,9 @@ func (s *Shard) TreeMove(d pilorama.CIDDescriptor, treeID string, m *pilorama.Mo
 
 // TreeAddByPath implements the pilorama.Forest interface.
 func (s *Shard) TreeAddByPath(d pilorama.CIDDescriptor, treeID string, attr string, path []string, meta []pilorama.KeyValue) ([]pilorama.LogMove, error) {
+	if s.pilorama == nil {
+		return nil, ErrPiloramaDisabled
+	}
 	if s.GetMode() != ModeReadWrite {
 		return nil, ErrReadOnlyMode
 	}
@@ -25,6 +36,9 @@ func (s *Shard) TreeAddByPath(d pilorama.CIDDescriptor, treeID string, attr stri
 
 // TreeApply implements the pilorama.Forest interface.
 func (s *Shard) TreeApply(d pilorama.CIDDescriptor, treeID string, m *pilorama.Move) error {
+	if s.pilorama == nil {
+		return ErrPiloramaDisabled
+	}
 	if s.GetMode() != ModeReadWrite {
 		return ErrReadOnlyMode
 	}
@@ -33,20 +47,32 @@ func (s *Shard) TreeApply(d pilorama.CIDDescriptor, treeID string, m *pilorama.M
 
 // TreeGetByPath implements the pilorama.Forest interface.
 func (s *Shard) TreeGetByPath(cid cidSDK.ID, treeID string, attr string, path []string, latest bool) ([]pilorama.Node, error) {
+	if s.pilorama == nil {
+		return nil, ErrPiloramaDisabled
+	}
 	return s.pilorama.TreeGetByPath(cid, treeID, attr, path, latest)
 }
 
 // TreeGetMeta implements the pilorama.Forest interface.
 func (s *Shard) TreeGetMeta(cid cidSDK.ID, treeID string, nodeID pilorama.Node) (pilorama.Meta, uint64, error) {
+	if s.pilorama == nil {
+		return pilorama.Meta{}, 0, ErrPiloramaDisabled
+	}
 	return s.pilorama.TreeGetMeta(cid, treeID, nodeID)
 }
 
 // TreeGetChildren implements the pilorama.Forest interface.
 func (s *Shard) TreeGetChildren(cid cidSDK.ID, treeID string, nodeID pilorama.Node) ([]uint64, error) {
+	if s.pilorama == nil {
+		return nil, ErrPiloramaDisabled
+	}
 	return s.pilorama.TreeGetChildren(cid, treeID, nodeID)
 }
 
 // TreeGetOpLog implements the pilorama.Forest interface.
 func (s *Shard) TreeGetOpLog(cid cidSDK.ID, treeID string, height uint64) (pilorama.Move, error) {
+	if s.pilorama == nil {
+		return pilorama.Move{}, ErrPiloramaDisabled
+	}
 	return s.pilorama.TreeGetOpLog(cid, treeID, height)
 }