diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6eb77546..1f90726c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,8 @@ NOTE: Add new changes BELOW THIS COMMENT.
 
 ### Added
 
+- Ability to define custom directories for storage of query log files and
+  statistics ([#5992]).
 - Context menu item in the Query Log to add a Client to the Persistent client
   list ([#6679]).
 
@@ -59,6 +61,7 @@ NOTE: Add new changes BELOW THIS COMMENT.
 
 - Go 1.20 support, as it has reached end of life.
 
+[#5992]: https://github.com/AdguardTeam/AdGuardHome/issues/5992
 [#6679]: https://github.com/AdguardTeam/AdGuardHome/issues/6679
 
 [go-toolchain]: https://go.dev/blog/toolchain
diff --git a/internal/aghnet/hostscontainer.go b/internal/aghnet/hostscontainer.go
index 76b61b03..b8e86448 100644
--- a/internal/aghnet/hostscontainer.go
+++ b/internal/aghnet/hostscontainer.go
@@ -154,8 +154,8 @@ func pathsToPatterns(fsys fs.FS, paths []string) (patterns []string, err error)
 }
 
 // handleEvents concurrently handles the file system events.  It closes the
-// update channel of HostsContainer when finishes.  It's used to be called
-// within a separate goroutine.
+// update channel of HostsContainer when finishes.  It is intended to be used as
+// a goroutine.
 func (hc *HostsContainer) handleEvents() {
 	defer log.OnPanic(fmt.Sprintf("%s: handling events", hostsContainerPrefix))
 
diff --git a/internal/aghos/fswatcher.go b/internal/aghos/fswatcher.go
index 32d88f21..1694242e 100644
--- a/internal/aghos/fswatcher.go
+++ b/internal/aghos/fswatcher.go
@@ -66,7 +66,7 @@ func NewOSWritesWatcher() (w FSWatcher, err error) {
 	return fsw, nil
 }
 
-// handleErrors handles accompanying errors.  It used to be called in a separate
+// handleErrors handles accompanying errors.  It is intended to be used as a
 // goroutine.
 func (w *osWatcher) handleErrors() {
 	defer log.OnPanic(fmt.Sprintf("%s: handling errors", osWatcherPref))
@@ -100,7 +100,7 @@ func (w *osWatcher) Close() (err error) {
 }
 
 // handleEvents notifies about the received file system's event if needed.  It
-// used to be called in a separate goroutine.
+// is intended to be used as a goroutine.
 func (w *osWatcher) handleEvents() {
 	defer log.OnPanic(fmt.Sprintf("%s: handling events", osWatcherPref))
 
diff --git a/internal/dhcpd/http_unix.go b/internal/dhcpd/http_unix.go
index 42ea315a..db81bafc 100644
--- a/internal/dhcpd/http_unix.go
+++ b/internal/dhcpd/http_unix.go
@@ -592,7 +592,7 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
 }
 
 // parseLease parses a lease from r.  If there is no error returns DHCPServer
-// and *Lease. r must be non-nil.
+// and *Lease.  r must be non-nil.
 func (s *server) parseLease(r io.Reader) (srv DHCPServer, lease *dhcpsvc.Lease, err error) {
 	l := &leaseStatic{}
 	err = json.NewDecoder(r).Decode(l)
diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go
index d4b8092e..30211677 100644
--- a/internal/dnsforward/config.go
+++ b/internal/dnsforward/config.go
@@ -40,7 +40,7 @@ type ClientsContainer interface {
 	) (conf *proxy.CustomUpstreamConfig, err error)
 }
 
-// Config represents the DNS filtering configuration of AdGuard Home. The zero
+// Config represents the DNS filtering configuration of AdGuard Home.  The zero
 // Config is empty and ready for use.
 type Config struct {
 	// Callbacks for other modules
diff --git a/internal/home/clients.go b/internal/home/clients.go
index 3da8572f..5aa2c81e 100644
--- a/internal/home/clients.go
+++ b/internal/home/clients.go
@@ -140,8 +140,7 @@ func (clients *clientsContainer) Init(
 }
 
 // handleHostsUpdates receives the updates from the hosts container and adds
-// them to the clients container.  It's used to be called in a separate
-// goroutine.
+// them to the clients container.  It is intended to be used as a goroutine.
 func (clients *clientsContainer) handleHostsUpdates() {
 	for upd := range clients.etcHosts.Upd() {
 		clients.addFromHostsFile(upd)
diff --git a/internal/home/config.go b/internal/home/config.go
index fac08df0..17a5661d 100644
--- a/internal/home/config.go
+++ b/internal/home/config.go
@@ -259,6 +259,10 @@ type tlsConfigSettings struct {
 }
 
 type queryLogConfig struct {
+	// DirPath is the custom directory for logs.  If it's empty the default
+	// directory will be used.  See [homeContext.getDataDir].
+	DirPath string `yaml:"dir_path"`
+
 	// Ignored is the list of host names, which should not be written to log.
 	// "." is considered to be the root domain.
 	Ignored []string `yaml:"ignored"`
@@ -278,6 +282,10 @@ type queryLogConfig struct {
 }
 
 type statsConfig struct {
+	// DirPath is the custom directory for statistics.  If it's empty the
+	// default directory is used.  See [homeContext.getDataDir].
+	DirPath string `yaml:"dir_path"`
+
 	// Ignored is the list of host names, which should not be counted.
 	Ignored []string `yaml:"ignored"`
 
diff --git a/internal/home/dns.go b/internal/home/dns.go
index 5d601694..2b0267e8 100644
--- a/internal/home/dns.go
+++ b/internal/home/dns.go
@@ -46,12 +46,15 @@ func onConfigModified() {
 // server and initializes it at last.  It also must not be called unless
 // [config] and [Context] are initialized.
 func initDNS() (err error) {
-	baseDir := Context.getDataDir()
-
 	anonymizer := config.anonymizer()
 
+	statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
+	if err != nil {
+		return err
+	}
+
 	statsConf := stats.Config{
-		Filename:          filepath.Join(baseDir, "stats.db"),
+		Filename:          filepath.Join(statsDir, "stats.db"),
 		Limit:             config.Stats.Interval.Duration,
 		ConfigModified:    onConfigModified,
 		HTTPRegister:      httpRegister,
@@ -75,7 +78,7 @@ func initDNS() (err error) {
 		ConfigModified:    onConfigModified,
 		HTTPRegister:      httpRegister,
 		FindClient:        Context.clients.findMultiple,
-		BaseDir:           baseDir,
+		BaseDir:           querylogDir,
 		AnonymizeClientIP: config.DNS.AnonymizeClientIP,
 		RotationIvl:       config.QueryLog.Interval.Duration,
 		MemSize:           config.QueryLog.MemSize,
@@ -545,3 +548,50 @@ func (r safeSearchResolver) LookupIP(
 
 	return ips, nil
 }
+
+// checkStatsAndQuerylogDirs checks and returns directory paths to store
+// statistics and query log.
+func checkStatsAndQuerylogDirs(
+	ctx *homeContext,
+	conf *configuration,
+) (statsDir, querylogDir string, err error) {
+	baseDir := ctx.getDataDir()
+
+	statsDir = conf.Stats.DirPath
+	if statsDir == "" {
+		statsDir = baseDir
+	} else {
+		err = checkDir(statsDir)
+		if err != nil {
+			return "", "", fmt.Errorf("statistics: custom directory: %w", err)
+		}
+	}
+
+	querylogDir = conf.QueryLog.DirPath
+	if querylogDir == "" {
+		querylogDir = baseDir
+	} else {
+		err = checkDir(querylogDir)
+		if err != nil {
+			return "", "", fmt.Errorf("querylog: custom directory: %w", err)
+		}
+	}
+
+	return statsDir, querylogDir, nil
+}
+
+// checkDir checks if the path is a directory.  It's used to check for
+// misconfiguration at startup.
+func checkDir(path string) (err error) {
+	var fi os.FileInfo
+	if fi, err = os.Stat(path); err != nil {
+		// Don't wrap the error, since it's informative enough as is.
+		return err
+	}
+
+	if !fi.IsDir() {
+		return fmt.Errorf("%q is not a directory", path)
+	}
+
+	return nil
+}
diff --git a/internal/home/tls.go b/internal/home/tls.go
index e022d043..7bd573dd 100644
--- a/internal/home/tls.go
+++ b/internal/home/tls.go
@@ -704,9 +704,9 @@ const (
 	keyTypeRSA     = "RSA"
 )
 
-// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
+// Attempt to parse the given private key DER block.  OpenSSL 0.9.8 generates
 // PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
-// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
+// OpenSSL ecparam generates SEC1 EC private keys for ECDSA.  We try all three.
 //
 // TODO(a.garipov): Find out if this version of parsePrivateKey from the stdlib
 // is actually necessary.
diff --git a/internal/querylog/search.go b/internal/querylog/search.go
index b5e0c4ec..f8c8f90e 100644
--- a/internal/querylog/search.go
+++ b/internal/querylog/search.go
@@ -49,8 +49,8 @@ func (l *queryLog) client(clientID, ip string, cache clientCache) (c *Client, er
 // the total amount of records in the buffer at the moment of searching.
 // l.confMu is expected to be locked.
 func (l *queryLog) searchMemory(params *searchParams, cache clientCache) (entries []*logEntry, total int) {
-	// We use this configuration check because a buffer can contain a single log
-	// record.  See [newQueryLog].
+	// Check memory size, as the buffer can contain a single log record.  See
+	// [newQueryLog].
 	if l.conf.MemSize == 0 {
 		return nil, 0
 	}
@@ -186,7 +186,7 @@ func (l *queryLog) setQLogReader(olderThan time.Time) (qr *qLogReader, err error
 	return r, nil
 }
 
-// readEntries reads entries from the reader to totalLimit.   By default, we do
+// readEntries reads entries from the reader to totalLimit.  By default, we do
 // not scan more than maxFileScanEntries at once.  The idea is to make search
 // calls faster so that the UI could handle it and show something quicker.
 // This behavior can be overridden if maxFileScanEntries is set to 0.
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index bf7a9dae..4e4a21d4 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -314,7 +314,7 @@ func (u *Updater) clean() {
 	_ = os.RemoveAll(u.updateDir)
 }
 
-// MaxPackageFileSize is a maximum package file length in bytes. The largest
+// MaxPackageFileSize is a maximum package file length in bytes.  The largest
 // package whose size is limited by this constant currently has the size of
 // approximately 9 MiB.
 const MaxPackageFileSize = 32 * 1024 * 1024