MHSanaei/3x-ui · critical

postgres unreachable after %d attempts: %w

Error message

postgres unreachable after %d attempts: %w

What it means

InitDB's Postgres path retries gorm.Open with escalating delays; after every attempt fails it wraps the last error in this aggregate. The underlying %w error (DNS failure, refused connection, auth error, bad DSN) is the real diagnosis — this message only says the retry budget ran out. It means the panel could not reach/authorize against PostgreSQL within the backoff window.

Source

Thrown at internal/database/db.go:2089

// used to be buried behind a generic startup failure.
func openPostgresWithRetry(dsn string, c *gorm.Config) (*gorm.DB, error) {
	delays := []time.Duration{0, 2 * time.Second, 5 * time.Second, 10 * time.Second, 20 * time.Second, 30 * time.Second}
	var lastErr error
	for i, delay := range delays {
		if delay > 0 {
			time.Sleep(delay)
		}
		conn, err := gorm.Open(postgres.Open(dsn), c)
		if err == nil {
			if i > 0 {
				log.Printf("postgres connection established on attempt %d/%d", i+1, len(delays))
			}
			return conn, nil
		}
		lastErr = err
		log.Printf("postgres connection attempt %d/%d failed: %v", i+1, len(delays), err)
	}
	return nil, fmt.Errorf("postgres unreachable after %d attempts: %w", len(delays), lastErr)
}

func sqliteJournalMode() string {
	switch strings.ToUpper(strings.TrimSpace(os.Getenv("XUI_DB_JOURNAL_MODE"))) {
	case "DELETE":
		return "DELETE"
	default:
		return "WAL"
	}
}

func backupSQLiteStepPages() int {
	if sqliteJournalMode() == "DELETE" {
		return 128
	}
	return -1
}

View on GitHub (pinned to ad32144c42)

Solutions

  1. Read the wrapped lastErr in the log line just above — it names the actual failure (connect refused / auth / TLS).
  2. Verify reachability and credentials: psql '<DSN>' -c 'select 1' from the panel host/container.
  3. Fix startup ordering: add depends_on with condition: service_healthy for postgres, or an entrypoint wait loop.
  4. For slow-starting remote DBs, ensure DSN has sane connect_timeout; the retry loop only covers initial open.

Example fix

# before (compose race)
services:
  x-ui:
    environment: [XUI_DB_TYPE=postgres, XUI_DB_DSN=...]
  # no ordering
# after
services:
  x-ui:
    depends_on:
      postgres:
        condition: service_healthy
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight the DSN before handing it to InitDB:
sqlDB, err := sql.Open("pgx", dsn)
if err != nil { return err }
defer sqlDB.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := sqlDB.PingContext(ctx); err != nil { return err }

Try / catch

for attempt := 0; attempt < 5; attempt++ {
    err := database.InitDB(path)
    if err == nil || !strings.Contains(err.Error(), "postgres unreachable") {
        break
    }
    time.Sleep(time.Duration(attempt+1) * 3 * time.Second)
}

Prevention

When it happens

Trigger: XUI_DB_TYPE=postgres with an XUI_DB_DSN pointing at a not-yet-started container (race on first boot), wrong password/host, TLS mismatch, or firewall/SG blocking 5432.

Common situations: docker-compose panel starting before the postgres service healthcheck passes; DSN copied with wrong user/db name; postgres requiring sslmode=verify-full without the CA present; k8s NetworkPolicy blocking the panel pod.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/7332cb054a90d700. Report an issue: GitHub.