diff --git a/bin/metrics-server b/bin/metrics-server
index d8f2ed9faa48ea5fb620db1e09b60fdac63657a1..f8eca93908ed4aa8e7088260ee0655068ebd9ece 100755
--- a/bin/metrics-server
+++ b/bin/metrics-server
@@ -9,4 +9,5 @@ raise "METRICS_SERVER_TARGET cannot be blank" if target.blank?
 metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}")
 wipe_metrics_dir = Gitlab::Utils.to_boolean(ENV['WIPE_METRICS_DIR']) || false
 
-Process.wait(MetricsServer.spawn(target, metrics_dir: metrics_dir, wipe_metrics_dir: wipe_metrics_dir))
+server = MetricsServer.new(target, metrics_dir, wipe_metrics_dir)
+server.start
diff --git a/metrics_server/metrics_server.rb b/metrics_server/metrics_server.rb
index f1bcd6f611eedc239f1953766379398640978ef3..70769459019c70f38dc1ea7b13d3dc7841d0d10c 100644
--- a/metrics_server/metrics_server.rb
+++ b/metrics_server/metrics_server.rb
@@ -6,8 +6,23 @@
 
 class MetricsServer # rubocop:disable Gitlab/NamespacedClass
   class << self
-    def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: [])
-      raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target)
+    def spawn(target, metrics_dir:, gitlab_config: nil, wipe_metrics_dir: false)
+      ensure_valid_target!(target)
+
+      cmd = "#{Rails.root}/bin/metrics-server"
+      env = {
+        'METRICS_SERVER_TARGET' => target,
+        'WIPE_METRICS_DIR' => wipe_metrics_dir ? '1' : '0'
+      }
+      env['GITLAB_CONFIG'] = gitlab_config if gitlab_config
+
+      Process.spawn(env, cmd, err: $stderr, out: $stdout, pgroup: true).tap do |pid|
+        Process.detach(pid)
+      end
+    end
+
+    def fork(target, metrics_dir:, wipe_metrics_dir: false, reset_signals: [])
+      ensure_valid_target!(target)
 
       pid = Process.fork
 
@@ -15,7 +30,7 @@ def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: [])
         # Remove any custom signal handlers the parent process had registered, since we do
         # not want to inherit them, and Ruby forks with a `clone` that has the `CLONE_SIGHAND`
         # flag set.
-        Gitlab::ProcessManagement.modify_signals(trapped_signals, 'DEFAULT')
+        Gitlab::ProcessManagement.modify_signals(reset_signals, 'DEFAULT')
 
         server = MetricsServer.new(target, metrics_dir, wipe_metrics_dir)
         # This rewrites /proc/cmdline, since otherwise tools like `top` will show the
@@ -29,6 +44,12 @@ def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: [])
 
       pid
     end
+
+    private
+
+    def ensure_valid_target!(target)
+      raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target)
+    end
   end
 
   def initialize(target, metrics_dir, wipe_metrics_dir)
@@ -40,7 +61,7 @@ def initialize(target, metrics_dir, wipe_metrics_dir)
   def start
     ::Prometheus::Client.configure do |config|
       config.multiprocess_files_dir = @metrics_dir
-      config.pid_provider = proc { "#{@target}_exporter" }
+      config.pid_provider = proc { name }
     end
 
     FileUtils.mkdir_p(@metrics_dir, mode: 0700)
@@ -57,16 +78,18 @@ def start
       case @target
       when 'puma'
         Gitlab::Metrics::Exporter::WebExporter.instance(**default_opts)
-      else
-        exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize
+      when 'sidekiq'
         settings = Settings.new(Settings.monitoring[name])
-        exporter_class.instance(settings, **default_opts)
+        Gitlab::Metrics::Exporter::SidekiqExporter.instance(settings, **default_opts)
       end
 
     exporter.start
   end
 
   def name
-    "#{@target}_exporter"
+    case @target
+    when 'puma' then 'web_exporter'
+    when 'sidekiq' then 'sidekiq_exporter'
+    end
   end
 end
diff --git a/sidekiq_cluster/cli.rb b/sidekiq_cluster/cli.rb
index bbe95788a356b5763bbdf13984a1121f1be0ffdf..2feb77601b89cbd96bd8fb55e89201f45dfb5155 100644
--- a/sidekiq_cluster/cli.rb
+++ b/sidekiq_cluster/cli.rb
@@ -191,11 +191,11 @@ def start_metrics_server(wipe_metrics_dir: false)
         return unless metrics_server_enabled?
 
         @logger.info("Starting metrics server on port #{sidekiq_exporter_port}")
-        @metrics_server_pid = MetricsServer.spawn(
+        @metrics_server_pid = MetricsServer.fork(
           'sidekiq',
           metrics_dir: @metrics_dir,
           wipe_metrics_dir: wipe_metrics_dir,
-          trapped_signals: TERMINATE_SIGNALS + FORWARD_SIGNALS
+          reset_signals: TERMINATE_SIGNALS + FORWARD_SIGNALS
         )
       end
 
diff --git a/spec/commands/metrics_server/metrics_server_spec.rb b/spec/commands/metrics_server/metrics_server_spec.rb
index 3b92c3c38fab6170040666b8a8db14083780d1b7..217aa185767969749fa384535fb9656046490dc7 100644
--- a/spec/commands/metrics_server/metrics_server_spec.rb
+++ b/spec/commands/metrics_server/metrics_server_spec.rb
@@ -38,13 +38,7 @@
         config_file.write(YAML.dump(config))
         config_file.close
 
-        env = {
-          'GITLAB_CONFIG' => config_file.path,
-          'METRICS_SERVER_TARGET' => target,
-          'WIPE_METRICS_DIR' => '1',
-          'prometheus_multiproc_dir' => metrics_dir
-        }
-        @pid = Process.spawn(env, 'bin/metrics-server', pgroup: true)
+        @pid = MetricsServer.spawn(target, metrics_dir: metrics_dir, gitlab_config: config_file.path, wipe_metrics_dir: true)
       end
 
       after do
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index 4d4e1f35cff9a7fb321095a3c4a31e5b95eaafac..15b738cacd1005c8e6bf87980e713afa82018e07 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -303,7 +303,7 @@
             end
 
             it 'does not start a sidekiq metrics server' do
-              expect(MetricsServer).not_to receive(:spawn)
+              expect(MetricsServer).not_to receive(:fork)
 
               cli.run(%w(foo))
             end
@@ -320,7 +320,7 @@
             end
 
             it 'does not start a sidekiq metrics server' do
-              expect(MetricsServer).not_to receive(:spawn)
+              expect(MetricsServer).not_to receive(:fork)
 
               cli.run(%w(foo))
             end
@@ -350,7 +350,7 @@
             end
 
             it 'does not start a sidekiq metrics server' do
-              expect(MetricsServer).not_to receive(:spawn)
+              expect(MetricsServer).not_to receive(:fork)
 
               cli.run(%w(foo))
             end
@@ -376,7 +376,7 @@
             end
 
             it 'does not start a sidekiq metrics server' do
-              expect(MetricsServer).not_to receive(:spawn)
+              expect(MetricsServer).not_to receive(:fork)
 
               cli.run(%w(foo))
             end
@@ -406,9 +406,9 @@
 
               specify do
                 if start_metrics_server
-                  expect(MetricsServer).to receive(:spawn).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, trapped_signals: trapped_signals)
+                  expect(MetricsServer).to receive(:fork).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, reset_signals: trapped_signals)
                 else
-                  expect(MetricsServer).not_to receive(:spawn)
+                  expect(MetricsServer).not_to receive(:fork)
                 end
 
                 cli.run(%w(foo))
@@ -421,7 +421,7 @@
           let(:sidekiq_exporter_enabled) { true }
 
           it 'does not start the server' do
-            expect(MetricsServer).not_to receive(:spawn)
+            expect(MetricsServer).not_to receive(:fork)
 
             cli.run(%w(foo --dryrun))
           end
@@ -434,7 +434,7 @@
 
         before do
           allow(cli).to receive(:sleep).with(a_kind_of(Numeric))
-          allow(MetricsServer).to receive(:spawn).and_return(99)
+          allow(MetricsServer).to receive(:fork).and_return(99)
           cli.start_metrics_server
         end
 
@@ -453,8 +453,8 @@
           allow(Gitlab::ProcessManagement).to receive(:all_alive?).with(an_instance_of(Array)).and_return(false)
           allow(cli).to receive(:stop_metrics_server)
 
-          expect(MetricsServer).to receive(:spawn).with(
-            'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, trapped_signals: trapped_signals
+          expect(MetricsServer).to receive(:fork).with(
+            'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, reset_signals: trapped_signals
           )
 
           cli.start_loop
diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb
index c90fd162814f49f4e2a2fb35f3a9839e6137decd..860a3299d853c9727c737e2331aab1e676ee1537 100644
--- a/spec/metrics_server/metrics_server_spec.rb
+++ b/spec/metrics_server/metrics_server_spec.rb
@@ -36,13 +36,13 @@
 
   %w(puma sidekiq).each do |target|
     context "when targeting #{target}" do
-      describe '.spawn' do
+      describe '.fork' do
         context 'when in parent process' do
           it 'forks into a new process and detaches it' do
             expect(Process).to receive(:fork).and_return(99)
             expect(Process).to receive(:detach).with(99)
 
-            described_class.spawn(target, metrics_dir: metrics_dir)
+            described_class.fork(target, metrics_dir: metrics_dir)
           end
         end
 
@@ -58,13 +58,47 @@
               expect(server).to receive(:start)
             end
 
-            described_class.spawn(target, metrics_dir: metrics_dir)
+            described_class.fork(target, metrics_dir: metrics_dir)
           end
 
           it 'resets signal handlers from parent process' do
             expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
 
-            described_class.spawn(target, metrics_dir: metrics_dir, trapped_signals: %i[A B])
+            described_class.fork(target, metrics_dir: metrics_dir, reset_signals: %i[A B])
+          end
+        end
+      end
+
+      describe '.spawn' do
+        let(:expected_env) do
+          {
+            'METRICS_SERVER_TARGET' => target,
+            'WIPE_METRICS_DIR' => '0'
+          }
+        end
+
+        it 'spawns a new server process and returns its PID' do
+          expect(Process).to receive(:spawn).with(
+            expected_env,
+            end_with('bin/metrics-server'),
+            hash_including(pgroup: true)
+          ).and_return(99)
+          expect(Process).to receive(:detach).with(99)
+
+          pid = described_class.spawn(target, metrics_dir: metrics_dir)
+
+          expect(pid).to eq(99)
+        end
+
+        context 'when path to gitlab.yml is passed' do
+          it 'sets the GITLAB_CONFIG environment variable' do
+            expect(Process).to receive(:spawn).with(
+              expected_env.merge('GITLAB_CONFIG' => 'path/to/config/gitlab.yml'),
+              end_with('bin/metrics-server'),
+              hash_including(pgroup: true)
+            ).and_return(99)
+
+            described_class.spawn(target, metrics_dir: metrics_dir, gitlab_config: 'path/to/config/gitlab.yml')
           end
         end
       end
@@ -72,6 +106,14 @@
   end
 
   context 'when targeting invalid target' do
+    describe '.fork' do
+      it 'raises an error' do
+        expect { described_class.fork('unsupported', metrics_dir: metrics_dir) }.to(
+          raise_error('Target must be one of [puma,sidekiq]')
+        )
+      end
+    end
+
     describe '.spawn' do
       it 'raises an error' do
         expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
@@ -81,64 +123,86 @@
     end
   end
 
-  describe '#start' do
-    let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) }
-    let(:exporter_double) { double('fake_exporter', start: true) }
-    let(:settings) { { "fake_exporter" => { "enabled" => true } } }
+  shared_examples 'a metrics exporter' do |target, expected_name|
+    describe '#start' do
+      let(:exporter_double) { double('exporter', start: true) }
+      let(:wipe_metrics_dir) { true }
 
-    subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
+      subject(:metrics_server) { described_class.new(target, metrics_dir, wipe_metrics_dir) }
 
-    before do
-      stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class)
-      expect(exporter_class).to receive(:instance).with(
-        settings['fake_exporter'], gc_requests: true, synchronous: true
-      ).and_return(exporter_double)
-      expect(Settings).to receive(:monitoring).and_return(settings)
-    end
+      it 'configures ::Prometheus::Client' do
+        metrics_server.start
 
-    it 'configures ::Prometheus::Client' do
-      metrics_server.start
+        expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
+        expect(::Prometheus::Client.configuration.pid_provider.call).to eq expected_name
+      end
 
-      expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
-      expect(::Prometheus::Client.configuration.pid_provider.call).to eq 'fake_exporter'
-    end
+      it 'ensures that metrics directory exists in correct mode (0700)' do
+        expect(FileUtils).to receive(:mkdir_p).with(metrics_dir, mode: 0700)
 
-    it 'ensures that metrics directory exists in correct mode (0700)' do
-      expect(FileUtils).to receive(:mkdir_p).with(metrics_dir, mode: 0700)
+        metrics_server.start
+      end
 
-      metrics_server.start
-    end
+      context 'when wipe_metrics_dir is true' do
+        it 'removes any old metrics files' do
+          FileUtils.touch("#{metrics_dir}/remove_this.db")
 
-    context 'when wipe_metrics_dir is true' do
-      subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
+          expect { metrics_server.start }.to change { Dir.empty?(metrics_dir) }.from(false).to(true)
+        end
+      end
 
-      it 'removes any old metrics files' do
-        FileUtils.touch("#{metrics_dir}/remove_this.db")
+      context 'when wipe_metrics_dir is false' do
+        let(:wipe_metrics_dir) { false }
 
-        expect { metrics_server.start }.to change { Dir.empty?(metrics_dir) }.from(false).to(true)
+        it 'does not remove any old metrics files' do
+          FileUtils.touch("#{metrics_dir}/remove_this.db")
+
+          expect { metrics_server.start }.not_to change { Dir.empty?(metrics_dir) }.from(false)
+        end
       end
-    end
 
-    context 'when wipe_metrics_dir is false' do
-      subject(:metrics_server) { described_class.new('fake', metrics_dir, false)}
+      it 'starts a metrics server' do
+        expect(exporter_double).to receive(:start)
+
+        metrics_server.start
+      end
 
-      it 'does not remove any old metrics files' do
-        FileUtils.touch("#{metrics_dir}/remove_this.db")
+      it 'starts a RubySampler instance' do
+        expect(ruby_sampler_double).to receive(:start)
 
-        expect { metrics_server.start }.not_to change { Dir.empty?(metrics_dir) }.from(false)
+        subject.start
       end
     end
 
-    it 'starts a metrics server' do
-      expect(exporter_double).to receive(:start)
+    describe '#name' do
+      let(:exporter_double) { double('exporter', start: true) }
 
-      metrics_server.start
+      subject(:name) { described_class.new(target, metrics_dir, true).name }
+
+      it { is_expected.to eq(expected_name) }
     end
+  end
 
-    it 'starts a RubySampler instance' do
-      expect(ruby_sampler_double).to receive(:start)
+  context 'for puma' do
+    before do
+      allow(Gitlab::Metrics::Exporter::WebExporter).to receive(:instance).with(
+        gc_requests: true, synchronous: true
+      ).and_return(exporter_double)
+    end
 
-      subject.start
+    it_behaves_like 'a metrics exporter', 'puma', 'web_exporter'
+  end
+
+  context 'for sidekiq' do
+    let(:settings) { { "sidekiq_exporter" => { "enabled" => true } } }
+
+    before do
+      allow(::Settings).to receive(:monitoring).and_return(settings)
+      allow(Gitlab::Metrics::Exporter::SidekiqExporter).to receive(:instance).with(
+        settings['sidekiq_exporter'], gc_requests: true, synchronous: true
+      ).and_return(exporter_double)
     end
+
+    it_behaves_like 'a metrics exporter', 'sidekiq', 'sidekiq_exporter'
   end
 end