• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 19889610058

03 Dec 2025 09:52AM UTC coverage: 56.158% (-0.03%) from 56.184%
19889610058

Pull #4570

github

web-flow
Merge 7b0a9db80 into b9afe63b1
Pull Request #4570: fix: ensure conf.d directory exists for postgres 17 compatibility

1 of 2 new or added lines in 1 file covered. (50.0%)

5 existing lines in 1 file now uncovered.

6817 of 12139 relevant lines covered (56.16%)

6.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.24
/internal/db/start/start.go
1
package start
2

3
import (
4
        "context"
5
        _ "embed"
6
        "fmt"
7
        "io"
8
        "os"
9
        "path/filepath"
10
        "strconv"
11
        "strings"
12
        "time"
13

14
        "github.com/cenkalti/backoff/v4"
15
        "github.com/containerd/errdefs"
16
        "github.com/docker/docker/api/types/container"
17
        "github.com/docker/docker/api/types/network"
18
        "github.com/docker/go-connections/nat"
19
        "github.com/go-errors/errors"
20
        "github.com/jackc/pgconn"
21
        "github.com/jackc/pgx/v4"
22
        "github.com/spf13/afero"
23
        "github.com/supabase/cli/internal/migration/apply"
24
        "github.com/supabase/cli/internal/status"
25
        "github.com/supabase/cli/internal/utils"
26
        "github.com/supabase/cli/internal/utils/flags"
27
        "github.com/supabase/cli/pkg/config"
28
        "github.com/supabase/cli/pkg/migration"
29
        "github.com/supabase/cli/pkg/vault"
30
)
31

32
var (
33
        HealthTimeout = 120 * time.Second
34
        //go:embed templates/schema.sql
35
        initialSchema string
36
        //go:embed templates/webhook.sql
37
        webhookSchema string
38
        //go:embed templates/_supabase.sql
39
        _supabaseSchema string
40
        //go:embed templates/restore.sh
41
        restoreScript string
42
)
43

44
func Run(ctx context.Context, fromBackup string, fsys afero.Fs) error {
4✔
45
        if err := flags.LoadConfig(fsys); err != nil {
5✔
46
                return err
1✔
47
        }
1✔
48
        if err := utils.AssertSupabaseDbIsRunning(); err == nil {
4✔
49
                fmt.Fprintln(os.Stderr, "Postgres database is already running.")
1✔
50
                return nil
1✔
51
        } else if !errors.Is(err, utils.ErrNotRunning) {
4✔
52
                return err
1✔
53
        }
1✔
54
        err := StartDatabase(ctx, fromBackup, fsys, os.Stderr)
1✔
55
        if err != nil {
2✔
56
                if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil {
1✔
57
                        fmt.Fprintln(os.Stderr, err)
×
58
                }
×
59
        }
60
        return err
1✔
61
}
62

63
func NewContainerConfig(args ...string) container.Config {
23✔
64
        env := []string{
23✔
65
                "POSTGRES_PASSWORD=" + utils.Config.Db.Password,
23✔
66
                "POSTGRES_HOST=/var/run/postgresql",
23✔
67
                "JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
23✔
68
                fmt.Sprintf("JWT_EXP=%d", utils.Config.Auth.JwtExpiry),
23✔
69
        }
23✔
70
        if len(utils.Config.Experimental.OrioleDBVersion) > 0 {
23✔
71
                env = append(env,
×
72
                        "POSTGRES_INITDB_ARGS=--lc-collate=C --lc-ctype=C",
×
73
                        fmt.Sprintf("S3_ENABLED=%t", true),
×
74
                        "S3_HOST="+utils.Config.Experimental.S3Host,
×
75
                        "S3_REGION="+utils.Config.Experimental.S3Region,
×
76
                        "S3_ACCESS_KEY="+utils.Config.Experimental.S3AccessKey,
×
77
                        "S3_SECRET_KEY="+utils.Config.Experimental.S3SecretKey,
×
78
                )
×
79
        } else if i := strings.IndexByte(utils.Config.Db.Image, ':'); config.VersionCompare(utils.Config.Db.Image[i+1:], "15.8.1.005") < 0 {
23✔
80
                env = append(env, "POSTGRES_INITDB_ARGS=--lc-collate=C.UTF-8")
×
81
        }
×
82
        config := container.Config{
23✔
83
                Image: utils.Config.Db.Image,
23✔
84
                Env:   env,
23✔
85
                Healthcheck: &container.HealthConfig{
23✔
86
                        Test:     []string{"CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1", "-p", "5432"},
23✔
87
                        Interval: 10 * time.Second,
23✔
88
                        Timeout:  2 * time.Second,
23✔
89
                        Retries:  3,
23✔
90
                },
23✔
91
                Entrypoint: []string{"sh", "-c", `
23✔
92
mkdir -p /etc/postgresql-custom/conf.d && \
23✔
93
cat <<'EOF' > /etc/postgresql.schema.sql && \
23✔
94
cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
23✔
95
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
23✔
96
docker-entrypoint.sh postgres -D /etc/postgresql ` + strings.Join(args, " ") + `
23✔
97
` + initialSchema + `
23✔
98
` + webhookSchema + `
23✔
99
` + _supabaseSchema + `
23✔
100
EOF
23✔
101
` + utils.Config.Db.RootKey.Value + `
23✔
102
EOF
23✔
103
` + utils.Config.Db.Settings.ToPostgresConfig() + `
23✔
104
EOF`},
23✔
105
        }
23✔
106
        if utils.Config.Db.MajorVersion <= 14 {
30✔
107
                config.Entrypoint = []string{"sh", "-c", `
7✔
108
cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql && \
7✔
109
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
7✔
110
docker-entrypoint.sh postgres -D /etc/postgresql ` + strings.Join(args, " ") + `
7✔
111
` + _supabaseSchema + `
7✔
112
EOF
7✔
113
` + utils.Config.Db.Settings.ToPostgresConfig() + `
7✔
114
EOF`}
7✔
115
        }
7✔
116
        return config
23✔
117
}
118

119
func NewHostConfig() container.HostConfig {
8✔
120
        hostPort := strconv.FormatUint(uint64(utils.Config.Db.Port), 10)
8✔
121
        hostConfig := container.HostConfig{
8✔
122
                PortBindings:  nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}},
8✔
123
                RestartPolicy: container.RestartPolicy{Name: "always"},
8✔
124
                Binds: []string{
8✔
125
                        utils.DbId + ":/var/lib/postgresql/data",
8✔
126
                        utils.ConfigId + ":/etc/postgresql-custom",
8✔
127
                },
8✔
128
        }
8✔
129
        if utils.Config.Db.MajorVersion <= 14 {
9✔
130
                hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
1✔
131
        }
1✔
132
        return hostConfig
8✔
133
}
134

135
func StartDatabase(ctx context.Context, fromBackup string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
7✔
136
        config := NewContainerConfig()
7✔
137
        hostConfig := NewHostConfig()
7✔
138
        networkingConfig := network.NetworkingConfig{
7✔
139
                EndpointsConfig: map[string]*network.EndpointSettings{
7✔
140
                        utils.NetId: {
7✔
141
                                Aliases: utils.DbAliases,
7✔
142
                        },
7✔
143
                },
7✔
144
        }
7✔
145
        if len(fromBackup) > 0 {
7✔
146
                config.Entrypoint = []string{"sh", "-c", `
×
NEW
147
mkdir -p /etc/postgresql-custom/conf.d && \
×
148
cat <<'EOF' > /etc/postgresql.schema.sql && \
×
149
cat <<'EOF' > /docker-entrypoint-initdb.d/migrate.sh && \
×
150
cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
×
151
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
×
152
docker-entrypoint.sh postgres -D /etc/postgresql
×
153
` + initialSchema + `
×
154
` + _supabaseSchema + `
×
155
EOF
×
156
` + restoreScript + `
×
157
EOF
×
158
` + utils.Config.Db.RootKey.Value + `
×
159
EOF
×
160
` + utils.Config.Db.Settings.ToPostgresConfig() + `
×
161
cron.launch_active_jobs = off
×
162
EOF`}
×
163
                if !filepath.IsAbs(fromBackup) {
×
164
                        fromBackup = filepath.Join(utils.CurrentDirAbs, fromBackup)
×
165
                }
×
166
                hostConfig.Binds = append(hostConfig.Binds, utils.ToDockerPath(fromBackup)+":/etc/backup.sql:ro")
×
167
        }
168
        // Creating volume will not override existing volume, so we must inspect explicitly
169
        _, err := utils.Docker.VolumeInspect(ctx, utils.DbId)
7✔
170
        utils.NoBackupVolume = errdefs.IsNotFound(err)
7✔
171
        if utils.NoBackupVolume {
10✔
172
                fmt.Fprintln(w, "Starting database...")
3✔
173
        } else if len(fromBackup) > 0 {
7✔
174
                utils.CmdSuggestion = fmt.Sprintf("Run %s to remove existing docker volumes.", utils.Aqua("supabase stop --no-backup"))
×
175
                return errors.Errorf("backup volume already exists")
×
176
        } else {
4✔
177
                fmt.Fprintln(w, "Starting database from backup...")
4✔
178
        }
4✔
179
        if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil {
9✔
180
                return err
2✔
181
        }
2✔
182
        // Ignore health check because restoring a large backup may take longer than 2 minutes
183
        if err := WaitForHealthyService(ctx, HealthTimeout, utils.DbId); err != nil && len(fromBackup) == 0 {
5✔
184
                return err
×
185
        }
×
186
        // Initialize if we are on PG14 and there's no existing db volume
187
        if utils.NoBackupVolume && len(fromBackup) == 0 {
8✔
188
                if err := SetupLocalDatabase(ctx, "", fsys, w, options...); err != nil {
3✔
189
                        return err
×
190
                }
×
191
        }
192
        return initCurrentBranch(fsys)
5✔
193
}
194

195
func NewBackoffPolicy(ctx context.Context, timeout time.Duration) backoff.BackOff {
36✔
196
        policy := backoff.WithMaxRetries(
36✔
197
                backoff.NewConstantBackOff(time.Second),
36✔
198
                uint64(timeout.Seconds()),
36✔
199
        )
36✔
200
        return backoff.WithContext(policy, ctx)
36✔
201
}
36✔
202

203
func WaitForHealthyService(ctx context.Context, timeout time.Duration, started ...string) error {
20✔
204
        probe := func() error {
40✔
205
                var errHealth []error
20✔
206
                var unhealthy []string
20✔
207
                for _, container := range started {
49✔
208
                        if err := status.IsServiceReady(ctx, container); err != nil {
32✔
209
                                unhealthy = append(unhealthy, container)
3✔
210
                                errHealth = append(errHealth, err)
3✔
211
                        }
3✔
212
                }
213
                started = unhealthy
20✔
214
                return errors.Join(errHealth...)
20✔
215
        }
216
        policy := NewBackoffPolicy(ctx, timeout)
20✔
217
        err := backoff.Retry(probe, policy)
20✔
218
        if err != nil && !errors.Is(err, context.Canceled) {
23✔
219
                // Print container logs for easier debugging
3✔
220
                for _, containerId := range started {
6✔
221
                        fmt.Fprintln(os.Stderr, containerId, "container logs:")
3✔
222
                        if err := utils.DockerStreamLogsOnce(context.Background(), containerId, os.Stderr, os.Stderr); err != nil {
6✔
223
                                fmt.Fprintln(os.Stderr, err)
3✔
224
                        }
3✔
225
                }
226
        }
227
        return err
20✔
228
}
229

230
func IsUnhealthyError(err error) bool {
×
231
        // Health check always returns a joinError
×
232
        _, ok := err.(interface{ Unwrap() []error })
×
233
        return ok
×
234
}
×
235

236
func initCurrentBranch(fsys afero.Fs) error {
8✔
237
        // Create _current_branch file to avoid breaking db branch commands
8✔
238
        if _, err := fsys.Stat(utils.CurrBranchPath); err == nil {
8✔
239
                return nil
×
240
        } else if !errors.Is(err, os.ErrNotExist) {
9✔
241
                return errors.Errorf("failed init current branch: %w", err)
1✔
242
        }
1✔
243
        return utils.WriteFile(utils.CurrBranchPath, []byte("main"), fsys)
7✔
244
}
245

246
func initSchema(ctx context.Context, conn *pgx.Conn, host string, w io.Writer) error {
15✔
247
        fmt.Fprintln(w, "Initialising schema...")
15✔
248
        if utils.Config.Db.MajorVersion <= 14 {
20✔
249
                if file, err := migration.NewMigrationFromReader(strings.NewReader(utils.GlobalsSql)); err != nil {
5✔
250
                        return err
×
251
                } else if err := file.ExecBatch(ctx, conn); err != nil {
7✔
252
                        return err
2✔
253
                }
2✔
254
                return InitSchema14(ctx, conn)
3✔
255
        }
256
        return initSchema15(ctx, host)
10✔
257
}
258

259
func InitSchema14(ctx context.Context, conn *pgx.Conn) error {
5✔
260
        sql := utils.InitialSchemaPg14Sql
5✔
261
        if utils.Config.Db.MajorVersion == 13 {
5✔
262
                sql = utils.InitialSchemaPg13Sql
×
263
        }
×
264
        file, err := migration.NewMigrationFromReader(strings.NewReader(sql))
5✔
265
        if err != nil {
5✔
266
                return err
×
267
        }
×
268
        return file.ExecBatch(ctx, conn)
5✔
269
}
270

271
func initRealtimeJob(host, jwks string) utils.DockerJob {
8✔
272
        return utils.DockerJob{
8✔
273
                Image: utils.Config.Realtime.Image,
8✔
274
                Env: []string{
8✔
275
                        "PORT=4000",
8✔
276
                        "DB_HOST=" + host,
8✔
277
                        "DB_PORT=5432",
8✔
278
                        "DB_USER=" + utils.SUPERUSER_ROLE,
8✔
279
                        "DB_PASSWORD=" + utils.Config.Db.Password,
8✔
280
                        "DB_NAME=postgres",
8✔
281
                        "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime",
8✔
282
                        "DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey,
8✔
283
                        fmt.Sprintf("API_JWT_JWKS=%s", jwks),
8✔
284
                        "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
285
                        "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
286
                        "APP_NAME=realtime",
8✔
287
                        "SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase,
8✔
288
                        "ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion),
8✔
289
                        "DNS_NODES=''",
8✔
290
                        "RLIMIT_NOFILE=",
8✔
291
                        "SEED_SELF_HOST=true",
8✔
292
                        "RUN_JANITOR=true",
8✔
293
                        fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength),
8✔
294
                },
8✔
295
                Cmd: []string{"/app/bin/realtime", "eval", fmt.Sprintf(`{:ok, _} = Application.ensure_all_started(:realtime)
8✔
296
{:ok, _} = Realtime.Tenants.health_check("%s")`, utils.Config.Realtime.TenantId)},
8✔
297
        }
8✔
298
}
8✔
299

300
func initStorageJob(host string) utils.DockerJob {
8✔
301
        return utils.DockerJob{
8✔
302
                Image: utils.Config.Storage.Image,
8✔
303
                Env: []string{
8✔
304
                        "DB_INSTALL_ROLES=false",
8✔
305
                        "DB_MIGRATIONS_FREEZE_AT=" + utils.Config.Storage.TargetMigration,
8✔
306
                        "ANON_KEY=" + utils.Config.Auth.AnonKey.Value,
8✔
307
                        "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value,
8✔
308
                        "PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
309
                        fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host),
8✔
310
                        fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
8✔
311
                        "STORAGE_BACKEND=file",
8✔
312
                        "STORAGE_FILE_BACKEND_PATH=/mnt",
8✔
313
                        "TENANT_ID=stub",
8✔
314
                        // TODO: https://github.com/supabase/storage-api/issues/55
8✔
315
                        "REGION=stub",
8✔
316
                        "GLOBAL_S3_BUCKET=stub",
8✔
317
                },
8✔
318
                Cmd: []string{"node", "dist/scripts/migrate-call.js"},
8✔
319
        }
8✔
320
}
8✔
321

322
func initAuthJob(host string) utils.DockerJob {
8✔
323
        return utils.DockerJob{
8✔
324
                Image: utils.Config.Auth.Image,
8✔
325
                Env: []string{
8✔
326
                        "API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl,
8✔
327
                        "GOTRUE_LOG_LEVEL=error",
8✔
328
                        "GOTRUE_DB_DRIVER=postgres",
8✔
329
                        fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host),
8✔
330
                        "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl,
8✔
331
                        "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
332
                },
8✔
333
                Cmd: []string{"gotrue", "migrate"},
8✔
334
        }
8✔
335
}
8✔
336

337
func initSchema15(ctx context.Context, host string) error {
10✔
338
        // Apply service migrations
10✔
339
        var initJobs []utils.DockerJob
10✔
340
        if utils.Config.Realtime.Enabled {
18✔
341
                jwks, err := utils.Config.Auth.ResolveJWKS(context.Background())
8✔
342
                if err != nil {
8✔
343
                        return err
×
344
                }
×
345
                initJobs = append(initJobs, initRealtimeJob(host, jwks))
8✔
346
        }
347
        if utils.Config.Storage.Enabled {
18✔
348
                initJobs = append(initJobs, initStorageJob(host))
8✔
349
        }
8✔
350
        if utils.Config.Auth.Enabled {
18✔
351
                initJobs = append(initJobs, initAuthJob(host))
8✔
352
        }
8✔
353
        logger := utils.GetDebugLogger()
10✔
354
        for _, job := range initJobs {
30✔
355
                if err := utils.DockerRunJob(ctx, job, io.Discard, logger); err != nil {
22✔
356
                        return err
2✔
357
                }
2✔
358
        }
359
        return nil
8✔
360
}
361

362
func SetupLocalDatabase(ctx context.Context, version string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
8✔
363
        conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
8✔
364
        if err != nil {
9✔
365
                return err
1✔
366
        }
1✔
367
        defer conn.Close(context.Background())
7✔
368
        if err := SetupDatabase(ctx, conn, utils.DbId, w, fsys); err != nil {
9✔
369
                return err
2✔
370
        }
2✔
371
        return apply.MigrateAndSeed(ctx, version, conn, fsys)
5✔
372
}
373

374
func SetupDatabase(ctx context.Context, conn *pgx.Conn, host string, w io.Writer, fsys afero.Fs) error {
15✔
375
        if err := initSchema(ctx, conn, host, w); err != nil {
19✔
376
                return err
4✔
377
        }
4✔
378
        // Create vault secrets first so roles.sql can reference them
379
        if err := vault.UpsertVaultSecrets(ctx, utils.Config.Db.Vault, conn); err != nil {
11✔
380
                return err
×
381
        }
×
382
        err := migration.SeedGlobals(ctx, []string{utils.CustomRolesPath}, conn, afero.NewIOFS(fsys))
11✔
383
        if errors.Is(err, os.ErrNotExist) {
19✔
384
                return nil
8✔
385
        }
8✔
386
        return err
3✔
387
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc