diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go
index a1ec97048..3de5b96b0 100644
--- a/caddytest/caddytest.go
+++ b/caddytest/caddytest.go
@@ -200,9 +200,8 @@ func (tc *Tester) startServer() error {
 	}
 
 	// start inprocess caddy server
-	os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"}
 	go func() {
-		caddycmd.Main()
+		caddycmd.MainForTesting("run", "--config", tc.configFileName, "--adapter", "caddyfile")
 	}()
 	// wait for caddy admin api to start. it should happen quickly.
 	for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
diff --git a/cmd/main.go b/cmd/main.go
index 3c3ae6270..3e0359334 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -34,6 +34,7 @@ import (
 	"time"
 
 	"github.com/caddyserver/certmagic"
+	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 	"go.uber.org/automaxprocs/maxprocs"
 	"go.uber.org/zap"
@@ -81,6 +82,24 @@ func Main() {
 	}
 }
 
+// MainForTesting implements the main function of the caddy command, used internally for testing
+func MainForTesting(args ...string) error {
+	// create a root command for testing which will not pollute the global namespace, and does not
+	// call os.Exit().
+	tmpRootCmp := cobra.Command{
+		Use:          rootCmd.Use,
+		Long:         rootCmd.Long,
+		Example:      rootCmd.Example,
+		SilenceUsage: rootCmd.SilenceUsage,
+		Version:      rootCmd.Version,
+	}
+	tmpRootCmp.SetArgs(args)
+	if err := rootCmd.Execute(); err != nil {
+		return err
+	}
+	return nil
+}
+
 // handlePingbackConn reads from conn and ensures it matches
 // the bytes in expect, or returns an error if it doesn't.
 func handlePingbackConn(conn net.Conn, expect []byte) error {