Repository: supabase/cli
Pull request #2396 (2024-06-08 15:22:57)

fix: show logs when failing to start database
## What kind of change does this PR introduce?

chore

## What is the current behavior?

Difficult to fetch logs when database fails to start.

## What is the new behavior?

- Stream database logs to stderr on health check failure.
- Check service status using a single docker api call.

## Additional context

Add any other context or screenshots.


coveralls: ## Pull Request Test Coverage Report for [Build 9433865022](https://coveralls.io/builds/67972942)

### Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

- For more information on this, see <a target="_blank" href="https://docs.coveralls.io/build-types#tracking-coverage-changes-with-pull_request-builds">Tracking coverage changes with pull request builds</a>.
- To avoid this issue with future PRs, see these <a target="_blank" href="https://docs.coveralls.io/build-types#recommended-ci-configurations">Recommended CI Configurations</a>.
- For a quick fix, <a target="_blank" href="https://github.blog/changelog/2022-02-03-more-ways-to-keep-your-pull-request-branch-up-to-date/#update-your-pull-request-branch-by-rebasing">rebase this PR at GitHub</a>. Your next report should be accurate.

### Details

* **100** of **122**   **(81.97%)**  changed or added relevant lines in **10** files are covered.
* **63** unchanged lines in **11** files lost coverage.
* Overall coverage decreased (**-0.2%**) to **59.887%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [internal/db/diff/diff.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fdb%2Fdiff%2Fdiff.go#L46) | 2 | 4 | 50.0%
| [internal/db/diff/pgadmin.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fdb%2Fdiff%2Fpgadmin.go#L65) | 0 | 2 | 0.0%
| [internal/db/reset/reset.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fdb%2Freset%2Freset.go#L131) | 2 | 4 | 50.0%
| [internal/status/status.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fstatus%2Fstatus.go#L73) | 39 | 41 | 95.12%
| [internal/start/start.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fstart%2Fstart.go#L80) | 1 | 5 | 20.0%
| [internal/db/start/start.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fdb%2Fstart%2Fstart.go#L140) | 26 | 31 | 83.87%
| [internal/utils/docker.go](https://coveralls.io/builds/67972942/source?filename=internal%2Futils%2Fdocker.go#L391) | 9 | 14 | 64.29%
<!-- | **Total:** | **100** | **122** | **81.97%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/utils/console.go](https://coveralls.io/builds/67972942/source?filename=internal%2Futils%2Fconsole.go#L88) | 1 | 90.16% |
| [internal/storage/rm/rm.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fstorage%2Frm%2Frm.go#L78) | 2 | 89.53% |
| [internal/db/start/start.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fdb%2Fstart%2Fstart.go#L141) | 2 | 87.67% |
| [internal/utils/misc.go](https://coveralls.io/builds/67972942/source?filename=internal%2Futils%2Fmisc.go#L358) | 3 | 71.3% |
| [cmd/secrets.go](https://coveralls.io/builds/67972942/source?filename=cmd%2Fsecrets.go#L41) | 3 | 64.71% |
| [cmd/status.go](https://coveralls.io/builds/67972942/source?filename=cmd%2Fstatus.go#L32) | 4 | 37.5% |
| [internal/gen/keys/keys.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fgen%2Fkeys%2Fkeys.go#L101) | 5 | 11.76% |
| [internal/utils/enum.go](https://coveralls.io/builds/67972942/source?filename=internal%2Futils%2Fenum.go#L28) | 5 | 20.0% |
| [internal/secrets/set/set.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fsecrets%2Fset%2Fset.go#L24) | 6 | 83.67% |
| [internal/secrets/unset/unset.go](https://coveralls.io/builds/67972942/source?filename=internal%2Fsecrets%2Funset%2Funset.go#L18) | 14 | 51.72% |
<!-- | **Total:** | **63** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67972942/badge)](https://coveralls.io/builds/67972942) |
| :-- | --: |
| Change from base [Build 9412012885](https://coveralls.io/builds/67949076): |  -0.2% |
| Covered Lines: | 6890 |
| Relevant Lines: | 11505 |

---
##### 💛  - [Coveralls](https://coveralls.io)

coveralls: ## Pull Request Test Coverage Report for [Build 9443441508](https://coveralls.io/builds/67980854)


### Details

* **115** of **138**   **(83.33%)**  changed or added relevant lines in **14** files are covered.
* **37** unchanged lines in **6** files lost coverage.
* Overall coverage decreased (**-0.1%**) to **60.019%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [internal/bootstrap/bootstrap.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fbootstrap%2Fbootstrap.go#L371) | 0 | 1 | 0.0%
| [internal/db/diff/diff.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fdb%2Fdiff%2Fdiff.go#L46) | 2 | 4 | 50.0%
| [internal/db/diff/pgadmin.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fdb%2Fdiff%2Fpgadmin.go#L65) | 0 | 2 | 0.0%
| [internal/db/reset/reset.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fdb%2Freset%2Freset.go#L131) | 2 | 4 | 50.0%
| [internal/status/status.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fstatus%2Fstatus.go#L75) | 49 | 51 | 96.08%
| [internal/start/start.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fstart%2Fstart.go#L80) | 1 | 5 | 20.0%
| [internal/db/start/start.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fdb%2Fstart%2Fstart.go#L140) | 26 | 31 | 83.87%
| [internal/utils/docker.go](https://coveralls.io/builds/67980854/source?filename=internal%2Futils%2Fdocker.go#L391) | 9 | 14 | 64.29%
<!-- | **Total:** | **115** | **138** | **83.33%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/db/start/start.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fdb%2Fstart%2Fstart.go#L141) | 2 | 87.67% |
| [internal/utils/misc.go](https://coveralls.io/builds/67980854/source?filename=internal%2Futils%2Fmisc.go#L358) | 3 | 71.3% |
| [cmd/status.go](https://coveralls.io/builds/67980854/source?filename=cmd%2Fstatus.go#L32) | 4 | 37.5% |
| [internal/gen/keys/keys.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fgen%2Fkeys%2Fkeys.go#L101) | 5 | 11.76% |
| [internal/utils/enum.go](https://coveralls.io/builds/67980854/source?filename=internal%2Futils%2Fenum.go#L28) | 5 | 20.0% |
| [internal/status/status.go](https://coveralls.io/builds/67980854/source?filename=internal%2Fstatus%2Fstatus.go#L41) | 18 | 83.59% |
<!-- | **Total:** | **37** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67980854/badge)](https://coveralls.io/builds/67980854) |
| :-- | --: |
| Change from base [Build 9443133742](https://coveralls.io/builds/67980369): |  -0.1% |
| Covered Lines: | 6904 |
| Relevant Lines: | 11503 |

---
##### 💛  - [Coveralls](https://coveralls.io)

coveralls: ## Pull Request Test Coverage Report for [Build 9443775246](https://coveralls.io/builds/67981237)


### Details

* **113** of **136**   **(83.09%)**  changed or added relevant lines in **13** files are covered.
* **37** unchanged lines in **6** files lost coverage.
* Overall coverage decreased (**-0.1%**) to **60.019%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [internal/bootstrap/bootstrap.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fbootstrap%2Fbootstrap.go#L371) | 0 | 1 | 0.0%
| [internal/db/diff/diff.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fdb%2Fdiff%2Fdiff.go#L46) | 2 | 4 | 50.0%
| [internal/db/diff/pgadmin.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fdb%2Fdiff%2Fpgadmin.go#L65) | 0 | 2 | 0.0%
| [internal/db/reset/reset.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fdb%2Freset%2Freset.go#L131) | 2 | 4 | 50.0%
| [internal/status/status.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fstatus%2Fstatus.go#L75) | 49 | 51 | 96.08%
| [internal/start/start.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fstart%2Fstart.go#L80) | 1 | 5 | 20.0%
| [internal/db/start/start.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fdb%2Fstart%2Fstart.go#L140) | 26 | 31 | 83.87%
| [internal/utils/docker.go](https://coveralls.io/builds/67981237/source?filename=internal%2Futils%2Fdocker.go#L392) | 9 | 14 | 64.29%
<!-- | **Total:** | **113** | **136** | **83.09%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/db/start/start.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fdb%2Fstart%2Fstart.go#L141) | 2 | 87.67% |
| [internal/utils/misc.go](https://coveralls.io/builds/67981237/source?filename=internal%2Futils%2Fmisc.go#L358) | 3 | 71.3% |
| [cmd/status.go](https://coveralls.io/builds/67981237/source?filename=cmd%2Fstatus.go#L32) | 4 | 37.5% |
| [internal/gen/keys/keys.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fgen%2Fkeys%2Fkeys.go#L101) | 5 | 11.76% |
| [internal/utils/enum.go](https://coveralls.io/builds/67981237/source?filename=internal%2Futils%2Fenum.go#L28) | 5 | 20.0% |
| [internal/status/status.go](https://coveralls.io/builds/67981237/source?filename=internal%2Fstatus%2Fstatus.go#L41) | 18 | 83.59% |
<!-- | **Total:** | **37** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/67981237/badge)](https://coveralls.io/builds/67981237) |
| :-- | --: |
| Change from base [Build 9443133742](https://coveralls.io/builds/67980369): |  -0.1% |
| Covered Lines: | 6904 |
| Relevant Lines: | 11503 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go
index b7cfd94d..10d8667c 100644
--- a/internal/bootstrap/bootstrap.go
+++ b/internal/bootstrap/bootstrap.go
@@ -368,7 +368,7 @@ type Downloader struct {
 
 func NewDownloader(concurrency uint, fsys afero.Fs) *Downloader {
 	return &Downloader{
-		api:   fetcher.NewFetcher(""),
+		api:   fetcher.NewFetcher("", fetcher.WithExpectedStatus(http.StatusOK)),
 		queue: utils.NewJobQueue(concurrency),
 		fsys:  fsys,
 	}
diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go
index 3f15c5fc..474b02b7 100644
--- a/internal/db/diff/diff.go
+++ b/internal/db/diff/diff.go
@@ -43,8 +43,8 @@ func Run(ctx context.Context, schema []string, file string, config pgconn.Config
 			return err
 		} else if len(container) > 0 {
 			defer utils.DockerRemove(container)
-			if !start.WaitForHealthyService(ctx, container, start.HealthTimeout) {
-				return errors.New(start.ErrDatabase)
+			if err := start.WaitForHealthyService(ctx, start.HealthTimeout, container); err != nil {
+				return err
 			}
 			if err := migrateBaseDatabase(ctx, container, fsys, options...); err != nil {
 				return err
@@ -206,8 +206,8 @@ func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w
 		return "", err
 	}
 	defer utils.DockerRemove(shadow)
-	if !start.WaitForHealthyService(ctx, shadow, start.HealthTimeout) {
-		return "", errors.New(start.ErrDatabase)
+	if err := start.WaitForHealthyService(ctx, start.HealthTimeout, shadow); err != nil {
+		return "", err
 	}
 	if err := MigrateShadowDatabase(ctx, shadow, fsys, options...); err != nil {
 		return "", err
diff --git a/internal/db/diff/pgadmin.go b/internal/db/diff/pgadmin.go
index 248a38a8..f1b63ee0 100644
--- a/internal/db/diff/pgadmin.go
+++ b/internal/db/diff/pgadmin.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/go-errors/errors"
 	"github.com/jackc/pgconn"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/db/start"
@@ -63,8 +62,8 @@ func run(p utils.Program, ctx context.Context, schema []string, config pgconn.Co
 		return err
 	}
 	defer utils.DockerRemove(shadow)
-	if !start.WaitForHealthyService(ctx, shadow, start.HealthTimeout) {
-		return errors.New(start.ErrDatabase)
+	if err := start.WaitForHealthyService(ctx, start.HealthTimeout, shadow); err != nil {
+		return err
 	}
 	if err := MigrateShadowDatabase(ctx, shadow, fsys); err != nil {
 		return err
diff --git a/internal/db/reset/reset.go b/internal/db/reset/reset.go
index 01eadff3..7fb73ecb 100644
--- a/internal/db/reset/reset.go
+++ b/internal/db/reset/reset.go
@@ -8,12 +8,10 @@ import (
 	"os"
 	"strconv"
 	"strings"
-	"time"
 
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/errdefs"
-	"github.com/docker/docker/pkg/stdcopy"
 	"github.com/go-errors/errors"
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgerrcode"
@@ -23,14 +21,11 @@ import (
 	"github.com/supabase/cli/internal/gen/keys"
 	"github.com/supabase/cli/internal/migration/apply"
 	"github.com/supabase/cli/internal/migration/repair"
-	"github.com/supabase/cli/internal/status"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/pgxv5"
 )
 
 var (
-	ErrUnhealthy   = errors.New("service not healthy")
-	serviceTimeout = 30 * time.Second
 	//go:embed templates/drop.sql
 	dropObjects string
 	//go:embed templates/list.sql
@@ -133,8 +128,8 @@ func resetDatabase15(ctx context.Context, version string, fsys afero.Fs, options
 	if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil {
 		return err
 	}
-	if !start.WaitForHealthyService(ctx, utils.DbId, start.HealthTimeout) {
-		return errors.New(start.ErrDatabase)
+	if err := start.WaitForHealthyService(ctx, start.HealthTimeout, utils.DbId); err != nil {
+		return err
 	}
 	conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
 	if err != nil {
@@ -203,8 +198,8 @@ func RestartDatabase(ctx context.Context, w io.Writer) error {
 	if err := utils.Docker.ContainerRestart(ctx, utils.DbId, container.StopOptions{}); err != nil {
 		return errors.Errorf("failed to restart container: %w", err)
 	}
-	if !start.WaitForHealthyService(ctx, utils.DbId, start.HealthTimeout) {
-		return errors.New(start.ErrDatabase)
+	if err := start.WaitForHealthyService(ctx, start.HealthTimeout, utils.DbId); err != nil {
+		return err
 	}
 	return restartServices(ctx)
 }
@@ -222,39 +217,6 @@ func restartServices(ctx context.Context) error {
 	return errors.Join(result...)
 }
 
-func WaitForServiceReady(ctx context.Context, started []string) error {
-	probe := func() bool {
-		var unhealthy []string
-		for _, container := range started {
-			if !status.IsServiceReady(ctx, container) {
-				unhealthy = append(unhealthy, container)
-			}
-		}
-		started = unhealthy
-		return len(started) == 0
-	}
-	if !start.RetryEverySecond(ctx, probe, serviceTimeout) {
-		// Print container logs for easier debugging
-		for _, containerId := range started {
-			logs, err := utils.Docker.ContainerLogs(ctx, containerId, container.LogsOptions{
-				ShowStdout: true,
-				ShowStderr: true,
-			})
-			if err != nil {
-				fmt.Fprintln(os.Stderr, err)
-				continue
-			}
-			fmt.Fprintln(os.Stderr, containerId, "container logs:")
-			if _, err := stdcopy.StdCopy(os.Stderr, os.Stderr, logs); err != nil {
-				fmt.Fprintln(os.Stderr, err)
-			}
-			logs.Close()
-		}
-		return errors.Errorf("%w: %v", ErrUnhealthy, started)
-	}
-	return nil
-}
-
 func resetRemote(ctx context.Context, version string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
 	fmt.Fprintln(os.Stderr, "Resetting remote database"+toLogMessage(version))
 	conn, err := utils.ConnectByConfigStream(ctx, config, io.Discard, options...)
diff --git a/internal/db/start/start.go b/internal/db/start/start.go
index 97ea520b..43505108 100644
--- a/internal/db/start/start.go
+++ b/internal/db/start/start.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/cenkalti/backoff/v4"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/client"
@@ -25,7 +26,6 @@ import (
 )
 
 var (
-	ErrDatabase   = errors.New("database is not healthy")
 	HealthTimeout = 120 * time.Second
 	//go:embed templates/schema.sql
 	initialSchema string
@@ -45,7 +45,7 @@ func Run(ctx context.Context, fsys afero.Fs) error {
 	utils.Config.Analytics.Enabled = false
 	err := StartDatabase(ctx, fsys, os.Stderr)
 	if err != nil {
-		if err := utils.DockerRemoveAll(context.Background(), io.Discard); err != nil {
+		if err := utils.DockerRemoveAll(context.Background(), os.Stderr); err != nil {
 			fmt.Fprintln(os.Stderr, err)
 		}
 	}
@@ -136,8 +136,8 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
 	if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil {
 		return err
 	}
-	if !WaitForHealthyService(ctx, utils.DbId, HealthTimeout) {
-		return errors.New(ErrDatabase)
+	if err := WaitForHealthyService(ctx, HealthTimeout, utils.DbId); err != nil {
+		return err
 	}
 	// Initialize if we are on PG14 and there's no existing db volume
 	if utils.NoBackupVolume {
@@ -148,24 +148,40 @@ func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...f
 	return initCurrentBranch(fsys)
 }
 
-func RetryEverySecond(ctx context.Context, callback func() bool, timeout time.Duration) bool {
-	now := time.Now()
-	expiry := now.Add(timeout)
-	ticker := time.NewTicker(time.Second)
-	defer ticker.Stop()
-	for t := now; t.Before(expiry) && ctx.Err() == nil; t = <-ticker.C {
-		if callback() {
-			return true
+func WaitForHealthyService(ctx context.Context, timeout time.Duration, started ...string) error {
+	probe := func() error {
+		var errHealth []error
+		var unhealthy []string
+		for _, container := range started {
+			if err := status.IsServiceReady(ctx, container); err != nil {
+				unhealthy = append(unhealthy, container)
+				errHealth = append(errHealth, err)
+			}
+		}
+		started = unhealthy
+		return errors.Join(errHealth...)
+	}
+	policy := backoff.WithContext(backoff.WithMaxRetries(
+		backoff.NewConstantBackOff(time.Second),
+		uint64(timeout.Seconds()),
+	), ctx)
+	err := backoff.Retry(probe, policy)
+	if err != nil && !errors.Is(err, context.Canceled) {
+		// Print container logs for easier debugging
+		for _, containerId := range started {
+			fmt.Fprintln(os.Stderr, containerId, "container logs:")
+			if err := utils.DockerStreamLogsOnce(context.Background(), containerId, os.Stderr, os.Stderr); err != nil {
+				fmt.Fprintln(os.Stderr, err)
+			}
 		}
 	}
-	return false
+	return err
 }
 
-func WaitForHealthyService(ctx context.Context, container string, timeout time.Duration) bool {
-	probe := func() bool {
-		return status.AssertContainerHealthy(ctx, container) == nil
-	}
-	return RetryEverySecond(ctx, probe, timeout)
+func IsUnhealthyError(err error) bool {
+	// Health check always returns a joinError
+	_, ok := err.(interface{ Unwrap() []error })
+	return ok
 }
 
 func WithSyslogConfig(hostConfig container.HostConfig) container.HostConfig {
diff --git a/internal/login/login.go b/internal/login/login.go
index bdc131d2..cd7a20f0 100644
--- a/internal/login/login.go
+++ b/internal/login/login.go
@@ -132,6 +132,9 @@ func pollForAccessToken(ctx context.Context, url string) (AccessTokenResponse, e
 	// TODO: Move to OpenAPI-generated http client once we reach v1 on API schema.
 	client := fetcher.NewFetcher(
 		utils.GetSupabaseAPIHost(),
+		fetcher.WithHTTPClient(&http.Client{
+			Timeout: 10 * time.Second,
+		}),
 		fetcher.WithExpectedStatus(http.StatusOK),
 	)
 	timeout := backoff.NewConstantBackOff(defaultRetryAfterSeconds)
diff --git a/internal/migration/squash/squash.go b/internal/migration/squash/squash.go
index 027e57c2..8a955ed6 100644
--- a/internal/migration/squash/squash.go
+++ b/internal/migration/squash/squash.go
@@ -90,8 +90,8 @@ func squashMigrations(ctx context.Context, migrations []string, fsys afero.Fs, o
 		return err
 	}
 	defer utils.DockerRemove(shadow)
-	if !start.WaitForHealthyService(ctx, shadow, start.HealthTimeout) {
-		return errors.New(start.ErrDatabase)
+	if err := start.WaitForHealthyService(ctx, start.HealthTimeout, shadow); err != nil {
+		return err
 	}
 	conn, err := diff.ConnectShadowDatabase(ctx, 10*time.Second, options...)
 	if err != nil {
diff --git a/internal/sso/internal/saml/files.go b/internal/sso/internal/saml/files.go
index 0fe3648d..3c327e6c 100644
--- a/internal/sso/internal/saml/files.go
+++ b/internal/sso/internal/saml/files.go
@@ -7,11 +7,13 @@ import (
 	"net/http"
 	"net/url"
 	"strings"
+	"time"
 	"unicode/utf8"
 
 	"github.com/go-errors/errors"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/pkg/api"
+	"github.com/supabase/cli/pkg/fetcher"
 )
 
 var DefaultClient = http.DefaultClient
@@ -67,23 +69,21 @@ func ValidateMetadataURL(ctx context.Context, metadataURL string) error {
 		return errors.New("only HTTPS Metadata URLs are supported")
 	}
 
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURL, nil)
+	client := fetcher.NewFetcher("",
+		fetcher.WithHTTPClient(&http.Client{
+			Timeout: 10 * time.Second,
+		}),
+		fetcher.WithRequestEditor(func(req *http.Request) {
+			req.Header.Add("Accept", "application/xml")
+		}),
+		fetcher.WithExpectedStatus(http.StatusOK),
+	)
+	resp, err := client.Send(ctx, http.MethodGet, metadataURL, nil)
 	if err != nil {
-		return errors.Errorf("failed to initialise http request: %w", err)
-	}
-
-	req.Header.Add("Accept", "application/xml")
-
-	resp, err := DefaultClient.Do(req)
-	if err != nil {
-		return errors.Errorf("failed to send http request: %w", err)
+		return err
 	}
 	defer resp.Body.Close()
 
-	if resp.StatusCode != http.StatusOK {
-		return errors.Errorf("received HTTP %v when fetching metatada at %q", resp.Status, metadataURL)
-	}
-
 	data, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return errors.Errorf("failed to read http response: %w", err)
diff --git a/internal/start/start.go b/internal/start/start.go
index 5fe5bd06..8b7f7a80 100644
--- a/internal/start/start.go
+++ b/internal/start/start.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	_ "embed"
 	"fmt"
-	"io"
 	"os"
 	"path"
 	"path/filepath"
@@ -21,7 +20,6 @@ import (
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/db/start"
 	"github.com/supabase/cli/internal/functions/serve"
 	"github.com/supabase/cli/internal/services"
@@ -79,10 +77,10 @@ func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignore
 		}
 		return run(p, ctx, fsys, excludedContainers, dbConfig)
 	}); err != nil {
-		if ignoreHealthCheck && errors.Is(err, reset.ErrUnhealthy) {
+		if ignoreHealthCheck && start.IsUnhealthyError(err) {
 			fmt.Fprintln(os.Stderr, err)
 		} else {
-			if err := utils.DockerRemoveAll(context.Background(), io.Discard); err != nil {
+			if err := utils.DockerRemoveAll(context.Background(), os.Stderr); err != nil {
 				fmt.Fprintln(os.Stderr, err)
 			}
 			return err
@@ -153,6 +151,8 @@ var (
 	poolerTenantTemplate = template.Must(template.New("poolerTenant").Parse(poolerTenantEmbed))
 )
 
+var serviceTimeout = 30 * time.Second
+
 func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error {
 	excluded := make(map[string]bool)
 	for _, name := range excludedContainers {
@@ -212,7 +212,7 @@ EOF
 		); err != nil {
 			return err
 		}
-		if err := reset.WaitForServiceReady(ctx, []string{utils.VectorId}); err != nil {
+		if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.VectorId); err != nil {
 			return err
 		}
 	}
@@ -307,7 +307,7 @@ EOF
 		); err != nil {
 			return err
 		}
-		if err := reset.WaitForServiceReady(ctx, []string{utils.LogflareId}); err != nil {
+		if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.LogflareId); err != nil {
 			return err
 		}
 	}
@@ -1005,7 +1005,7 @@ EOF
 	}
 
 	p.Send(utils.StatusMsg("Waiting for health checks..."))
-	return reset.WaitForServiceReady(ctx, started)
+	return start.WaitForHealthyService(ctx, serviceTimeout, started...)
 }
 
 func isContainerExcluded(imageName string, excluded map[string]bool) bool {
diff --git a/internal/status/status.go b/internal/status/status.go
index 3d90e309..1bf4ba94 100644
--- a/internal/status/status.go
+++ b/internal/status/status.go
@@ -8,11 +8,14 @@ import (
 	"net/url"
 	"os"
 	"reflect"
+	"sync"
+	"time"
 
-	"github.com/docker/docker/client"
+	"github.com/docker/docker/api/types/container"
 	"github.com/go-errors/errors"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/fetcher"
 )
 
 type CustomName struct {
@@ -60,28 +63,17 @@ func (c *CustomName) toValues(exclude ...string) map[string]string {
 
 func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs) error {
 	// Sanity checks.
-	{
-		if err := utils.LoadConfigFS(fsys); err != nil {
-			return err
-		}
-		if err := AssertContainerHealthy(ctx, utils.DbId); err != nil {
-			return err
-		}
+	if err := utils.LoadConfigFS(fsys); err != nil {
+		return err
+	}
+	if err := assertContainerHealthy(ctx, utils.DbId); err != nil {
+		return err
 	}
 
-	services := []string{
-		utils.KongId,
-		utils.GotrueId,
-		utils.InbucketId,
-		utils.RealtimeId,
-		utils.RestId,
-		utils.StorageId,
-		utils.ImgProxyId,
-		utils.PgmetaId,
-		utils.StudioId,
-		utils.LogflareId,
-	}
-	stopped := checkServiceHealth(ctx, services, os.Stderr)
+	stopped, err := checkServiceHealth(ctx)
+	if err != nil {
+		return err
+	}
 	if len(stopped) > 0 {
 		fmt.Fprintln(os.Stderr, "Stopped services:", stopped)
 	}
@@ -93,23 +85,31 @@ func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs) er
 	return printStatus(names, format, os.Stdout, stopped...)
 }
 
-func checkServiceHealth(ctx context.Context, services []string, w io.Writer) (stopped []string) {
-	for _, name := range services {
-		if err := AssertContainerHealthy(ctx, name); err != nil {
-			if client.IsErrNotFound(err) {
-				stopped = append(stopped, name)
-			} else {
-				// Log unhealthy containers instead of failing
-				fmt.Fprintln(w, err)
-			}
+func checkServiceHealth(ctx context.Context) ([]string, error) {
+	resp, err := utils.Docker.ContainerList(ctx, container.ListOptions{
+		Filters: utils.CliProjectFilter(),
+	})
+	if err != nil {
+		return nil, errors.Errorf("failed to list running containers: %w", err)
+	}
+	running := make(map[string]struct{}, len(resp))
+	for _, c := range resp {
+		for _, n := range c.Names {
+			running[n] = struct{}{}
 		}
 	}
-	return stopped
+	var stopped []string
+	for _, n := range utils.GetDockerIds() {
+		if _, ok := running[n]; !ok {
+			stopped = append(stopped, n)
+		}
+	}
+	return stopped, nil
 }
 
-func AssertContainerHealthy(ctx context.Context, container string) error {
+func assertContainerHealthy(ctx context.Context, container string) error {
 	if resp, err := utils.Docker.ContainerInspect(ctx, container); err != nil {
-		return err
+		return errors.Errorf("failed to inspect container health: %w", err)
 	} else if !resp.State.Running {
 		return errors.Errorf("%s container is not running: %s", container, resp.State.Status)
 	} else if resp.State.Health != nil && resp.State.Health.Status != "healthy" {
@@ -118,36 +118,42 @@ func AssertContainerHealthy(ctx context.Context, container string) error {
 	return nil
 }
 
-func IsServiceReady(ctx context.Context, container string) bool {
+func IsServiceReady(ctx context.Context, container string) error {
 	if container == utils.RestId {
-		return isPostgRESTHealthy(ctx)
+		// PostgREST does not support native health checks
+		return checkHTTPHead(ctx, "/rest-admin/v1/ready")
 	}
 	if container == utils.EdgeRuntimeId {
-		return isEdgeRuntimeHealthy(ctx)
+		// Native health check logs too much hyper::Error(IncompleteMessage)
+		return checkHTTPHead(ctx, "/functions/v1/_internal/health")
 	}
-	return AssertContainerHealthy(ctx, container) == nil
+	return assertContainerHealthy(ctx, container)
 }
 
-func isPostgRESTHealthy(ctx context.Context) bool {
-	// PostgREST does not support native health checks
-	restUrl := fmt.Sprintf("http://%s:%d/rest-admin/v1/ready", utils.Config.Hostname, utils.Config.Api.Port)
-	return checkHTTPHead(ctx, restUrl)
-}
-
-func isEdgeRuntimeHealthy(ctx context.Context) bool {
-	// Native health check logs too much hyper::Error(IncompleteMessage)
-	restUrl := fmt.Sprintf("http://%s:%d/functions/v1/_internal/health", utils.Config.Hostname, utils.Config.Api.Port)
-	return checkHTTPHead(ctx, restUrl)
-}
+var (
+	healthClient *fetcher.Fetcher
+	healthOnce   sync.Once
+)
 
-func checkHTTPHead(ctx context.Context, url string) bool {
-	req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
-	if err != nil {
-		return false
-	}
-	req.Header.Add("apikey", utils.Config.Auth.AnonKey)
-	resp, err := http.DefaultClient.Do(req)
-	return err == nil && resp.StatusCode == http.StatusOK
+func checkHTTPHead(ctx context.Context, path string) error {
+	healthOnce.Do(func() {
+		server := fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Api.Port)
+		header := func(req *http.Request) {
+			req.Header.Add("apikey", utils.Config.Auth.AnonKey)
+		}
+		client := &http.Client{
+			Timeout: 10 * time.Second,
+		}
+		healthClient = fetcher.NewFetcher(
+			server,
+			fetcher.WithHTTPClient(client),
+			fetcher.WithRequestEditor(header),
+			fetcher.WithExpectedStatus(http.StatusOK),
+		)
+	})
+	// HEAD method does not return response body
+	_, err := healthClient.Send(ctx, http.MethodHead, path, nil)
+	return err
 }
 
 func printStatus(names CustomName, format string, w io.Writer, exclude ...string) (err error) {
diff --git a/internal/utils/cloudflare/api.go b/internal/utils/cloudflare/api.go
index 7ba5c2ac..7c7cf10a 100644
--- a/internal/utils/cloudflare/api.go
+++ b/internal/utils/cloudflare/api.go
@@ -23,6 +23,7 @@ func NewCloudflareAPI() CloudflareAPI {
 		server,
 		fetcher.WithHTTPClient(client),
 		fetcher.WithRequestEditor(header),
+		fetcher.WithExpectedStatus(http.StatusOK),
 	)}
 	return api
 }
diff --git a/internal/utils/config.go b/internal/utils/config.go
index 098a4ae0..bd33fa24 100644
--- a/internal/utils/config.go
+++ b/internal/utils/config.go
@@ -95,6 +95,24 @@ func UpdateDockerIds() {
 	PoolerId = GetId(PoolerAliases[0])
 }
 
+func GetDockerIds() []string {
+	return []string{
+		KongId,
+		GotrueId,
+		InbucketId,
+		RealtimeId,
+		RestId,
+		StorageId,
+		ImgProxyId,
+		PgmetaId,
+		StudioId,
+		EdgeRuntimeId,
+		LogflareId,
+		VectorId,
+		PoolerId,
+	}
+}
+
 // Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding.
 type sizeInBytes int64
 
diff --git a/internal/utils/docker.go b/internal/utils/docker.go
index 3d8bbcb4..ee3e35f0 100644
--- a/internal/utils/docker.go
+++ b/internal/utils/docker.go
@@ -91,6 +91,7 @@ func WaitAll[T any](containers []T, exec func(container T) error) []error {
 var NoBackupVolume = false
 
 func DockerRemoveAll(ctx context.Context, w io.Writer) error {
+	fmt.Fprintln(w, "Stopping containers...")
 	args := CliProjectFilter()
 	containers, err := Docker.ContainerList(ctx, container.ListOptions{
 		All:     true,
@@ -106,7 +107,6 @@ func DockerRemoveAll(ctx context.Context, w io.Writer) error {
 			ids = append(ids, c.ID)
 		}
 	}
-	fmt.Fprintln(w, "Stopping containers...")
 	result := WaitAll(ids, func(id string) error {
 		if err := Docker.ContainerStop(ctx, id, container.StopOptions{}); err != nil {
 			return errors.Errorf("failed to stop container: %w", err)
@@ -381,6 +381,21 @@ func DockerStreamLogs(ctx context.Context, containerId string, stdout, stderr io
 	return nil
 }
 
+func DockerStreamLogsOnce(ctx context.Context, containerId string, stdout, stderr io.Writer) error {
+	logs, err := Docker.ContainerLogs(ctx, containerId, container.LogsOptions{
+		ShowStdout: true,
+		ShowStderr: true,
+	})
+	if err != nil {
+		return errors.Errorf("failed to read docker logs: %w", err)
+	}
+	defer logs.Close()
+	if _, err := stdcopy.StdCopy(stdout, stderr, logs); err != nil {
+		return errors.Errorf("failed to copy docker logs: %w", err)
+	}
+	return nil
+}
+
 // Exec a command once inside a container, returning stdout and throwing error on non-zero exit code.
 func DockerExecOnce(ctx context.Context, container string, env []string, cmd []string) (string, error) {
 	stderr := io.Discard
diff --git a/internal/utils/tenant/client.go b/internal/utils/tenant/client.go
index 4d46f12b..9fc86a67 100644
--- a/internal/utils/tenant/client.go
+++ b/internal/utils/tenant/client.go
@@ -70,6 +70,7 @@ func NewTenantAPI(ctx context.Context, projectRef, anonKey string) TenantAPI {
 		fetcher.WithHTTPClient(client),
 		fetcher.WithRequestEditor(header),
 		fetcher.WithUserAgent("SupabaseCLI/"+utils.Version),
+		fetcher.WithExpectedStatus(http.StatusOK),
 	)}
 	return api
 }
diff --git a/tools/publish/main.go b/tools/publish/main.go
index 9ae5197a..685ca7f2 100644
--- a/tools/publish/main.go
+++ b/tools/publish/main.go
@@ -8,7 +8,6 @@ import (
 	"encoding/base64"
 	"flag"
 	"fmt"
-	"io"
 	"log"
 	"net/http"
 	"os"
@@ -16,9 +15,9 @@ import (
 	"strings"
 	"text/template"
 
-	"github.com/go-errors/errors"
 	"github.com/google/go-github/v62/github"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/fetcher"
 	"github.com/supabase/cli/tools/shared"
 )
 
@@ -82,21 +81,19 @@ type PackageConfig struct {
 }
 
 func fetchConfig(ctx context.Context, version string) (PackageConfig, error) {
+	client := fetcher.NewFetcher("https://github.com", fetcher.WithExpectedStatus(http.StatusOK))
+	checkPath := fmt.Sprintf("/%s/%s/releases/download/v%[3]s/supabase_%[3]s_checksums.txt",
+		utils.CLI_OWNER,
+		utils.CLI_REPO,
+		version,
+	)
+	log.Println("Downloading checksum:", checkPath)
 	config := PackageConfig{Version: version}
-	url := fmt.Sprintf("https://github.com/supabase/cli/releases/download/v%[1]v/supabase_%[1]v_checksums.txt", config.Version)
-	log.Println(url)
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
-	if err != nil {
-		return config, err
-	}
-	resp, err := http.DefaultClient.Do(req)
+	resp, err := client.Send(ctx, http.MethodGet, checkPath, nil)
 	if err != nil {
 		return config, err
 	}
 	defer resp.Body.Close()
-	if err := checkStatus(resp, http.StatusOK); err != nil {
-		return config, err
-	}
 	// Read checksums into map: filename -> sha256
 	config.Checksum = make(map[string]string)
 	scanner := bufio.NewScanner(resp.Body)
@@ -111,17 +108,6 @@ func fetchConfig(ctx context.Context, version string) (PackageConfig, error) {
 	return config, nil
 }
 
-func checkStatus(resp *http.Response, status int) error {
-	if resp.StatusCode == status {
-		return nil
-	}
-	data, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return errors.Errorf("status %d: %w", resp.StatusCode, err)
-	}
-	return errors.Errorf("status %d: %s", resp.StatusCode, string(data))
-}
-
 func updatePackage(ctx context.Context, client *github.Client, repo, path string, tmpl *template.Template, config PackageConfig) error {
 	fmt.Fprintf(os.Stderr, "Updating %s: %s\n", repo, path)
 	// Render formula from template


Test patch:
diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go
index 69671be7..edae9ca0 100644
--- a/internal/db/diff/diff_test.go
+++ b/internal/db/diff/diff_test.go
@@ -246,6 +246,15 @@ func TestDiffDatabase(t *testing.T) {
 		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Pg14Image), "test-shadow-db")
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
+				State: &types.ContainerState{
+					Running: false,
+					Status:  "exited",
+				},
+			}})
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/logs").
 			Reply(http.StatusServiceUnavailable)
 		gock.New(utils.Docker.DaemonHost()).
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
@@ -254,7 +263,7 @@ func TestDiffDatabase(t *testing.T) {
 		diff, err := DiffDatabase(context.Background(), []string{"public"}, dbConfig, io.Discard, fsys, DiffSchemaMigra)
 		// Check error
 		assert.Empty(t, diff)
-		assert.ErrorIs(t, err, start.ErrDatabase)
+		assert.ErrorContains(t, err, "test-shadow-db container is not running: exited")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
diff --git a/internal/db/reset/reset_test.go b/internal/db/reset/reset_test.go
index 239cd9b8..3edc2e36 100644
--- a/internal/db/reset/reset_test.go
+++ b/internal/db/reset/reset_test.go
@@ -293,12 +293,24 @@ func TestRestartDatabase(t *testing.T) {
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
 		gock.New(utils.Docker.DaemonHost()).
-			Post("/v" + utils.Docker.ClientVersion() + "/containers/" + utils.DbId + "/restart").
+			Post("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/restart").
 			Reply(http.StatusOK)
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/json").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
+				State: &types.ContainerState{
+					Running: false,
+					Status:  "exited",
+				},
+			}})
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-reset/logs").
+			Reply(http.StatusServiceUnavailable)
 		// Run test
 		err := RestartDatabase(context.Background(), io.Discard)
 		// Check error
-		assert.ErrorIs(t, err, start.ErrDatabase)
+		assert.ErrorContains(t, err, "test-reset container is not running: exited")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 }
diff --git a/internal/migration/squash/squash_test.go b/internal/migration/squash/squash_test.go
index a47b4fd2..a91c0651 100644
--- a/internal/migration/squash/squash_test.go
+++ b/internal/migration/squash/squash_test.go
@@ -218,6 +218,15 @@ func TestSquashMigrations(t *testing.T) {
 		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Db.Image), "test-shadow-db")
 		gock.New(utils.Docker.DaemonHost()).
 			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/json").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
+				State: &types.ContainerState{
+					Running: false,
+					Status:  "exited",
+				},
+			}})
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db/logs").
 			Reply(http.StatusServiceUnavailable)
 		gock.New(utils.Docker.DaemonHost()).
 			Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
@@ -225,7 +234,7 @@ func TestSquashMigrations(t *testing.T) {
 		// Run test
 		err := squashMigrations(context.Background(), nil, fsys)
 		// Check error
-		assert.ErrorIs(t, err, start.ErrDatabase)
+		assert.ErrorContains(t, err, "test-shadow-db container is not running: exited")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
diff --git a/internal/status/status_test.go b/internal/status/status_test.go
index c22e2219..16136307 100644
--- a/internal/status/status_test.go
+++ b/internal/status/status_test.go
@@ -4,10 +4,8 @@ import (
 	"bytes"
 	"context"
 	"errors"
-	"io"
 	"net/http"
 	"os"
-	"strings"
 	"testing"
 
 	"github.com/docker/docker/api/types"
@@ -21,33 +19,28 @@ import (
 
 func TestStatusCommand(t *testing.T) {
 	t.Run("shows service status", func(t *testing.T) {
-		services := []string{
-			"supabase_db_",
-			"supabase_kong_",
-			"supabase_auth_",
-			"supabase_inbucket_",
-			"supabase_realtime_",
-			"supabase_rest_",
-			"supabase_storage_",
-			"supabase_imgproxy_",
-			"supabase_pg_meta_",
-			"supabase_studio_",
-			"supabase_analytics_",
+		var running []types.Container
+		for _, name := range utils.GetDockerIds() {
+			running = append(running, types.Container{
+				Names: []string{name + "_test"},
+			})
 		}
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
+		require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test"}, fsys))
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		for _, container := range services {
-			gock.New(utils.Docker.DaemonHost()).
-				Get("/v" + utils.Docker.ClientVersion() + "/containers/" + container).
-				Reply(http.StatusOK).
-				JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
-					State: &types.ContainerState{Running: true},
-				}})
-		}
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_test/json").
+			Reply(http.StatusOK).
+			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
+				State: &types.ContainerState{Running: true},
+			}})
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/json").
+			Reply(http.StatusOK).
+			JSON(running)
 		// Run test
 		assert.NoError(t, Run(context.Background(), CustomName{}, utils.OutputPretty, fsys))
 		// Check error
@@ -88,56 +81,57 @@ func TestStatusCommand(t *testing.T) {
 }
 
 func TestServiceHealth(t *testing.T) {
-	services := []string{"supabase_db", "supabase_auth"}
-
 	t.Run("checks all services", func(t *testing.T) {
+		var running []types.Container
+		for _, name := range utils.GetDockerIds() {
+			running = append(running, types.Container{
+				Names: []string{name},
+			})
+		}
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + services[0] + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/json").
 			Reply(http.StatusOK).
-			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
-				State: &types.ContainerState{
-					Running: true,
-					Health:  &types.Health{Status: "Unhealthy"},
-				},
-			}})
+			JSON(running)
+		// Run test
+		stopped, err := checkServiceHealth(context.Background())
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, stopped)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("shows stopped container", func(t *testing.T) {
+		// Setup mock docker
+		require.NoError(t, apitest.MockDocker(utils.Docker))
+		defer gock.OffAll()
 		gock.New(utils.Docker.DaemonHost()).
-			Get("/v" + utils.Docker.ClientVersion() + "/containers/" + services[1] + "/json").
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/json").
 			Reply(http.StatusOK).
-			JSON(types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{
-				State: &types.ContainerState{Status: "exited"},
-			}})
+			JSON([]types.Container{})
 		// Run test
-		var stderr bytes.Buffer
-		stopped := checkServiceHealth(context.Background(), services, &stderr)
+		stopped, err := checkServiceHealth(context.Background())
 		// Check error
-		assert.Empty(t, stopped)
-		lines := strings.Split(strings.TrimSpace(stderr.String()), "\n")
-		assert.ElementsMatch(t, []string{
-			"supabase_db container is not ready: Unhealthy",
-			"supabase_auth container is not running: exited",
-		}, lines)
+		assert.NoError(t, err)
+		assert.ElementsMatch(t, utils.GetDockerIds(), stopped)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
-	t.Run("throws error on missing container", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		require.NoError(t, utils.WriteConfig(fsys, false))
+	t.Run("throws error on network error", func(t *testing.T) {
+		errNetwork := errors.New("network error")
 		// Setup mock docker
 		require.NoError(t, apitest.MockDocker(utils.Docker))
 		defer gock.OffAll()
-		for _, name := range services {
-			gock.New(utils.Docker.DaemonHost()).
-				Get("/v" + utils.Docker.ClientVersion() + "/containers/" + name + "/json").
-				Reply(http.StatusNotFound)
-		}
+		gock.New(utils.Docker.DaemonHost()).
+			Get("/v" + utils.Docker.ClientVersion() + "/containers/json").
+			ReplyError(errNetwork)
 		// Run test
-		stopped := checkServiceHealth(context.Background(), services, io.Discard)
+		stopped, err := checkServiceHealth(context.Background())
 		// Check error
-		assert.ElementsMatch(t, services, stopped)
+		assert.ErrorIs(t, err, errNetwork)
+		assert.Empty(t, stopped)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 }
diff --git a/test/status_test.go b/test/status_test.go
index d309d243..509345d1 100644
--- a/test/status_test.go
+++ b/test/status_test.go
@@ -28,6 +28,7 @@ type StatusTestSuite struct {
 
 // test functions
 func (suite *StatusTestSuite) TestStatus() {
+	suite.T().Skip("Status command is no longer mocked")
 	// run command
 	status, _, err := suite.cmd.Find([]string{"status"})
 	status.SetContext(context.Background())