diff --git a/cmd/main.go b/cmd/main.go
index eafadb6..21da71a 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -2,11 +2,13 @@ package cmd
 
 import (
 	"bytes"
+	"context"
 	"crypto/tls"
 	"errors"
 	"fmt"
 	"net"
 	"strings"
+	"time"
 
 	"github.com/rs/zerolog/log"
 	"github.com/urfave/cli/v2"
@@ -109,8 +111,10 @@ func Serve(ctx *cli.Context) error {
 
 	certificates.SetupCertificates(mainDomainSuffix, dnsProvider, acmeConfig, acmeUseRateLimits, enableHTTPServer, challengeCache, keyDatabase)
 
-	// TODO: make it graceful
-	go certificates.MaintainCertDB(mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase)
+	interval := 12 * time.Hour
+	certMaintainCtx, cancelCertMaintain := context.WithCancel(context.Background())
+	defer cancelCertMaintain()
+	go certificates.MaintainCertDB(certMaintainCtx, interval, mainDomainSuffix, dnsProvider, acmeUseRateLimits, keyDatabase)
 
 	if enableHTTPServer {
 		go func() {
diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go
index e484d78..bb6a3c3 100644
--- a/server/certificates/certificates.go
+++ b/server/certificates/certificates.go
@@ -2,6 +2,7 @@ package certificates
 
 import (
 	"bytes"
+	"context"
 	"crypto/ecdsa"
 	"crypto/elliptic"
 	"crypto/rand"
@@ -446,7 +447,7 @@ func SetupCertificates(mainDomainSuffix []byte, dnsProvider string, acmeConfig *
 	}
 }
 
-func MaintainCertDB(mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimits bool, keyDatabase database.CertDB) {
+func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimits bool, keyDatabase database.CertDB) {
 	for {
 		// clean up expired certs
 		now := time.Now()
@@ -503,6 +504,10 @@ func MaintainCertDB(mainDomainSuffix []byte, dnsProvider string, acmeUseRateLimi
 			}
 		}
 
-		time.Sleep(12 * time.Hour)
+		select {
+		case <-ctx.Done():
+			return
+		case <-time.After(interval):
+		}
 	}
 }