Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions apps/cli-go/internal/db/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,20 @@ func WaitForHealthyService(ctx context.Context, timeout time.Duration, started .
policy := NewBackoffPolicy(ctx, timeout)
err := backoff.Retry(probe, policy)
if err != nil && !errors.Is(err, context.Canceled) {
// Print container logs for easier debugging
// Print container logs for easier debugging and suggest recovery steps.
var logBuf strings.Builder
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)
w := io.MultiWriter(os.Stderr, &logBuf)
if logErr := utils.DockerStreamLogsOnce(context.Background(), containerId, w, w); logErr != nil {
fmt.Fprintln(os.Stderr, logErr)
}
}
if suggestion := SuggestFromUnhealthyLogs(logBuf.String()); len(suggestion) > 0 {
if len(utils.CmdSuggestion) > 0 {
utils.CmdSuggestion += "\n\n" + suggestion
} else {
utils.CmdSuggestion = suggestion
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions apps/cli-go/internal/db/start/suggest_unhealthy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package start

import (
"fmt"
"runtime"
"strings"

"github.com/supabase/cli/internal/utils"
)

// SuggestFromUnhealthyLogs returns a CmdSuggestion for common local-stack
// startup failures seen after image upgrades or wrong-arch pulls
// (see https://github.com/supabase/supabase/issues/48224).
func SuggestFromUnhealthyLogs(logs string) string {
lower := strings.ToLower(logs)
var parts []string

if strings.Contains(lower, "exec format error") {
arch := runtime.GOARCH
parts = append(parts, fmt.Sprintf(
"A container failed with \"exec format error\" (wrong CPU architecture image).\n"+
"Try removing the mismatched images and restarting for linux/%s:\n"+
" %s\n"+
" docker image prune -f\n"+

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove tagged wrong-arch images explicitly

For the cached wrong-architecture image case that produces exec format error, this recovery command does not actually remove the bad image: Docker's image prune docs (https://docs.docker.com/reference/cli/docker/image/prune/) define the default as removing only dangling images unless -a is used, while the image Supabase just started is still tagged and will be reused by DockerResolveImageIfNotCached on the next supabase start. Users who follow the suggestion can therefore hit the same health-check failure again; the hint needs to remove the relevant cached image tag(s) or force a repull.

Useful? React with 👍 / 👎.

" %s",
arch,
utils.Aqua("supabase stop --no-backup"),
utils.Aqua("supabase start"),
))
}

if strings.Contains(lower, "migrations_name_key") ||
(strings.Contains(lower, "migration failed") && strings.Contains(lower, "duplicate key")) {
parts = append(parts, fmt.Sprintf(
"Storage migrations failed against an existing database volume (often after upgrading images while restoring a backup).\n"+
"Reset local data and start fresh:\n"+
" %s\n"+
" %s",
utils.Aqua("supabase stop --no-backup"),
utils.Aqua("supabase start"),
))
}

if strings.Contains(lower, "err_invalid_package_config") ||
strings.Contains(lower, "invalid package config") {
parts = append(parts, fmt.Sprintf(
"Studio image looks corrupted or incomplete. Remove it and re-pull:\n"+
" %s\n"+
" docker image rm -f $(docker images -q supabase/studio) 2>/dev/null\n"+

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the registry-qualified Studio image

When Studio was pulled through the normal registry resolution path, the cached repository is usually public.ecr.aws/supabase/studio first, then ghcr.io/supabase/studio, not supabase/studio; after this command, supabase start can still inspect and reuse the corrupted registry-qualified image, so the ERR_INVALID_PACKAGE_CONFIG recovery suggestion fails for the default install path. Please target the resolved registry candidates or the running container's image ID instead of only the Docker Hub name.

Useful? React with 👍 / 👎.

" %s",
utils.Aqua("supabase stop --no-backup"),
utils.Aqua("supabase start"),
))
}

return strings.Join(parts, "\n\n")
}
51 changes: 51 additions & 0 deletions apps/cli-go/internal/db/start/suggest_unhealthy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package start

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSuggestFromUnhealthyLogs(t *testing.T) {
t.Run("suggests recovery for exec format error", func(t *testing.T) {
suggestion := SuggestFromUnhealthyLogs("exec /mailpit: exec format error\n")
require.NotEmpty(t, suggestion)
assert.Contains(t, suggestion, "exec format error")
assert.Contains(t, suggestion, "supabase stop --no-backup")
assert.Contains(t, suggestion, "supabase start")
assert.Contains(t, suggestion, "linux/"+runtime.GOARCH)
})

t.Run("suggests recovery for storage migrations_name_key", func(t *testing.T) {
logs := `Migration failed. Reason: duplicate key value violates unique constraint "migrations_name_key"`
suggestion := SuggestFromUnhealthyLogs(logs)
require.NotEmpty(t, suggestion)
assert.Contains(t, suggestion, "Storage migrations failed")
assert.Contains(t, suggestion, "supabase stop --no-backup")
assert.Contains(t, suggestion, "supabase start")
})

t.Run("suggests recovery for studio invalid package config", func(t *testing.T) {
logs := "Error: Invalid package config /app/apps/studio/node_modules/next/package.json.\n code: 'ERR_INVALID_PACKAGE_CONFIG'"
suggestion := SuggestFromUnhealthyLogs(logs)
require.NotEmpty(t, suggestion)
assert.Contains(t, suggestion, "Studio")
assert.Contains(t, suggestion, "supabase stop --no-backup")
assert.Contains(t, suggestion, "supabase start")
})

t.Run("combines multiple failure suggestions", func(t *testing.T) {
logs := "exec /mailpit: exec format error\nmigrations_name_key\nERR_INVALID_PACKAGE_CONFIG"
suggestion := SuggestFromUnhealthyLogs(logs)
require.NotEmpty(t, suggestion)
assert.Contains(t, suggestion, "exec format error")
assert.Contains(t, suggestion, "Storage migrations failed")
assert.Contains(t, suggestion, "Studio")
})

t.Run("returns empty for unrelated logs", func(t *testing.T) {
assert.Empty(t, SuggestFromUnhealthyLogs("listening on :5432\nready\n"))
})
}
4 changes: 4 additions & 0 deletions apps/cli-go/internal/utils/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"log"
"os"
"regexp"
"runtime"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -289,6 +290,9 @@ func registryFromImage(imageTag string) string {
func DockerImagePull(ctx context.Context, imageTag string, w io.Writer) error {
out, err := Docker.ImagePull(ctx, imageTag, image.PullOptions{
RegistryAuth: GetRegistryAuthForImage(imageTag),
// Prefer the host architecture so services like mailpit do not start
// with a wrong-arch binary (exec format error). See supabase/supabase#48224.
Platform: "linux/" + runtime.GOARCH,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate the platform pin to compose pulls

This platform pin only applies to DockerImagePull, but a normal supabase start first runs pullImagesUsingCompose and only then ensureImagesCached; the compose path goes through RetryClient.ImagePull with options.Platform left empty. When that best-effort pre-pull succeeds for a missing service image, the later resolver sees the tag cached and never reaches this pinned pull, so the wrong-arch image this change is meant to prevent can still be introduced during startup. Set the same platform on the compose pull/project path or validate the cached image platform before accepting it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the daemon architecture for remote Docker pulls

When the CLI talks to a Docker daemon with a different architecture from the CLI binary, such as a remote DOCKER_HOST or an amd64 dev container using an arm64 host socket, runtime.GOARCH is the client architecture rather than the daemon's. This forces the daemon to pull linux/<client-arch> images and can recreate the same exec-format/no-matching-manifest startup failures on the remote host; query the daemon platform or avoid overriding Docker's default when the daemon architecture is not known.

Useful? React with 👍 / 👎.

})
if err != nil {
return errors.Errorf("failed to pull docker image: %w", err)
Expand Down
Loading