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

supabase / cli / 19032133102

03 Nov 2025 10:55AM UTC coverage: 54.585% (-0.1%) from 54.699%
19032133102

Pull #4394

github

web-flow
Merge 3b6ecd499 into df557dc33
Pull Request #4394: feat(cli): add concurrent images pulling

213 of 384 new or added lines in 3 files covered. (55.47%)

16 existing lines in 2 files now uncovered.

6584 of 12062 relevant lines covered (54.58%)

6.36 hits per line

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

65.25
/internal/start/start.go
1
package start
2

3
import (
4
        "bytes"
5
        "context"
6
        _ "embed"
7
        "fmt"
8
        "net"
9
        "net/url"
10
        "os"
11
        "path"
12
        "path/filepath"
13
        "slices"
14
        "strconv"
15
        "strings"
16
        "sync"
17
        "text/template"
18
        "time"
19

20
        "github.com/containerd/errdefs"
21
        "github.com/docker/cli/cli/streams"
22
        "github.com/docker/docker/api/types/container"
23
        "github.com/docker/docker/api/types/network"
24
        "github.com/docker/docker/client"
25
        "github.com/docker/go-connections/nat"
26
        "github.com/go-errors/errors"
27
        "github.com/jackc/pgconn"
28
        "github.com/jackc/pgx/v4"
29
        "github.com/spf13/afero"
30
        "golang.org/x/sync/errgroup"
31

32
        "github.com/docker/compose/v2/pkg/progress"
33
        "github.com/supabase/cli/internal/db/start"
34
        "github.com/supabase/cli/internal/functions/serve"
35
        "github.com/supabase/cli/internal/seed/buckets"
36
        "github.com/supabase/cli/internal/services"
37
        "github.com/supabase/cli/internal/status"
38
        "github.com/supabase/cli/internal/utils"
39
        "github.com/supabase/cli/internal/utils/flags"
40
        "github.com/supabase/cli/pkg/config"
41
)
42

43
func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error {
3✔
44
        // Sanity checks.
3✔
45
        {
6✔
46
                if err := flags.LoadConfig(fsys); err != nil {
4✔
47
                        return err
1✔
48
                }
1✔
49
                if err := utils.AssertSupabaseDbIsRunning(); err == nil {
3✔
50
                        fmt.Fprintln(os.Stderr, utils.Aqua("supabase start")+" is already running.")
1✔
51
                        names := status.CustomName{}
1✔
52
                        return status.Run(ctx, names, utils.OutputPretty, fsys)
1✔
53
                } else if !errors.Is(err, utils.ErrNotRunning) {
3✔
54
                        return err
1✔
55
                }
1✔
56
                if err := flags.LoadProjectRef(fsys); err == nil {
×
57
                        _ = services.CheckVersions(ctx, fsys)
×
58
                }
×
59
        }
60

61
        dbConfig := pgconn.Config{
×
62
                Host:     utils.DbId,
×
63
                Port:     5432,
×
64
                User:     "postgres",
×
65
                Password: utils.Config.Db.Password,
×
66
                Database: "postgres",
×
67
        }
×
68
        if err := run(ctx, fsys, excludedContainers, dbConfig); err != nil {
×
69
                if ignoreHealthCheck && start.IsUnhealthyError(err) {
×
70
                        fmt.Fprintln(os.Stderr, err)
×
71
                } else {
×
72
                        if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil {
×
73
                                fmt.Fprintln(os.Stderr, err)
×
74
                        }
×
75
                        return err
×
76
                }
77
        }
78

79
        fmt.Fprintf(os.Stderr, "Started %s local development setup.\n\n", utils.Aqua("supabase"))
×
80
        status.PrettyPrint(os.Stdout, excludedContainers...)
×
81
        return nil
×
82
}
83

84
type kongConfig struct {
85
        GotrueId      string
86
        RestId        string
87
        RealtimeId    string
88
        StorageId     string
89
        StudioId      string
90
        PgmetaId      string
91
        EdgeRuntimeId string
92
        LogflareId    string
93
        PoolerId      string
94
        ApiHost       string
95
        ApiPort       uint16
96
        BearerToken   string
97
        QueryToken    string
98
}
99

100
var (
101
        //go:embed templates/kong.yml
102
        kongConfigEmbed    string
103
        kongConfigTemplate = template.Must(template.New("kongConfig").Parse(kongConfigEmbed))
104

105
        //go:embed templates/custom_nginx.template
106
        nginxConfigEmbed string
107
        // Hardcoded configs which match nginxConfigEmbed
108
        nginxEmailTemplateDir   = "/home/kong/templates/email"
109
        nginxTemplateServerPort = 8088
110
)
111

112
type vectorConfig struct {
113
        ApiKey        string
114
        VectorId      string
115
        LogflareId    string
116
        KongId        string
117
        GotrueId      string
118
        RestId        string
119
        RealtimeId    string
120
        StorageId     string
121
        EdgeRuntimeId string
122
        DbId          string
123
}
124

125
var (
126
        //go:embed templates/vector.yaml
127
        vectorConfigEmbed    string
128
        vectorConfigTemplate = template.Must(template.New("vectorConfig").Parse(vectorConfigEmbed))
129
)
130

131
type poolerTenant struct {
132
        DbHost            string
133
        DbPort            uint16
134
        DbDatabase        string
135
        DbPassword        string
136
        ExternalId        string
137
        ModeType          config.PoolMode
138
        DefaultMaxClients uint
139
        DefaultPoolSize   uint
140
}
141

142
var (
143
        //go:embed templates/pooler.exs
144
        poolerTenantEmbed    string
145
        poolerTenantTemplate = template.Must(template.New("poolerTenant").Parse(poolerTenantEmbed))
146
)
147

148
var serviceTimeout = 30 * time.Second
149

150
// getImageBaseName extracts the base image name from a full image string.
151
// For example: "supabase/postgres:17.6" -> "postgres", "library/kong:2.8.1" -> "kong"
152
func getImageBaseName(fullImage string) string {
100✔
153
        // Remove tag if present (everything after ':')
100✔
154
        baseWithTag := strings.Split(fullImage, ":")[0]
100✔
155
        // Extract the last part after '/'
100✔
156
        parts := strings.Split(baseWithTag, "/")
100✔
157
        return parts[len(parts)-1]
100✔
158
}
100✔
159

160
// imagePullPriority returns a priority value for an image based on its base name.
161
// Lower values indicate higher priority (heavier images should be pulled first).
162
// Priority is based on approximate image sizes from largest to smallest.
163
func imagePullPriority(imageName string) int {
100✔
164
        baseName := getImageBaseName(imageName)
100✔
165

100✔
166
        // Priority map: lower number = higher priority (starts first)
100✔
167
        // Based on approximate sizes: postgres (2.95GB) > studio (819MB) > edge-runtime (680MB) > ...
100✔
168
        priorityMap := map[string]int{
100✔
169
                "postgres":      1,  // 2.95 GB - highest priority
100✔
170
                "studio":        2,  // 819.8 MB
100✔
171
                "edge-runtime":  3,  // 680.6 MB
100✔
172
                "logflare":      4,  // 670 MB
100✔
173
                "storage-api":   5,  // 626.2 MB
100✔
174
                "realtime":      6,  // 463.1 MB
100✔
175
                "postgrest":     7,  // 436.6 MB
100✔
176
                "postgres-meta": 8,  // 405.2 MB
100✔
177
                "kong":          9,  // 149 MB
100✔
178
                "vector":        10, // 111.3 MB
100✔
179
                "gotrue":        11, // 48.3 MB
100✔
180
                "mailpit":       12, // 28.9 MB
100✔
181
                "supavisor":     13, // pooler - similar size to gotrue
100✔
182
                "imgproxy":      14, // image proxy - typically smaller
100✔
183
        }
100✔
184

100✔
185
        if priority, ok := priorityMap[baseName]; ok {
200✔
186
                return priority
100✔
187
        }
100✔
188
        // Unknown images get lowest priority (highest number)
NEW
189
        return 100
×
190
}
191

192
// collectRequiredImages collects all Docker images that need to be pulled
193
// based on the current configuration and excluded containers.
194
// includeDb indicates whether the database will be started and should have its image pulled.
195
// Images are returned sorted by priority (heavier images first).
196
func collectRequiredImages(excluded map[string]bool, isStorageEnabled, isImgProxyEnabled bool, includeDb bool) []string {
2✔
197
        images := make(map[string]bool)
2✔
198

2✔
199
        // Analytics services
2✔
200
        if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.Image, excluded) {
3✔
201
                images[utils.Config.Analytics.Image] = true
1✔
202
        }
1✔
203
        if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.VectorImage, excluded) {
3✔
204
                images[utils.Config.Analytics.VectorImage] = true
1✔
205
        }
1✔
206

207
        // API Gateway
208
        if !isContainerExcluded(utils.Config.Api.KongImage, excluded) {
3✔
209
                images[utils.Config.Api.KongImage] = true
1✔
210
        }
1✔
211

212
        // Auth service
213
        if utils.Config.Auth.Enabled && !isContainerExcluded(utils.Config.Auth.Image, excluded) {
3✔
214
                images[utils.Config.Auth.Image] = true
1✔
215
        }
1✔
216

217
        // Mailpit/Inbucket
218
        if utils.Config.Inbucket.Enabled && !isContainerExcluded(utils.Config.Inbucket.Image, excluded) {
3✔
219
                images[utils.Config.Inbucket.Image] = true
1✔
220
        }
1✔
221

222
        // Realtime
223
        if utils.Config.Realtime.Enabled && !isContainerExcluded(utils.Config.Realtime.Image, excluded) {
3✔
224
                images[utils.Config.Realtime.Image] = true
1✔
225
        }
1✔
226

227
        // PostgREST
228
        if utils.Config.Api.Enabled && !isContainerExcluded(utils.Config.Api.Image, excluded) {
3✔
229
                images[utils.Config.Api.Image] = true
1✔
230
        }
1✔
231

232
        // Storage
233
        if isStorageEnabled {
3✔
234
                images[utils.Config.Storage.Image] = true
1✔
235
        }
1✔
236
        if isImgProxyEnabled {
2✔
NEW
237
                images[utils.Config.Storage.ImgProxyImage] = true
×
NEW
238
        }
×
239

240
        // Edge Runtime
241
        if utils.Config.EdgeRuntime.Enabled && !isContainerExcluded(utils.Config.EdgeRuntime.Image, excluded) {
3✔
242
                images[utils.Config.EdgeRuntime.Image] = true
1✔
243
        }
1✔
244

245
        // Studio services
246
        if utils.Config.Studio.Enabled {
4✔
247
                if !isContainerExcluded(utils.Config.Studio.PgmetaImage, excluded) {
3✔
248
                        images[utils.Config.Studio.PgmetaImage] = true
1✔
249
                }
1✔
250
                if !isContainerExcluded(utils.Config.Studio.Image, excluded) {
3✔
251
                        images[utils.Config.Studio.Image] = true
1✔
252
                }
1✔
253
        }
254

255
        // Pooler
256
        if utils.Config.Db.Pooler.Enabled && !isContainerExcluded(utils.Config.Db.Pooler.Image, excluded) {
2✔
NEW
257
                images[utils.Config.Db.Pooler.Image] = true
×
NEW
258
        }
×
259

260
        // Database (if it will be started)
261
        if includeDb {
4✔
262
                images[utils.Config.Db.Image] = true
2✔
263
        }
2✔
264

265
        // Convert map to slice
266
        result := make([]string, 0, len(images))
2✔
267
        for img := range images {
15✔
268
                result = append(result, img)
13✔
269
        }
13✔
270

271
        // Sort by priority (heavier images first)
272
        slices.SortFunc(result, func(a, b string) int {
52✔
273
                priorityA := imagePullPriority(a)
50✔
274
                priorityB := imagePullPriority(b)
50✔
275
                if priorityA != priorityB {
100✔
276
                        return priorityA - priorityB
50✔
277
                }
50✔
278
                // If priorities are equal, sort alphabetically for consistency
NEW
279
                return strings.Compare(a, b)
×
280
        })
281

282
        return result
2✔
283
}
284

285
// pullImagesInParallel pulls all provided images in parallel using docker-compose style progress bars.
286
// Returns an error if any image pull fails.
287
func pullImagesInParallel(ctx context.Context, images []string) error {
2✔
288
        if len(images) == 0 {
2✔
NEW
289
                return nil
×
NEW
290
        }
×
291

292
        // Use docker-compose's progress bar system
293
        out := streams.NewOut(os.Stderr)
2✔
294

2✔
295
        return progress.Run(ctx, func(ctx context.Context) error {
4✔
296
                return pullImagesWithProgress(ctx, images)
2✔
297
        }, out)
2✔
298
}
299

300
func pullImagesWithProgress(ctx context.Context, images []string) error {
2✔
301
        eg, ctx := errgroup.WithContext(ctx)
2✔
302
        eg.SetLimit(10) // Limit concurrent pulls
2✔
303

2✔
304
        type pullResult struct {
2✔
305
                image string
2✔
306
                err   error
2✔
307
        }
2✔
308

2✔
309
        resultChan := make(chan pullResult, len(images))
2✔
310
        var imagesBeingPulled sync.Map
2✔
311

2✔
312
        // Start pulling images in parallel
2✔
313
        // Note: images are already sorted by priority (heavier images first) from collectRequiredImages
2✔
314
        // Add 1 second delay between each image pull initiation to avoid rate limit errors
2✔
315
        // Only delay for images that actually need to be pulled (not cached)
2✔
316
        firstPull := true
2✔
317
        for _, img := range images {
15✔
318
                // Deduplicate images being pulled (pattern from docker-compose)
13✔
319
                if _, alreadyPulling := imagesBeingPulled.LoadOrStore(img, true); alreadyPulling {
13✔
NEW
320
                        continue
×
321
                }
322

323
                imageName := img // capture loop variable
13✔
324
                imageUrl := utils.GetRegistryImageUrl(imageName)
13✔
325

13✔
326
                // Check if image is already cached before adding delay
13✔
327
                isCached := false
13✔
328
                if _, err := utils.Docker.ImageInspect(ctx, imageUrl); err == nil {
26✔
329
                        isCached = true
13✔
330
                } else if !errdefs.IsNotFound(err) {
13✔
NEW
331
                        // Error inspecting image, but not a "not found" error - still need to pull
×
NEW
332
                        isCached = false
×
NEW
333
                }
×
334

335
                // If image needs to be pulled, add 1 second delay between initiating each pull
336
                // (skip delay for first image that needs to be pulled)
337
                if !isCached {
13✔
NEW
338
                        if !firstPull {
×
NEW
339
                                select {
×
NEW
340
                                case <-ctx.Done():
×
NEW
341
                                        return ctx.Err()
×
NEW
342
                                case <-time.After(1 * time.Second):
×
343
                                }
344
                        }
NEW
345
                        firstPull = false
×
346
                }
347

348
                eg.Go(func() error {
26✔
349
                        defer imagesBeingPulled.Delete(imageName)
13✔
350

13✔
351
                        resource := "Image " + imageName
13✔
352
                        writer := progress.ContextWriter(ctx)
13✔
353

13✔
354
                        // If cached, mark as done immediately without pulling
13✔
355
                        if isCached {
26✔
356
                                writer.Event(progress.Event{
13✔
357
                                        ID:         resource,
13✔
358
                                        Status:     progress.Done,
13✔
359
                                        StatusText: "Already cached",
13✔
360
                                })
13✔
361
                                resultChan <- pullResult{image: imageName, err: nil}
13✔
362
                                return nil
13✔
363
                        }
13✔
364

NEW
365
                        writer.Event(progress.Event{
×
NEW
366
                                ID:         resource,
×
NEW
367
                                Status:     progress.Working,
×
NEW
368
                                StatusText: "Pulling",
×
NEW
369
                        })
×
NEW
370

×
NEW
371
                        err := pullSingleImageWithProgress(ctx, imageName, resource)
×
NEW
372

×
NEW
373
                        if err != nil {
×
NEW
374
                                writer.Event(progress.Event{
×
NEW
375
                                        ID:         resource,
×
NEW
376
                                        Status:     progress.Error,
×
NEW
377
                                        StatusText: getUnwrappedErrorMessage(err),
×
NEW
378
                                })
×
NEW
379
                                resultChan <- pullResult{image: imageName, err: err}
×
NEW
380
                                return nil // Don't fail fast - collect all errors
×
NEW
381
                        }
×
382

NEW
383
                        writer.Event(progress.Event{
×
NEW
384
                                ID:         resource,
×
NEW
385
                                Status:     progress.Done,
×
NEW
386
                                StatusText: "Pulled",
×
NEW
387
                        })
×
NEW
388
                        resultChan <- pullResult{image: imageName, err: nil}
×
NEW
389
                        return nil
×
390
                })
391
        }
392

393
        // Wait for all pulls to complete
394
        _ = eg.Wait()
2✔
395
        close(resultChan)
2✔
396

2✔
397
        // Collect results
2✔
398
        var errs []error
2✔
399
        var failedImages []string
2✔
400

2✔
401
        for result := range resultChan {
15✔
402
                if result.err != nil {
13✔
NEW
403
                        errs = append(errs, errors.Errorf("failed to pull image %s: %w", result.image, result.err))
×
NEW
404
                        failedImages = append(failedImages, result.image)
×
NEW
405
                }
×
406
        }
407

408
        if len(errs) > 0 {
2✔
NEW
409
                return errors.Errorf("failed to pull images:\n%s", strings.Join(failedImages, "\n"))
×
NEW
410
        }
×
411

412
        return nil
2✔
413
}
414

415
// pullSingleImageWithProgress pulls a single image with retry logic and reports progress using docker-compose style events
NEW
416
func pullSingleImageWithProgress(ctx context.Context, imageName, resource string) error {
×
NEW
417
        imageUrl := utils.GetRegistryImageUrl(imageName)
×
NEW
418

×
NEW
419
        // Check if already cached
×
NEW
420
        if _, err := utils.Docker.ImageInspect(ctx, imageUrl); err == nil {
×
NEW
421
                return nil
×
NEW
422
        } else if !errdefs.IsNotFound(err) {
×
NEW
423
                return errors.Errorf("failed to inspect docker image: %w", err)
×
NEW
424
        }
×
425

426
        // Pull with retry (same retry logic as DockerPullImageIfNotCachedWithWriter)
NEW
427
        const maxRetries uint = 5
×
NEW
428
        writer := progress.ContextWriter(ctx)
×
NEW
429
        isRetry := false
×
NEW
430
        return utils.RetryWithExponentialBackoff(ctx, func() error {
×
NEW
431
                // Reset status to "Pulling" on retry attempts (not the first attempt)
×
NEW
432
                if isRetry {
×
NEW
433
                        writer.Event(progress.Event{
×
NEW
434
                                ID:         resource,
×
NEW
435
                                Status:     progress.Working,
×
NEW
436
                                StatusText: "Pulling",
×
NEW
437
                        })
×
NEW
438
                }
×
NEW
439
                isRetry = true
×
NEW
440
                return utils.DockerImagePullWithProgress(ctx, imageUrl, resource, writer)
×
NEW
441
        }, maxRetries, func(attempt uint, backoff time.Duration) {
×
NEW
442
                writer.Event(progress.Event{
×
NEW
443
                        ID:         resource,
×
NEW
444
                        Status:     progress.Warning,
×
NEW
445
                        StatusText: fmt.Sprintf("Retrying after %v...", backoff),
×
NEW
446
                })
×
NEW
447
        }, utils.IsRetryablePullError)
×
448
}
449

NEW
450
func getUnwrappedErrorMessage(err error) string {
×
NEW
451
        derr := errors.Unwrap(err)
×
NEW
452
        if derr != nil {
×
NEW
453
                return getUnwrappedErrorMessage(derr)
×
NEW
454
        }
×
NEW
455
        return err.Error()
×
456
}
457

458
func run(ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error {
2✔
459
        excluded := make(map[string]bool)
2✔
460
        for _, name := range excludedContainers {
17✔
461
                excluded[name] = true
15✔
462
        }
15✔
463

464
        jwks, err := utils.Config.Auth.ResolveJWKS(ctx)
2✔
465
        if err != nil {
2✔
466
                return err
×
467
        }
×
468

469
        var started []string
2✔
470
        var isStorageEnabled = utils.Config.Storage.Enabled && !isContainerExcluded(utils.Config.Storage.Image, excluded)
2✔
471
        var isImgProxyEnabled = utils.Config.Storage.ImageTransformation != nil &&
2✔
472
                utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.Config.Storage.ImgProxyImage, excluded)
2✔
473

2✔
474
        // Collect and pull all required images in parallel before starting containers
2✔
475
        includeDb := dbConfig.Host == utils.DbId
2✔
476
        requiredImages := collectRequiredImages(excluded, isStorageEnabled, isImgProxyEnabled, includeDb)
2✔
477
        if err := pullImagesInParallel(ctx, requiredImages); err != nil {
2✔
NEW
478
                return err
×
NEW
479
        }
×
480

481
        // Start Postgres.
482
        if includeDb {
4✔
483
                if err := start.StartDatabase(ctx, "", fsys, os.Stderr, options...); err != nil {
2✔
484
                        return err
×
485
                }
×
486
        }
487

488
        fmt.Fprintln(os.Stderr, "Starting containers...")
2✔
489

2✔
490
        // Start Logflare
2✔
491
        if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.Image, excluded) {
3✔
492
                env := []string{
1✔
493
                        "DB_DATABASE=_supabase",
1✔
494
                        "DB_HOSTNAME=" + dbConfig.Host,
1✔
495
                        fmt.Sprintf("DB_PORT=%d", dbConfig.Port),
1✔
496
                        "DB_SCHEMA=_analytics",
1✔
497
                        "DB_USERNAME=" + utils.SUPERUSER_ROLE,
1✔
498
                        "DB_PASSWORD=" + dbConfig.Password,
1✔
499
                        "LOGFLARE_MIN_CLUSTER_SIZE=1",
1✔
500
                        "LOGFLARE_SINGLE_TENANT=true",
1✔
501
                        "LOGFLARE_SUPABASE_MODE=true",
1✔
502
                        "LOGFLARE_PRIVATE_ACCESS_TOKEN=" + utils.Config.Analytics.ApiKey,
1✔
503
                        "LOGFLARE_LOG_LEVEL=warn",
1✔
504
                        "LOGFLARE_NODE_HOST=127.0.0.1",
1✔
505
                        "LOGFLARE_FEATURE_FLAG_OVERRIDE='multibackend=true'",
1✔
506
                        "RELEASE_COOKIE=cookie",
1✔
507
                }
1✔
508
                bind := []string{}
1✔
509

1✔
510
                switch utils.Config.Analytics.Backend {
1✔
511
                case config.LogflareBigQuery:
×
512
                        workdir, err := os.Getwd()
×
513
                        if err != nil {
×
514
                                return errors.Errorf("failed to get working directory: %w", err)
×
515
                        }
×
516
                        hostJwtPath := filepath.Join(workdir, utils.Config.Analytics.GcpJwtPath)
×
517
                        bind = append(bind, hostJwtPath+":/opt/app/rel/logflare/bin/gcloud.json")
×
518
                        // This is hardcoded in studio frontend
×
519
                        env = append(env,
×
520
                                "GOOGLE_DATASET_ID_APPEND=_prod",
×
521
                                "GOOGLE_PROJECT_ID="+utils.Config.Analytics.GcpProjectId,
×
522
                                "GOOGLE_PROJECT_NUMBER="+utils.Config.Analytics.GcpProjectNumber,
×
523
                        )
×
524
                case config.LogflarePostgres:
1✔
525
                        env = append(env,
1✔
526
                                fmt.Sprintf("POSTGRES_BACKEND_URL=postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, "_supabase"),
1✔
527
                                "POSTGRES_BACKEND_SCHEMA=_analytics",
1✔
528
                        )
1✔
529
                }
530

531
                if _, err := utils.DockerStart(
1✔
532
                        ctx,
1✔
533
                        container.Config{
1✔
534
                                Hostname: "127.0.0.1",
1✔
535
                                Image:    utils.Config.Analytics.Image,
1✔
536
                                Env:      env,
1✔
537
                                // Original entrypoint conflicts with healthcheck due to 15 seconds sleep:
1✔
538
                                // https://github.com/Logflare/logflare/blob/staging/run.sh#L35
1✔
539
                                Entrypoint: []string{"sh", "-c", `cat <<'EOF' > run.sh && sh run.sh
1✔
540
./logflare eval Logflare.Release.migrate
1✔
541
./logflare start --sname logflare
1✔
542
EOF
1✔
543
`},
1✔
544
                                Healthcheck: &container.HealthConfig{
1✔
545
                                        Test: []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null",
1✔
546
                                                "http://127.0.0.1:4000/health",
1✔
547
                                        },
1✔
548
                                        Interval:    10 * time.Second,
1✔
549
                                        Timeout:     2 * time.Second,
1✔
550
                                        Retries:     3,
1✔
551
                                        StartPeriod: 10 * time.Second,
1✔
552
                                },
1✔
553
                                ExposedPorts: nat.PortSet{"4000/tcp": {}},
1✔
554
                        },
1✔
555
                        container.HostConfig{
1✔
556
                                Binds:         bind,
1✔
557
                                PortBindings:  nat.PortMap{"4000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Analytics.Port), 10)}}},
1✔
558
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
559
                        },
1✔
560
                        network.NetworkingConfig{
1✔
561
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
562
                                        utils.NetId: {
1✔
563
                                                Aliases: utils.LogflareAliases,
1✔
564
                                        },
1✔
565
                                },
1✔
566
                        },
1✔
567
                        utils.LogflareId,
1✔
568
                ); err != nil {
1✔
569
                        return err
×
570
                }
×
571
                started = append(started, utils.LogflareId)
1✔
572
        }
573

574
        // Start vector
575
        if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.VectorImage, excluded) {
3✔
576
                var vectorConfigBuf bytes.Buffer
1✔
577
                if err := vectorConfigTemplate.Option("missingkey=error").Execute(&vectorConfigBuf, vectorConfig{
1✔
578
                        ApiKey:        utils.Config.Analytics.ApiKey,
1✔
579
                        VectorId:      utils.VectorId,
1✔
580
                        LogflareId:    utils.LogflareId,
1✔
581
                        KongId:        utils.KongId,
1✔
582
                        GotrueId:      utils.GotrueId,
1✔
583
                        RestId:        utils.RestId,
1✔
584
                        RealtimeId:    utils.RealtimeId,
1✔
585
                        StorageId:     utils.StorageId,
1✔
586
                        EdgeRuntimeId: utils.EdgeRuntimeId,
1✔
587
                        DbId:          utils.DbId,
1✔
588
                }); err != nil {
1✔
589
                        return errors.Errorf("failed to exec template: %w", err)
×
590
                }
×
591
                var binds, env, securityOpts []string
1✔
592
                // Special case for GitLab pipeline
1✔
593
                parsed, err := client.ParseHostURL(utils.Docker.DaemonHost())
1✔
594
                if err != nil {
1✔
595
                        return errors.Errorf("failed to parse docker host: %w", err)
×
596
                }
×
597
                // Ref: https://vector.dev/docs/reference/configuration/sources/docker_logs/#docker_host
598
                dindHost := &url.URL{Scheme: "http", Host: net.JoinHostPort(utils.DinDHost, "2375")}
1✔
599
                switch parsed.Scheme {
1✔
600
                case "tcp":
×
601
                        if _, port, err := net.SplitHostPort(parsed.Host); err == nil {
×
602
                                dindHost.Host = net.JoinHostPort(utils.DinDHost, port)
×
603
                        }
×
604
                        env = append(env, "DOCKER_HOST="+dindHost.String())
×
605
                case "npipe":
×
606
                        const dockerDaemonNeededErr = "Analytics on Windows requires Docker daemon exposed on tcp://localhost:2375.\nSee https://supabase.com/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=windows#running-supabase-locally for more details."
×
607
                        fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), dockerDaemonNeededErr)
×
608
                        env = append(env, "DOCKER_HOST="+dindHost.String())
×
609
                case "unix":
×
610
                        if dindHost, err = client.ParseHostURL(client.DefaultDockerHost); err != nil {
×
611
                                return errors.Errorf("failed to parse default host: %w", err)
×
612
                        } else if strings.HasSuffix(parsed.Host, "/.docker/run/docker.sock") {
×
613
                                fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "analytics requires mounting default docker socket:", dindHost.Host)
×
614
                                binds = append(binds, fmt.Sprintf("%[1]s:%[1]s:ro", dindHost.Host))
×
615
                        } else {
×
616
                                // Podman and OrbStack can mount root-less socket without issue
×
617
                                binds = append(binds, fmt.Sprintf("%s:%s:ro", parsed.Host, dindHost.Host))
×
618
                                securityOpts = append(securityOpts, "label:disable")
×
619
                        }
×
620
                }
621
                if _, err := utils.DockerStart(
1✔
622
                        ctx,
1✔
623
                        container.Config{
1✔
624
                                Image: utils.Config.Analytics.VectorImage,
1✔
625
                                Env:   env,
1✔
626
                                Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/vector/vector.yaml && vector --config /etc/vector/vector.yaml
1✔
627
` + vectorConfigBuf.String() + `
1✔
628
EOF
1✔
629
`},
1✔
630
                                Healthcheck: &container.HealthConfig{
1✔
631
                                        Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
1✔
632
                                                "http://127.0.0.1:9001/health",
1✔
633
                                        },
1✔
634
                                        Interval: 10 * time.Second,
1✔
635
                                        Timeout:  2 * time.Second,
1✔
636
                                        Retries:  3,
1✔
637
                                },
1✔
638
                        },
1✔
639
                        container.HostConfig{
1✔
640
                                Binds:         binds,
1✔
641
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
642
                                SecurityOpt:   securityOpts,
1✔
643
                        },
1✔
644
                        network.NetworkingConfig{
1✔
645
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
646
                                        utils.NetId: {
1✔
647
                                                Aliases: utils.VectorAliases,
1✔
648
                                        },
1✔
649
                                },
1✔
650
                        },
1✔
651
                        utils.VectorId,
1✔
652
                ); err != nil {
1✔
653
                        return err
×
654
                }
×
655
                if parsed.Scheme != "npipe" {
2✔
656
                        started = append(started, utils.VectorId)
1✔
657
                }
1✔
658
        }
659

660
        // Start Kong.
661
        if !isContainerExcluded(utils.Config.Api.KongImage, excluded) {
3✔
662
                var kongConfigBuf bytes.Buffer
1✔
663
                if err := kongConfigTemplate.Option("missingkey=error").Execute(&kongConfigBuf, kongConfig{
1✔
664
                        GotrueId:      utils.GotrueId,
1✔
665
                        RestId:        utils.RestId,
1✔
666
                        RealtimeId:    utils.Config.Realtime.TenantId,
1✔
667
                        StorageId:     utils.StorageId,
1✔
668
                        StudioId:      utils.StudioId,
1✔
669
                        PgmetaId:      utils.PgmetaId,
1✔
670
                        EdgeRuntimeId: utils.EdgeRuntimeId,
1✔
671
                        LogflareId:    utils.LogflareId,
1✔
672
                        PoolerId:      utils.PoolerId,
1✔
673
                        ApiHost:       utils.Config.Hostname,
1✔
674
                        ApiPort:       utils.Config.Api.Port,
1✔
675
                        BearerToken: fmt.Sprintf(
1✔
676
                                // If Authorization header is set to a self-minted JWT, we want to pass it down.
1✔
677
                                // Legacy supabase-js may set Authorization header to Bearer <apikey>. We must remove it
1✔
678
                                // to avoid failing JWT validation.
1✔
679
                                // If Authorization header is missing, we want to match against apikey header to set the
1✔
680
                                // default JWT for downstream services.
1✔
681
                                // Finally, the apikey header may be set to a legacy JWT. In that case, we want to copy
1✔
682
                                // it to Authorization header for backwards compatibility.
1✔
683
                                `$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '%s' and 'Bearer %s') or (headers.apikey == '%s' and 'Bearer %s') or headers.apikey)`,
1✔
684
                                utils.Config.Auth.SecretKey.Value,
1✔
685
                                utils.Config.Auth.ServiceRoleKey.Value,
1✔
686
                                utils.Config.Auth.PublishableKey.Value,
1✔
687
                                utils.Config.Auth.AnonKey.Value,
1✔
688
                        ),
1✔
689
                        QueryToken: fmt.Sprintf(
1✔
690
                                `$((query_params.apikey == '%s' and '%s') or (query_params.apikey == '%s' and '%s') or query_params.apikey)`,
1✔
691
                                utils.Config.Auth.SecretKey.Value,
1✔
692
                                utils.Config.Auth.ServiceRoleKey.Value,
1✔
693
                                utils.Config.Auth.PublishableKey.Value,
1✔
694
                                utils.Config.Auth.AnonKey.Value,
1✔
695
                        ),
1✔
696
                }); err != nil {
1✔
697
                        return errors.Errorf("failed to exec template: %w", err)
×
698
                }
×
699

700
                binds := []string{}
1✔
701
                for id, tmpl := range utils.Config.Auth.Email.Template {
1✔
702
                        if len(tmpl.ContentPath) == 0 {
×
703
                                continue
×
704
                        }
705
                        hostPath := tmpl.ContentPath
×
706
                        if !filepath.IsAbs(tmpl.ContentPath) {
×
707
                                var err error
×
708
                                hostPath, err = filepath.Abs(hostPath)
×
709
                                if err != nil {
×
710
                                        return errors.Errorf("failed to resolve absolute path: %w", err)
×
711
                                }
×
712
                        }
713
                        dockerPath := path.Join(nginxEmailTemplateDir, id+filepath.Ext(hostPath))
×
714
                        binds = append(binds, fmt.Sprintf("%s:%s:rw", hostPath, dockerPath))
×
715
                }
716

717
                dockerPort := uint16(8000)
1✔
718
                if utils.Config.Api.Tls.Enabled {
1✔
719
                        dockerPort = 8443
×
720
                }
×
721
                if _, err := utils.DockerStart(
1✔
722
                        ctx,
1✔
723
                        container.Config{
1✔
724
                                Image: utils.Config.Api.KongImage,
1✔
725
                                Env: []string{
1✔
726
                                        "KONG_DATABASE=off",
1✔
727
                                        "KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml",
1✔
728
                                        "KONG_DNS_ORDER=LAST,A,CNAME", // https://github.com/supabase/cli/issues/14
1✔
729
                                        "KONG_PLUGINS=request-transformer,cors",
1✔
730
                                        fmt.Sprintf("KONG_PORT_MAPS=%d:8000", utils.Config.Api.Port),
1✔
731
                                        // Need to increase the nginx buffers in kong to avoid it rejecting the rather
1✔
732
                                        // sizeable response headers azure can generate
1✔
733
                                        // Ref: https://github.com/Kong/kong/issues/3974#issuecomment-482105126
1✔
734
                                        "KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k",
1✔
735
                                        "KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k",
1✔
736
                                        "KONG_NGINX_WORKER_PROCESSES=1",
1✔
737
                                        // Use modern TLS certificate
1✔
738
                                        "KONG_SSL_CERT=/home/kong/localhost.crt",
1✔
739
                                        "KONG_SSL_CERT_KEY=/home/kong/localhost.key",
1✔
740
                                },
1✔
741
                                Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /home/kong/kong.yml && \
1✔
742
cat <<'EOF' > /home/kong/custom_nginx.template && \
1✔
743
cat <<'EOF' > /home/kong/localhost.crt && \
1✔
744
cat <<'EOF' > /home/kong/localhost.key && \
1✔
745
./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template
1✔
746
` + kongConfigBuf.String() + `
1✔
747
EOF
1✔
748
` + nginxConfigEmbed + `
1✔
749
EOF
1✔
750
` + string(utils.Config.Api.Tls.CertContent) + `
1✔
751
EOF
1✔
752
` + string(utils.Config.Api.Tls.KeyContent) + `
1✔
753
EOF
1✔
754
`},
1✔
755
                                ExposedPorts: nat.PortSet{
1✔
756
                                        "8000/tcp": {},
1✔
757
                                        "8443/tcp": {},
1✔
758
                                        nat.Port(fmt.Sprintf("%d/tcp", nginxTemplateServerPort)): {},
1✔
759
                                },
1✔
760
                        },
1✔
761
                        container.HostConfig{
1✔
762
                                Binds: binds,
1✔
763
                                PortBindings: nat.PortMap{nat.Port(fmt.Sprintf("%d/tcp", dockerPort)): []nat.PortBinding{{
1✔
764
                                        HostPort: strconv.FormatUint(uint64(utils.Config.Api.Port), 10)},
1✔
765
                                }},
1✔
766
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
767
                        },
1✔
768
                        network.NetworkingConfig{
1✔
769
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
770
                                        utils.NetId: {
1✔
771
                                                Aliases: utils.KongAliases,
1✔
772
                                        },
1✔
773
                                },
1✔
774
                        },
1✔
775
                        utils.KongId,
1✔
776
                ); err != nil {
1✔
777
                        return err
×
778
                }
×
779
                started = append(started, utils.KongId)
1✔
780
        }
781

782
        // Start GoTrue.
783
        if utils.Config.Auth.Enabled && !isContainerExcluded(utils.Config.Auth.Image, excluded) {
3✔
784
                var testOTP bytes.Buffer
1✔
785
                if len(utils.Config.Auth.Sms.TestOTP) > 0 {
1✔
786
                        formatMapForEnvConfig(utils.Config.Auth.Sms.TestOTP, &testOTP)
×
787
                }
×
788

789
                env := []string{
1✔
790
                        "API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl,
1✔
791

1✔
792
                        "GOTRUE_API_HOST=0.0.0.0",
1✔
793
                        "GOTRUE_API_PORT=9999",
1✔
794

1✔
795
                        "GOTRUE_DB_DRIVER=postgres",
1✔
796
                        fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
1✔
797

1✔
798
                        "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl,
1✔
799
                        "GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","),
1✔
800
                        fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !utils.Config.Auth.EnableSignup),
1✔
801

1✔
802
                        "GOTRUE_JWT_ADMIN_ROLES=service_role",
1✔
803
                        "GOTRUE_JWT_AUD=authenticated",
1✔
804
                        "GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated",
1✔
805
                        fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry),
1✔
806
                        "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
807
                        "GOTRUE_JWT_ISSUER=" + utils.GetApiUrl("/auth/v1"),
1✔
808

1✔
809
                        fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", utils.Config.Auth.Email.EnableSignup),
1✔
810
                        fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", utils.Config.Auth.Email.DoubleConfirmChanges),
1✔
811
                        fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !utils.Config.Auth.Email.EnableConfirmations),
1✔
812
                        fmt.Sprintf("GOTRUE_MAILER_OTP_LENGTH=%v", utils.Config.Auth.Email.OtpLength),
1✔
813
                        fmt.Sprintf("GOTRUE_MAILER_OTP_EXP=%v", utils.Config.Auth.Email.OtpExpiry),
1✔
814

1✔
815
                        fmt.Sprintf("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=%v", utils.Config.Auth.EnableAnonymousSignIns),
1✔
816

1✔
817
                        fmt.Sprintf("GOTRUE_SMTP_MAX_FREQUENCY=%v", utils.Config.Auth.Email.MaxFrequency),
1✔
818

1✔
819
                        "GOTRUE_MAILER_URLPATHS_INVITE=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
820
                        "GOTRUE_MAILER_URLPATHS_CONFIRMATION=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
821
                        "GOTRUE_MAILER_URLPATHS_RECOVERY=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
822
                        "GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
823
                        "GOTRUE_RATE_LIMIT_EMAIL_SENT=360000",
1✔
824

1✔
825
                        fmt.Sprintf("GOTRUE_EXTERNAL_PHONE_ENABLED=%v", utils.Config.Auth.Sms.EnableSignup),
1✔
826
                        fmt.Sprintf("GOTRUE_SMS_AUTOCONFIRM=%v", !utils.Config.Auth.Sms.EnableConfirmations),
1✔
827
                        fmt.Sprintf("GOTRUE_SMS_MAX_FREQUENCY=%v", utils.Config.Auth.Sms.MaxFrequency),
1✔
828
                        "GOTRUE_SMS_OTP_EXP=6000",
1✔
829
                        "GOTRUE_SMS_OTP_LENGTH=6",
1✔
830
                        fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template),
1✔
831
                        "GOTRUE_SMS_TEST_OTP=" + testOTP.String(),
1✔
832

1✔
833
                        fmt.Sprintf("GOTRUE_PASSWORD_MIN_LENGTH=%v", utils.Config.Auth.MinimumPasswordLength),
1✔
834
                        fmt.Sprintf("GOTRUE_PASSWORD_REQUIRED_CHARACTERS=%v", utils.Config.Auth.PasswordRequirements.ToChar()),
1✔
835
                        fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation),
1✔
836
                        fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval),
1✔
837
                        fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking),
1✔
838
                        fmt.Sprintf("GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=%v", utils.Config.Auth.Email.SecurePasswordChange),
1✔
839
                        fmt.Sprintf("GOTRUE_MFA_PHONE_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.Phone.EnrollEnabled),
1✔
840
                        fmt.Sprintf("GOTRUE_MFA_PHONE_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.Phone.VerifyEnabled),
1✔
841
                        fmt.Sprintf("GOTRUE_MFA_TOTP_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.TOTP.EnrollEnabled),
1✔
842
                        fmt.Sprintf("GOTRUE_MFA_TOTP_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.TOTP.VerifyEnabled),
1✔
843
                        fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.EnrollEnabled),
1✔
844
                        fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.VerifyEnabled),
1✔
845
                        fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors),
1✔
846

1✔
847
                        // Add rate limit configurations
1✔
848
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_ANONYMOUS_USERS=%v", utils.Config.Auth.RateLimit.AnonymousUsers),
1✔
849
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_TOKEN_REFRESH=%v", utils.Config.Auth.RateLimit.TokenRefresh),
1✔
850
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_OTP=%v", utils.Config.Auth.RateLimit.SignInSignUps),
1✔
851
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_VERIFY=%v", utils.Config.Auth.RateLimit.TokenVerifications),
1✔
852
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_SMS_SENT=%v", utils.Config.Auth.RateLimit.SmsSent),
1✔
853
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_WEB3=%v", utils.Config.Auth.RateLimit.Web3),
1✔
854
                }
1✔
855

1✔
856
                // Since signing key is validated by ResolveJWKS, simply read the key file.
1✔
857
                if keys, err := afero.ReadFile(fsys, utils.Config.Auth.SigningKeysPath); err == nil && len(keys) > 0 {
1✔
858
                        env = append(env, "GOTRUE_JWT_KEYS="+string(keys))
×
859
                }
×
860

861
                if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.Enabled {
1✔
862
                        env = append(env,
×
863
                                fmt.Sprintf("GOTRUE_RATE_LIMIT_EMAIL_SENT=%v", utils.Config.Auth.RateLimit.EmailSent),
×
864
                                fmt.Sprintf("GOTRUE_SMTP_HOST=%s", utils.Config.Auth.Email.Smtp.Host),
×
865
                                fmt.Sprintf("GOTRUE_SMTP_PORT=%d", utils.Config.Auth.Email.Smtp.Port),
×
866
                                fmt.Sprintf("GOTRUE_SMTP_USER=%s", utils.Config.Auth.Email.Smtp.User),
×
867
                                fmt.Sprintf("GOTRUE_SMTP_PASS=%s", utils.Config.Auth.Email.Smtp.Pass.Value),
×
868
                                fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Auth.Email.Smtp.AdminEmail),
×
869
                                fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Auth.Email.Smtp.SenderName),
×
870
                        )
×
871
                } else if utils.Config.Inbucket.Enabled {
2✔
872
                        env = append(env,
1✔
873
                                "GOTRUE_SMTP_HOST="+utils.InbucketId,
1✔
874
                                "GOTRUE_SMTP_PORT=1025",
1✔
875
                                fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Inbucket.AdminEmail),
1✔
876
                                fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Inbucket.SenderName),
1✔
877
                        )
1✔
878
                }
1✔
879

880
                if utils.Config.Auth.Sessions.Timebox > 0 {
1✔
881
                        env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_TIMEBOX=%v", utils.Config.Auth.Sessions.Timebox))
×
882
                }
×
883
                if utils.Config.Auth.Sessions.InactivityTimeout > 0 {
1✔
884
                        env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_INACTIVITY_TIMEOUT=%v", utils.Config.Auth.Sessions.InactivityTimeout))
×
885
                }
×
886

887
                for id, tmpl := range utils.Config.Auth.Email.Template {
1✔
888
                        if len(tmpl.ContentPath) > 0 {
×
889
                                env = append(env, fmt.Sprintf("GOTRUE_MAILER_TEMPLATES_%s=http://%s:%d/email/%s",
×
890
                                        strings.ToUpper(id),
×
891
                                        utils.KongId,
×
892
                                        nginxTemplateServerPort,
×
893
                                        id+filepath.Ext(tmpl.ContentPath),
×
894
                                ))
×
895
                        }
×
896
                        if tmpl.Subject != nil {
×
897
                                env = append(env, fmt.Sprintf("GOTRUE_MAILER_SUBJECTS_%s=%s",
×
898
                                        strings.ToUpper(id),
×
899
                                        *tmpl.Subject,
×
900
                                ))
×
901
                        }
×
902
                }
903

904
                switch {
1✔
905
                case utils.Config.Auth.Sms.Twilio.Enabled:
×
906
                        env = append(
×
907
                                env,
×
908
                                "GOTRUE_SMS_PROVIDER=twilio",
×
909
                                "GOTRUE_SMS_TWILIO_ACCOUNT_SID="+utils.Config.Auth.Sms.Twilio.AccountSid,
×
910
                                "GOTRUE_SMS_TWILIO_AUTH_TOKEN="+utils.Config.Auth.Sms.Twilio.AuthToken.Value,
×
911
                                "GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.Twilio.MessageServiceSid,
×
912
                        )
×
913
                case utils.Config.Auth.Sms.TwilioVerify.Enabled:
×
914
                        env = append(
×
915
                                env,
×
916
                                "GOTRUE_SMS_PROVIDER=twilio_verify",
×
917
                                "GOTRUE_SMS_TWILIO_VERIFY_ACCOUNT_SID="+utils.Config.Auth.Sms.TwilioVerify.AccountSid,
×
918
                                "GOTRUE_SMS_TWILIO_VERIFY_AUTH_TOKEN="+utils.Config.Auth.Sms.TwilioVerify.AuthToken.Value,
×
919
                                "GOTRUE_SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.TwilioVerify.MessageServiceSid,
×
920
                        )
×
921
                case utils.Config.Auth.Sms.Messagebird.Enabled:
×
922
                        env = append(
×
923
                                env,
×
924
                                "GOTRUE_SMS_PROVIDER=messagebird",
×
925
                                "GOTRUE_SMS_MESSAGEBIRD_ACCESS_KEY="+utils.Config.Auth.Sms.Messagebird.AccessKey.Value,
×
926
                                "GOTRUE_SMS_MESSAGEBIRD_ORIGINATOR="+utils.Config.Auth.Sms.Messagebird.Originator,
×
927
                        )
×
928
                case utils.Config.Auth.Sms.Textlocal.Enabled:
×
929
                        env = append(
×
930
                                env,
×
931
                                "GOTRUE_SMS_PROVIDER=textlocal",
×
932
                                "GOTRUE_SMS_TEXTLOCAL_API_KEY="+utils.Config.Auth.Sms.Textlocal.ApiKey.Value,
×
933
                                "GOTRUE_SMS_TEXTLOCAL_SENDER="+utils.Config.Auth.Sms.Textlocal.Sender,
×
934
                        )
×
935
                case utils.Config.Auth.Sms.Vonage.Enabled:
×
936
                        env = append(
×
937
                                env,
×
938
                                "GOTRUE_SMS_PROVIDER=vonage",
×
939
                                "GOTRUE_SMS_VONAGE_API_KEY="+utils.Config.Auth.Sms.Vonage.ApiKey,
×
940
                                "GOTRUE_SMS_VONAGE_API_SECRET="+utils.Config.Auth.Sms.Vonage.ApiSecret.Value,
×
941
                                "GOTRUE_SMS_VONAGE_FROM="+utils.Config.Auth.Sms.Vonage.From,
×
942
                        )
×
943
                }
944

945
                if captcha := utils.Config.Auth.Captcha; captcha != nil {
1✔
946
                        env = append(
×
947
                                env,
×
948
                                fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_ENABLED=%v", captcha.Enabled),
×
949
                                fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_PROVIDER=%v", captcha.Provider),
×
950
                                fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_SECRET=%v", captcha.Secret.Value),
×
951
                        )
×
952
                }
×
953

954
                if hook := utils.Config.Auth.Hook.MFAVerificationAttempt; hook != nil && hook.Enabled {
1✔
955
                        env = append(
×
956
                                env,
×
957
                                "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED=true",
×
958
                                "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="+hook.URI,
×
959
                                "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value,
×
960
                        )
×
961
                }
×
962
                if hook := utils.Config.Auth.Hook.PasswordVerificationAttempt; hook != nil && hook.Enabled {
1✔
963
                        env = append(
×
964
                                env,
×
965
                                "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED=true",
×
966
                                "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="+hook.URI,
×
967
                                "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value,
×
968
                        )
×
969
                }
×
970
                if hook := utils.Config.Auth.Hook.CustomAccessToken; hook != nil && hook.Enabled {
1✔
971
                        env = append(
×
972
                                env,
×
973
                                "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED=true",
×
974
                                "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="+hook.URI,
×
975
                                "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS="+hook.Secrets.Value,
×
976
                        )
×
977
                }
×
978
                if hook := utils.Config.Auth.Hook.SendSMS; hook != nil && hook.Enabled {
1✔
979
                        env = append(
×
980
                                env,
×
981
                                "GOTRUE_HOOK_SEND_SMS_ENABLED=true",
×
982
                                "GOTRUE_HOOK_SEND_SMS_URI="+hook.URI,
×
983
                                "GOTRUE_HOOK_SEND_SMS_SECRETS="+hook.Secrets.Value,
×
984
                        )
×
985
                }
×
986
                if hook := utils.Config.Auth.Hook.SendEmail; hook != nil && hook.Enabled {
1✔
987
                        env = append(
×
988
                                env,
×
989
                                "GOTRUE_HOOK_SEND_EMAIL_ENABLED=true",
×
990
                                "GOTRUE_HOOK_SEND_EMAIL_URI="+hook.URI,
×
991
                                "GOTRUE_HOOK_SEND_EMAIL_SECRETS="+hook.Secrets.Value,
×
992
                        )
×
993
                }
×
994
                if hook := utils.Config.Auth.Hook.BeforeUserCreated; hook != nil && hook.Enabled {
1✔
995
                        env = append(
×
996
                                env,
×
997
                                "GOTRUE_HOOK_BEFORE_USER_CREATED_ENABLED=true",
×
998
                                "GOTRUE_HOOK_BEFORE_USER_CREATED_URI="+hook.URI,
×
999
                                "GOTRUE_HOOK_BEFORE_USER_CREATED_SECRETS="+hook.Secrets.Value,
×
1000
                        )
×
1001
                }
×
1002

1003
                if utils.Config.Auth.MFA.Phone.EnrollEnabled || utils.Config.Auth.MFA.Phone.VerifyEnabled {
1✔
1004
                        env = append(
×
1005
                                env,
×
1006
                                "GOTRUE_MFA_PHONE_TEMPLATE="+utils.Config.Auth.MFA.Phone.Template,
×
1007
                                fmt.Sprintf("GOTRUE_MFA_PHONE_OTP_LENGTH=%v", utils.Config.Auth.MFA.Phone.OtpLength),
×
1008
                                fmt.Sprintf("GOTRUE_MFA_PHONE_MAX_FREQUENCY=%v", utils.Config.Auth.MFA.Phone.MaxFrequency),
×
1009
                        )
×
1010
                }
×
1011

1012
                for name, config := range utils.Config.Auth.External {
2✔
1013
                        env = append(
1✔
1014
                                env,
1✔
1015
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled),
1✔
1016
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId),
1✔
1017
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret.Value),
1✔
1018
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_SKIP_NONCE_CHECK=%t", strings.ToUpper(name), config.SkipNonceCheck),
1✔
1019
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_EMAIL_OPTIONAL=%t", strings.ToUpper(name), config.EmailOptional),
1✔
1020
                        )
1✔
1021

1✔
1022
                        redirectUri := config.RedirectUri
1✔
1023
                        if redirectUri == "" {
2✔
1024
                                redirectUri = utils.GetApiUrl("/auth/v1/callback")
1✔
1025
                        }
1✔
1026
                        env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), redirectUri))
1✔
1027

1✔
1028
                        if config.Url != "" {
1✔
1029
                                env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url))
×
1030
                        }
×
1031
                }
1032
                env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_WEB3_SOLANA_ENABLED=%v", utils.Config.Auth.Web3.Solana.Enabled))
1✔
1033

1✔
1034
                // OAuth server configuration
1✔
1035
                if utils.Config.Auth.OAuthServer.Enabled {
1✔
1036
                        env = append(env,
×
1037
                                fmt.Sprintf("GOTRUE_OAUTH_SERVER_ENABLED=%v", utils.Config.Auth.OAuthServer.Enabled),
×
1038
                                "GOTRUE_OAUTH_SERVER_AUTHORIZATION_PATH="+utils.Config.Auth.OAuthServer.AuthorizationUrlPath,
×
1039
                                fmt.Sprintf("GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION=%v", utils.Config.Auth.OAuthServer.AllowDynamicRegistration),
×
1040
                        )
×
1041
                }
×
1042

1043
                if _, err := utils.DockerStart(
1✔
1044
                        ctx,
1✔
1045
                        container.Config{
1✔
1046
                                Image:        utils.Config.Auth.Image,
1✔
1047
                                Env:          env,
1✔
1048
                                ExposedPorts: nat.PortSet{"9999/tcp": {}},
1✔
1049
                                Healthcheck: &container.HealthConfig{
1✔
1050
                                        Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
1✔
1051
                                                "http://127.0.0.1:9999/health",
1✔
1052
                                        },
1✔
1053
                                        Interval: 10 * time.Second,
1✔
1054
                                        Timeout:  2 * time.Second,
1✔
1055
                                        Retries:  3,
1✔
1056
                                },
1✔
1057
                        },
1✔
1058
                        container.HostConfig{
1✔
1059
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1060
                        },
1✔
1061
                        network.NetworkingConfig{
1✔
1062
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1063
                                        utils.NetId: {
1✔
1064
                                                Aliases: utils.GotrueAliases,
1✔
1065
                                        },
1✔
1066
                                },
1✔
1067
                        },
1✔
1068
                        utils.GotrueId,
1✔
1069
                ); err != nil {
1✔
1070
                        return err
×
1071
                }
×
1072
                started = append(started, utils.GotrueId)
1✔
1073
        }
1074

1075
        // Start Mailpit
1076
        if utils.Config.Inbucket.Enabled && !isContainerExcluded(utils.Config.Inbucket.Image, excluded) {
3✔
1077
                inbucketPortBindings := nat.PortMap{"8025/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Port), 10)}}}
1✔
1078
                if utils.Config.Inbucket.SmtpPort != 0 {
1✔
1079
                        inbucketPortBindings["1025/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.SmtpPort), 10)}}
×
1080
                }
×
1081
                if utils.Config.Inbucket.Pop3Port != 0 {
1✔
1082
                        inbucketPortBindings["1110/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Pop3Port), 10)}}
×
1083
                }
×
1084
                if _, err := utils.DockerStart(
1✔
1085
                        ctx,
1✔
1086
                        container.Config{
1✔
1087
                                Image: utils.Config.Inbucket.Image,
1✔
1088
                                Healthcheck: &container.HealthConfig{
1✔
1089
                                        Test:     []string{"CMD", "/mailpit", "readyz"},
1✔
1090
                                        Interval: 10 * time.Second,
1✔
1091
                                        Timeout:  2 * time.Second,
1✔
1092
                                        Retries:  3,
1✔
1093
                                        // StartPeriod taken from upstream Dockerfile
1✔
1094
                                        StartPeriod: 10 * time.Second,
1✔
1095
                                },
1✔
1096
                        },
1✔
1097
                        container.HostConfig{
1✔
1098
                                PortBindings:  inbucketPortBindings,
1✔
1099
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1100
                        },
1✔
1101
                        network.NetworkingConfig{
1✔
1102
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1103
                                        utils.NetId: {
1✔
1104
                                                Aliases: utils.InbucketAliases,
1✔
1105
                                        },
1✔
1106
                                },
1✔
1107
                        },
1✔
1108
                        utils.InbucketId,
1✔
1109
                ); err != nil {
1✔
1110
                        return err
×
1111
                }
×
1112
                started = append(started, utils.InbucketId)
1✔
1113
        }
1114

1115
        // Start Realtime.
1116
        if utils.Config.Realtime.Enabled && !isContainerExcluded(utils.Config.Realtime.Image, excluded) {
3✔
1117
                if _, err := utils.DockerStart(
1✔
1118
                        ctx,
1✔
1119
                        container.Config{
1✔
1120
                                Image: utils.Config.Realtime.Image,
1✔
1121
                                Env: []string{
1✔
1122
                                        "PORT=4000",
1✔
1123
                                        "DB_HOST=" + dbConfig.Host,
1✔
1124
                                        fmt.Sprintf("DB_PORT=%d", dbConfig.Port),
1✔
1125
                                        "DB_USER=" + utils.SUPERUSER_ROLE,
1✔
1126
                                        "DB_PASSWORD=" + dbConfig.Password,
1✔
1127
                                        "DB_NAME=" + dbConfig.Database,
1✔
1128
                                        "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime",
1✔
1129
                                        "DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey,
1✔
1130
                                        "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
1131
                                        fmt.Sprintf("API_JWT_JWKS=%s", jwks),
1✔
1132
                                        "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
1133
                                        "APP_NAME=realtime",
1✔
1134
                                        "SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase,
1✔
1135
                                        "ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion),
1✔
1136
                                        "DNS_NODES=''",
1✔
1137
                                        "RLIMIT_NOFILE=",
1✔
1138
                                        "SEED_SELF_HOST=true",
1✔
1139
                                        "RUN_JANITOR=true",
1✔
1140
                                        fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength),
1✔
1141
                                },
1✔
1142
                                ExposedPorts: nat.PortSet{"4000/tcp": {}},
1✔
1143
                                Healthcheck: &container.HealthConfig{
1✔
1144
                                        // Podman splits command by spaces unless it's quoted, but curl header can't be quoted.
1✔
1145
                                        Test: []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null",
1✔
1146
                                                "-H", "Host:" + utils.Config.Realtime.TenantId,
1✔
1147
                                                "http://127.0.0.1:4000/api/ping",
1✔
1148
                                        },
1✔
1149
                                        Interval: 10 * time.Second,
1✔
1150
                                        Timeout:  2 * time.Second,
1✔
1151
                                        Retries:  3,
1✔
1152
                                },
1✔
1153
                        },
1✔
1154
                        container.HostConfig{
1✔
1155
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1156
                        },
1✔
1157
                        network.NetworkingConfig{
1✔
1158
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1159
                                        utils.NetId: {
1✔
1160
                                                Aliases: utils.RealtimeAliases,
1✔
1161
                                        },
1✔
1162
                                },
1✔
1163
                        },
1✔
1164
                        utils.RealtimeId,
1✔
1165
                ); err != nil {
1✔
1166
                        return err
×
1167
                }
×
1168
                started = append(started, utils.RealtimeId)
1✔
1169
        }
1170

1171
        // Start PostgREST.
1172
        if utils.Config.Api.Enabled && !isContainerExcluded(utils.Config.Api.Image, excluded) {
3✔
1173
                if _, err := utils.DockerStart(
1✔
1174
                        ctx,
1✔
1175
                        container.Config{
1✔
1176
                                Image: utils.Config.Api.Image,
1✔
1177
                                Env: []string{
1✔
1178
                                        fmt.Sprintf("PGRST_DB_URI=postgresql://authenticator:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
1✔
1179
                                        "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","),
1✔
1180
                                        "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","),
1✔
1181
                                        fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows),
1✔
1182
                                        "PGRST_DB_ANON_ROLE=anon",
1✔
1183
                                        fmt.Sprintf("PGRST_JWT_SECRET=%s", jwks),
1✔
1184
                                        "PGRST_ADMIN_SERVER_PORT=3001",
1✔
1185
                                },
1✔
1186
                                // PostgREST does not expose a shell for health check
1✔
1187
                        },
1✔
1188
                        container.HostConfig{
1✔
1189
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1190
                        },
1✔
1191
                        network.NetworkingConfig{
1✔
1192
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1193
                                        utils.NetId: {
1✔
1194
                                                Aliases: utils.RestAliases,
1✔
1195
                                        },
1✔
1196
                                },
1✔
1197
                        },
1✔
1198
                        utils.RestId,
1✔
1199
                ); err != nil {
1✔
1200
                        return err
×
1201
                }
×
1202
                started = append(started, utils.RestId)
1✔
1203
        }
1204

1205
        // Start Storage.
1206
        if isStorageEnabled {
3✔
1207
                dockerStoragePath := "/mnt"
1✔
1208
                if _, err := utils.DockerStart(
1✔
1209
                        ctx,
1✔
1210
                        container.Config{
1✔
1211
                                Image: utils.Config.Storage.Image,
1✔
1212
                                Env: []string{
1✔
1213
                                        "DB_MIGRATIONS_FREEZE_AT=" + utils.Config.Storage.TargetMigration,
1✔
1214
                                        "ANON_KEY=" + utils.Config.Auth.AnonKey.Value,
1✔
1215
                                        "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value,
1✔
1216
                                        "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
1217
                                        fmt.Sprintf("JWT_JWKS=%s", jwks),
1✔
1218
                                        fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
1✔
1219
                                        fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
1✔
1220
                                        "STORAGE_BACKEND=file",
1✔
1221
                                        "FILE_STORAGE_BACKEND_PATH=" + dockerStoragePath,
1✔
1222
                                        "TENANT_ID=stub",
1✔
1223
                                        // TODO: https://github.com/supabase/storage-api/issues/55
1✔
1224
                                        "STORAGE_S3_REGION=" + utils.Config.Storage.S3Credentials.Region,
1✔
1225
                                        "GLOBAL_S3_BUCKET=stub",
1✔
1226
                                        fmt.Sprintf("ENABLE_IMAGE_TRANSFORMATION=%t", isImgProxyEnabled),
1✔
1227
                                        fmt.Sprintf("IMGPROXY_URL=http://%s:5001", utils.ImgProxyId),
1✔
1228
                                        "TUS_URL_PATH=/storage/v1/upload/resumable",
1✔
1229
                                        "S3_PROTOCOL_ACCESS_KEY_ID=" + utils.Config.Storage.S3Credentials.AccessKeyId,
1✔
1230
                                        "S3_PROTOCOL_ACCESS_KEY_SECRET=" + utils.Config.Storage.S3Credentials.SecretAccessKey,
1✔
1231
                                        "S3_PROTOCOL_PREFIX=/storage/v1",
1✔
1232
                                        "UPLOAD_FILE_SIZE_LIMIT=52428800000",
1✔
1233
                                        "UPLOAD_FILE_SIZE_LIMIT_STANDARD=5242880000",
1✔
1234
                                        "SIGNED_UPLOAD_URL_EXPIRATION_TIME=7200",
1✔
1235
                                },
1✔
1236
                                Healthcheck: &container.HealthConfig{
1✔
1237
                                        // For some reason, localhost resolves to IPv6 address on GitPod which breaks healthcheck.
1✔
1238
                                        Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
1✔
1239
                                                "http://127.0.0.1:5000/status",
1✔
1240
                                        },
1✔
1241
                                        Interval: 10 * time.Second,
1✔
1242
                                        Timeout:  2 * time.Second,
1✔
1243
                                        Retries:  3,
1✔
1244
                                },
1✔
1245
                        },
1✔
1246
                        container.HostConfig{
1✔
1247
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1248
                                Binds:         []string{utils.StorageId + ":" + dockerStoragePath},
1✔
1249
                        },
1✔
1250
                        network.NetworkingConfig{
1✔
1251
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1252
                                        utils.NetId: {
1✔
1253
                                                Aliases: utils.StorageAliases,
1✔
1254
                                        },
1✔
1255
                                },
1✔
1256
                        },
1✔
1257
                        utils.StorageId,
1✔
1258
                ); err != nil {
1✔
1259
                        return err
×
1260
                }
×
1261
                started = append(started, utils.StorageId)
1✔
1262
        }
1263

1264
        // Start Storage ImgProxy.
1265
        if isStorageEnabled && isImgProxyEnabled {
2✔
1266
                if _, err := utils.DockerStart(
×
1267
                        ctx,
×
1268
                        container.Config{
×
1269
                                Image: utils.Config.Storage.ImgProxyImage,
×
1270
                                Env: []string{
×
1271
                                        "IMGPROXY_BIND=:5001",
×
1272
                                        "IMGPROXY_LOCAL_FILESYSTEM_ROOT=/",
×
1273
                                        "IMGPROXY_USE_ETAG=/",
×
1274
                                        "IMGPROXY_MAX_SRC_RESOLUTION=50",
×
1275
                                        "IMGPROXY_MAX_SRC_FILE_SIZE=25000000",
×
1276
                                        "IMGPROXY_MAX_ANIMATION_FRAMES=60",
×
1277
                                        "IMGPROXY_ENABLE_WEBP_DETECTION=true",
×
1278
                                        "IMGPROXY_PRESETS=default=width:3000/height:8192",
×
1279
                                        "IMGPROXY_FORMAT_QUALITY=jpeg=80,avif=62,webp=80",
×
1280
                                },
×
1281
                                Healthcheck: &container.HealthConfig{
×
1282
                                        Test:     []string{"CMD", "imgproxy", "health"},
×
1283
                                        Interval: 10 * time.Second,
×
1284
                                        Timeout:  2 * time.Second,
×
1285
                                        Retries:  3,
×
1286
                                },
×
1287
                        },
×
1288
                        container.HostConfig{
×
1289
                                VolumesFrom:   []string{utils.StorageId},
×
1290
                                RestartPolicy: container.RestartPolicy{Name: "always"},
×
1291
                        },
×
1292
                        network.NetworkingConfig{
×
1293
                                EndpointsConfig: map[string]*network.EndpointSettings{
×
1294
                                        utils.NetId: {
×
1295
                                                Aliases: utils.ImgProxyAliases,
×
1296
                                        },
×
1297
                                },
×
1298
                        },
×
1299
                        utils.ImgProxyId,
×
1300
                ); err != nil {
×
1301
                        return err
×
1302
                }
×
1303
                started = append(started, utils.ImgProxyId)
×
1304
        }
1305

1306
        // Start all functions.
1307
        if utils.Config.EdgeRuntime.Enabled && !isContainerExcluded(utils.Config.EdgeRuntime.Image, excluded) {
3✔
1308
                dbUrl := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database)
1✔
1309
                if err := serve.ServeFunctions(ctx, "", nil, "", dbUrl, serve.RuntimeOption{}, fsys); err != nil {
1✔
1310
                        return err
×
1311
                }
×
1312
                started = append(started, utils.EdgeRuntimeId)
1✔
1313
        }
1314

1315
        // Start pg-meta.
1316
        if utils.Config.Studio.Enabled && !isContainerExcluded(utils.Config.Studio.PgmetaImage, excluded) {
3✔
1317
                if _, err := utils.DockerStart(
1✔
1318
                        ctx,
1✔
1319
                        container.Config{
1✔
1320
                                Image: utils.Config.Studio.PgmetaImage,
1✔
1321
                                Env: []string{
1✔
1322
                                        "PG_META_PORT=8080",
1✔
1323
                                        "PG_META_DB_HOST=" + dbConfig.Host,
1✔
1324
                                        "PG_META_DB_NAME=" + dbConfig.Database,
1✔
1325
                                        "PG_META_DB_USER=" + dbConfig.User,
1✔
1326
                                        fmt.Sprintf("PG_META_DB_PORT=%d", dbConfig.Port),
1✔
1327
                                        "PG_META_DB_PASSWORD=" + dbConfig.Password,
1✔
1328
                                },
1✔
1329
                                Healthcheck: &container.HealthConfig{
1✔
1330
                                        Test:     []string{"CMD-SHELL", `node --eval="fetch('http://127.0.0.1:8080/health').then((r) => {if (!r.ok) throw new Error(r.status)})"`},
1✔
1331
                                        Interval: 10 * time.Second,
1✔
1332
                                        Timeout:  2 * time.Second,
1✔
1333
                                        Retries:  3,
1✔
1334
                                },
1✔
1335
                        },
1✔
1336
                        container.HostConfig{
1✔
1337
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1338
                        },
1✔
1339
                        network.NetworkingConfig{
1✔
1340
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1341
                                        utils.NetId: {
1✔
1342
                                                Aliases: utils.PgmetaAliases,
1✔
1343
                                        },
1✔
1344
                                },
1✔
1345
                        },
1✔
1346
                        utils.PgmetaId,
1✔
1347
                ); err != nil {
1✔
1348
                        return err
×
1349
                }
×
1350
                started = append(started, utils.PgmetaId)
1✔
1351
        }
1352

1353
        // Start Studio.
1354
        if utils.Config.Studio.Enabled && !isContainerExcluded(utils.Config.Studio.Image, excluded) {
3✔
1355
                if _, err := utils.DockerStart(
1✔
1356
                        ctx,
1✔
1357
                        container.Config{
1✔
1358
                                Image: utils.Config.Studio.Image,
1✔
1359
                                Env: []string{
1✔
1360
                                        "CURRENT_CLI_VERSION=" + utils.Version,
1✔
1361
                                        "STUDIO_PG_META_URL=http://" + utils.PgmetaId + ":8080",
1✔
1362
                                        "POSTGRES_PASSWORD=" + dbConfig.Password,
1✔
1363
                                        "SUPABASE_URL=http://" + utils.KongId + ":8000",
1✔
1364
                                        "SUPABASE_PUBLIC_URL=" + utils.Config.Studio.ApiUrl,
1✔
1365
                                        "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
1366
                                        "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey.Value,
1✔
1367
                                        "SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value,
1✔
1368
                                        "LOGFLARE_PRIVATE_ACCESS_TOKEN=" + utils.Config.Analytics.ApiKey,
1✔
1369
                                        "OPENAI_API_KEY=" + utils.Config.Studio.OpenaiApiKey.Value,
1✔
1370
                                        fmt.Sprintf("LOGFLARE_URL=http://%v:4000", utils.LogflareId),
1✔
1371
                                        fmt.Sprintf("NEXT_PUBLIC_ENABLE_LOGS=%v", utils.Config.Analytics.Enabled),
1✔
1372
                                        fmt.Sprintf("NEXT_ANALYTICS_BACKEND_PROVIDER=%v", utils.Config.Analytics.Backend),
1✔
1373
                                        // Ref: https://github.com/vercel/next.js/issues/51684#issuecomment-1612834913
1✔
1374
                                        "HOSTNAME=0.0.0.0",
1✔
1375
                                },
1✔
1376
                                Healthcheck: &container.HealthConfig{
1✔
1377
                                        Test:     []string{"CMD-SHELL", `node --eval="fetch('http://127.0.0.1:3000/api/platform/profile').then((r) => {if (!r.ok) throw new Error(r.status)})"`},
1✔
1378
                                        Interval: 10 * time.Second,
1✔
1379
                                        Timeout:  2 * time.Second,
1✔
1380
                                        Retries:  3,
1✔
1381
                                },
1✔
1382
                        },
1✔
1383
                        container.HostConfig{
1✔
1384
                                PortBindings:  nat.PortMap{"3000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Studio.Port), 10)}}},
1✔
1385
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1386
                        },
1✔
1387
                        network.NetworkingConfig{
1✔
1388
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1389
                                        utils.NetId: {
1✔
1390
                                                Aliases: utils.StudioAliases,
1✔
1391
                                        },
1✔
1392
                                },
1✔
1393
                        },
1✔
1394
                        utils.StudioId,
1✔
1395
                ); err != nil {
1✔
1396
                        return err
×
1397
                }
×
1398
                started = append(started, utils.StudioId)
1✔
1399
        }
1400

1401
        // Start pooler.
1402
        if utils.Config.Db.Pooler.Enabled && !isContainerExcluded(utils.Config.Db.Pooler.Image, excluded) {
2✔
1403
                portSession := uint16(5432)
×
1404
                portTransaction := uint16(6543)
×
1405
                dockerPort := portTransaction
×
1406
                if utils.Config.Db.Pooler.PoolMode == config.SessionMode {
×
1407
                        dockerPort = portSession
×
1408
                }
×
1409
                // Create pooler tenant
1410
                var poolerTenantBuf bytes.Buffer
×
1411
                if err := poolerTenantTemplate.Option("missingkey=error").Execute(&poolerTenantBuf, poolerTenant{
×
1412
                        DbHost:            dbConfig.Host,
×
1413
                        DbPort:            dbConfig.Port,
×
1414
                        DbDatabase:        dbConfig.Database,
×
1415
                        DbPassword:        dbConfig.Password,
×
1416
                        ExternalId:        utils.Config.Db.Pooler.TenantId,
×
1417
                        ModeType:          utils.Config.Db.Pooler.PoolMode,
×
1418
                        DefaultMaxClients: utils.Config.Db.Pooler.MaxClientConn,
×
1419
                        DefaultPoolSize:   utils.Config.Db.Pooler.DefaultPoolSize,
×
1420
                }); err != nil {
×
1421
                        return errors.Errorf("failed to exec template: %w", err)
×
1422
                }
×
1423
                if _, err := utils.DockerStart(
×
1424
                        ctx,
×
1425
                        container.Config{
×
1426
                                Image: utils.Config.Db.Pooler.Image,
×
1427
                                Env: []string{
×
1428
                                        "PORT=4000",
×
1429
                                        fmt.Sprintf("PROXY_PORT_SESSION=%d", portSession),
×
1430
                                        fmt.Sprintf("PROXY_PORT_TRANSACTION=%d", portTransaction),
×
1431
                                        fmt.Sprintf("DATABASE_URL=ecto://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, "_supabase"),
×
1432
                                        "CLUSTER_POSTGRES=true",
×
1433
                                        "SECRET_KEY_BASE=" + utils.Config.Db.Pooler.SecretKeyBase,
×
1434
                                        "VAULT_ENC_KEY=" + utils.Config.Db.Pooler.EncryptionKey,
×
1435
                                        "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
×
1436
                                        "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
×
1437
                                        "REGION=local",
×
1438
                                        "RUN_JANITOR=true",
×
1439
                                        "ERL_AFLAGS=-proto_dist inet_tcp",
×
1440
                                },
×
1441
                                Cmd: []string{
×
1442
                                        "/bin/sh", "-c",
×
1443
                                        fmt.Sprintf("/app/bin/migrate && /app/bin/supavisor eval '%s' && /app/bin/server", poolerTenantBuf.String()),
×
1444
                                },
×
1445
                                ExposedPorts: nat.PortSet{
×
1446
                                        "4000/tcp": {},
×
1447
                                        nat.Port(fmt.Sprintf("%d/tcp", portSession)):     {},
×
1448
                                        nat.Port(fmt.Sprintf("%d/tcp", portTransaction)): {},
×
1449
                                },
×
1450
                                Healthcheck: &container.HealthConfig{
×
1451
                                        Test:     []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null", "http://127.0.0.1:4000/api/health"},
×
1452
                                        Interval: 10 * time.Second,
×
1453
                                        Timeout:  2 * time.Second,
×
1454
                                        Retries:  3,
×
1455
                                },
×
1456
                        },
×
1457
                        container.HostConfig{
×
1458
                                PortBindings: nat.PortMap{nat.Port(fmt.Sprintf("%d/tcp", dockerPort)): []nat.PortBinding{{
×
1459
                                        HostPort: strconv.FormatUint(uint64(utils.Config.Db.Pooler.Port), 10)},
×
1460
                                }},
×
1461
                                RestartPolicy: container.RestartPolicy{Name: "always"},
×
1462
                        },
×
1463
                        network.NetworkingConfig{
×
1464
                                EndpointsConfig: map[string]*network.EndpointSettings{
×
1465
                                        utils.NetId: {
×
1466
                                                Aliases: utils.PoolerAliases,
×
1467
                                        },
×
1468
                                },
×
1469
                        },
×
1470
                        utils.PoolerId,
×
1471
                ); err != nil {
×
1472
                        return err
×
1473
                }
×
1474
                started = append(started, utils.PoolerId)
×
1475
        }
1476

1477
        fmt.Fprintln(os.Stderr, "Waiting for health checks...")
2✔
1478
        if utils.NoBackupVolume && slices.Contains(started, utils.StorageId) {
3✔
1479
                if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.StorageId); err != nil {
1✔
1480
                        return err
×
1481
                }
×
1482
                // Disable prompts when seeding
1483
                if err := buckets.Run(ctx, "", false, fsys); err != nil {
1✔
1484
                        return err
×
1485
                }
×
1486
        }
1487
        return start.WaitForHealthyService(ctx, serviceTimeout, started...)
2✔
1488
}
1489

1490
func isContainerExcluded(imageName string, excluded map[string]bool) bool {
42✔
1491
        short := utils.ShortContainerImageName(imageName)
42✔
1492
        val, ok := excluded[short]
42✔
1493
        return ok && val
42✔
1494
}
42✔
1495

1496
func ExcludableContainers() []string {
1✔
1497
        names := []string{}
1✔
1498
        for _, image := range config.Images.Services() {
14✔
1499
                names = append(names, utils.ShortContainerImageName(image))
13✔
1500
        }
13✔
1501
        return names
1✔
1502
}
1503

1504
func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) {
5✔
1505
        numOfKeyPairs := len(input)
5✔
1506
        i := 0
5✔
1507
        for k, v := range input {
15✔
1508
                output.WriteString(k)
10✔
1509
                output.WriteString(":")
10✔
1510
                output.WriteString(v)
10✔
1511
                i++
10✔
1512
                if i < numOfKeyPairs {
16✔
1513
                        output.WriteString(",")
6✔
1514
                }
6✔
1515
        }
1516
}
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