github.com/supabase/cli@v1.168.1/internal/start/start.go (about)

     1  package start
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	_ "embed"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"text/template"
    15  	"time"
    16  
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/network"
    19  	"github.com/docker/go-connections/nat"
    20  	"github.com/go-errors/errors"
    21  	"github.com/jackc/pgconn"
    22  	"github.com/jackc/pgx/v4"
    23  	"github.com/spf13/afero"
    24  	"github.com/supabase/cli/internal/db/reset"
    25  	"github.com/supabase/cli/internal/db/start"
    26  	"github.com/supabase/cli/internal/functions/serve"
    27  	"github.com/supabase/cli/internal/services"
    28  	"github.com/supabase/cli/internal/status"
    29  	"github.com/supabase/cli/internal/utils"
    30  	"github.com/supabase/cli/internal/utils/flags"
    31  )
    32  
    33  func suggestUpdateCmd(serviceImages map[string]string) string {
    34  	cmd := "You are running outdated service versions locally:\n"
    35  	for k, v := range serviceImages {
    36  		cmd += fmt.Sprintf("%s => %s\n", k, v)
    37  	}
    38  	cmd += fmt.Sprintf("Run %s to update them.", utils.Aqua("supabase link"))
    39  	return cmd
    40  }
    41  
    42  func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error {
    43  	// Sanity checks.
    44  	{
    45  		if err := utils.LoadConfigFS(fsys); err != nil {
    46  			return err
    47  		}
    48  		if err := utils.AssertSupabaseDbIsRunning(); err == nil {
    49  			fmt.Fprintln(os.Stderr, utils.Aqua("supabase start")+" is already running.")
    50  			utils.CmdSuggestion = fmt.Sprintf("Run %s to show status of local Supabase containers.", utils.Aqua("supabase status"))
    51  			return nil
    52  		} else if !errors.Is(err, utils.ErrNotRunning) {
    53  			return err
    54  		}
    55  		if _, err := utils.LoadAccessTokenFS(fsys); err == nil {
    56  			if ref, err := flags.LoadProjectRef(fsys); err == nil {
    57  				local := services.GetServiceImages()
    58  				remote := services.GetRemoteImages(ctx, ref)
    59  				for _, image := range local {
    60  					parts := strings.Split(image, ":")
    61  					if version, ok := remote[image]; ok && version == parts[1] {
    62  						delete(remote, image)
    63  					}
    64  				}
    65  				if len(remote) > 0 {
    66  					fmt.Fprintln(os.Stderr, suggestUpdateCmd(remote))
    67  				}
    68  			}
    69  		}
    70  	}
    71  
    72  	if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error {
    73  		dbConfig := pgconn.Config{
    74  			Host:     utils.DbId,
    75  			Port:     5432,
    76  			User:     "postgres",
    77  			Password: utils.Config.Db.Password,
    78  			Database: "postgres",
    79  		}
    80  		return run(p, ctx, fsys, excludedContainers, dbConfig)
    81  	}); err != nil {
    82  		if ignoreHealthCheck && errors.Is(err, reset.ErrUnhealthy) {
    83  			fmt.Fprintln(os.Stderr, err)
    84  		} else {
    85  			if err := utils.DockerRemoveAll(context.Background(), io.Discard); err != nil {
    86  				fmt.Fprintln(os.Stderr, err)
    87  			}
    88  			return err
    89  		}
    90  	}
    91  
    92  	fmt.Fprintf(os.Stderr, "Started %s local development setup.\n\n", utils.Aqua("supabase"))
    93  	status.PrettyPrint(os.Stdout, excludedContainers...)
    94  	return nil
    95  }
    96  
    97  type kongConfig struct {
    98  	GotrueId      string
    99  	RestId        string
   100  	RealtimeId    string
   101  	StorageId     string
   102  	PgmetaId      string
   103  	EdgeRuntimeId string
   104  	LogflareId    string
   105  	ApiHost       string
   106  	ApiPort       uint16
   107  }
   108  
   109  var (
   110  	//go:embed templates/kong.yml
   111  	kongConfigEmbed    string
   112  	kongConfigTemplate = template.Must(template.New("kongConfig").Parse(kongConfigEmbed))
   113  
   114  	//go:embed templates/custom_nginx.template
   115  	nginxConfigEmbed string
   116  	// Hardcoded configs which match nginxConfigEmbed
   117  	nginxEmailTemplateDir   = "/home/kong/templates/email"
   118  	nginxTemplateServerPort = 8088
   119  )
   120  
   121  type vectorConfig struct {
   122  	ApiKey        string
   123  	LogflareId    string
   124  	KongId        string
   125  	GotrueId      string
   126  	RestId        string
   127  	RealtimeId    string
   128  	StorageId     string
   129  	EdgeRuntimeId string
   130  	DbId          string
   131  }
   132  
   133  var (
   134  	//go:embed templates/vector.yaml
   135  	vectorConfigEmbed    string
   136  	vectorConfigTemplate = template.Must(template.New("vectorConfig").Parse(vectorConfigEmbed))
   137  )
   138  
   139  func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error {
   140  	excluded := make(map[string]bool)
   141  	for _, name := range excludedContainers {
   142  		excluded[name] = true
   143  	}
   144  
   145  	// Start vector
   146  	if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.VectorImage, excluded) {
   147  		var vectorConfigBuf bytes.Buffer
   148  		if err := vectorConfigTemplate.Execute(&vectorConfigBuf, vectorConfig{
   149  			ApiKey:        utils.Config.Analytics.ApiKey,
   150  			LogflareId:    utils.LogflareId,
   151  			KongId:        utils.KongId,
   152  			GotrueId:      utils.GotrueId,
   153  			RestId:        utils.RestId,
   154  			RealtimeId:    utils.RealtimeId,
   155  			StorageId:     utils.StorageId,
   156  			EdgeRuntimeId: utils.EdgeRuntimeId,
   157  			DbId:          utils.DbId,
   158  		}); err != nil {
   159  			return errors.Errorf("failed to exec template: %w", err)
   160  		}
   161  		p.Send(utils.StatusMsg("Starting syslog driver..."))
   162  		if _, err := utils.DockerStart(
   163  			ctx,
   164  			container.Config{
   165  				Image: utils.VectorImage,
   166  				Env: []string{
   167  					"VECTOR_CONFIG=/etc/vector/vector.yaml",
   168  				},
   169  				Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/vector/vector.yaml && vector
   170  ` + vectorConfigBuf.String() + `
   171  EOF
   172  `},
   173  				Healthcheck: &container.HealthConfig{
   174  					Test: []string{"CMD",
   175  						"wget",
   176  						"--no-verbose",
   177  						"--tries=1",
   178  						"--spider",
   179  						"http://127.0.0.1:9001/health"},
   180  					Interval: 10 * time.Second,
   181  					Timeout:  2 * time.Second,
   182  					Retries:  3,
   183  				},
   184  				ExposedPorts: nat.PortSet{"9000/tcp": {}},
   185  			},
   186  			container.HostConfig{
   187  				PortBindings:  nat.PortMap{"9000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Analytics.VectorPort), 10)}}},
   188  				RestartPolicy: container.RestartPolicy{Name: "always"},
   189  			},
   190  			network.NetworkingConfig{
   191  				EndpointsConfig: map[string]*network.EndpointSettings{
   192  					utils.NetId: {
   193  						Aliases: utils.VectorAliases,
   194  					},
   195  				},
   196  			},
   197  			utils.VectorId,
   198  		); err != nil {
   199  			return err
   200  		}
   201  		if err := reset.WaitForServiceReady(ctx, []string{utils.VectorId}); err != nil {
   202  			return err
   203  		}
   204  	}
   205  
   206  	// Start Postgres.
   207  	w := utils.StatusWriter{Program: p}
   208  	if dbConfig.Host == utils.DbId {
   209  		if err := start.StartDatabase(ctx, fsys, w, options...); err != nil {
   210  			return err
   211  		}
   212  	}
   213  
   214  	var started []string
   215  	// Start Logflare
   216  	if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.LogflareImage, excluded) {
   217  		env := []string{
   218  			"DB_DATABASE=" + dbConfig.Database,
   219  			"DB_HOSTNAME=" + dbConfig.Host,
   220  			fmt.Sprintf("DB_PORT=%d", dbConfig.Port),
   221  			"DB_SCHEMA=_analytics",
   222  			"DB_USERNAME=supabase_admin",
   223  			"DB_PASSWORD=" + dbConfig.Password,
   224  			"LOGFLARE_MIN_CLUSTER_SIZE=1",
   225  			"LOGFLARE_SINGLE_TENANT=true",
   226  			"LOGFLARE_SUPABASE_MODE=true",
   227  			"LOGFLARE_API_KEY=" + utils.Config.Analytics.ApiKey,
   228  			"LOGFLARE_LOG_LEVEL=warn",
   229  			"LOGFLARE_NODE_HOST=127.0.0.1",
   230  			"LOGFLARE_FEATURE_FLAG_OVERRIDE='multibackend=true'",
   231  			"RELEASE_COOKIE=cookie",
   232  		}
   233  		bind := []string{}
   234  
   235  		switch utils.Config.Analytics.Backend {
   236  		case utils.LogflareBigQuery:
   237  			workdir, err := os.Getwd()
   238  			if err != nil {
   239  				return errors.Errorf("failed to get working directory: %w", err)
   240  			}
   241  			hostJwtPath := filepath.Join(workdir, utils.Config.Analytics.GcpJwtPath)
   242  			bind = append(bind, hostJwtPath+":/opt/app/rel/logflare/bin/gcloud.json")
   243  			// This is hardcoded in studio frontend
   244  			env = append(env,
   245  				"GOOGLE_DATASET_ID_APPEND=_prod",
   246  				"GOOGLE_PROJECT_ID="+utils.Config.Analytics.GcpProjectId,
   247  				"GOOGLE_PROJECT_NUMBER="+utils.Config.Analytics.GcpProjectNumber,
   248  			)
   249  		case utils.LogflarePostgres:
   250  			env = append(env,
   251  				fmt.Sprintf("POSTGRES_BACKEND_URL=postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
   252  				"POSTGRES_BACKEND_SCHEMA=_analytics",
   253  			)
   254  		}
   255  
   256  		if _, err := utils.DockerStart(
   257  			ctx,
   258  			container.Config{
   259  				Hostname: "127.0.0.1",
   260  				Image:    utils.LogflareImage,
   261  				Env:      env,
   262  				// Original entrypoint conflicts with healthcheck due to 15 seconds sleep:
   263  				// https://github.com/Logflare/logflare/blob/staging/run.sh#L35
   264  				Entrypoint: []string{"sh", "-c", `cat <<'EOF' > run.sh && sh run.sh
   265  ./logflare eval Logflare.Release.migrate
   266  ./logflare start --sname logflare
   267  EOF
   268  `},
   269  				Healthcheck: &container.HealthConfig{
   270  					Test:        []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null", "http://127.0.0.1:4000/health"},
   271  					Interval:    10 * time.Second,
   272  					Timeout:     2 * time.Second,
   273  					Retries:     3,
   274  					StartPeriod: 10 * time.Second,
   275  				},
   276  				ExposedPorts: nat.PortSet{"4000/tcp": {}},
   277  			},
   278  			container.HostConfig{
   279  				Binds:         bind,
   280  				PortBindings:  nat.PortMap{"4000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Analytics.Port), 10)}}},
   281  				RestartPolicy: container.RestartPolicy{Name: "always"},
   282  			},
   283  			network.NetworkingConfig{
   284  				EndpointsConfig: map[string]*network.EndpointSettings{
   285  					utils.NetId: {
   286  						Aliases: utils.LogflareAliases,
   287  					},
   288  				},
   289  			},
   290  			utils.LogflareId,
   291  		); err != nil {
   292  			return err
   293  		}
   294  		if err := reset.WaitForServiceReady(ctx, []string{utils.LogflareId}); err != nil {
   295  			return err
   296  		}
   297  	}
   298  
   299  	// Start Kong.
   300  	p.Send(utils.StatusMsg("Starting containers..."))
   301  	if !isContainerExcluded(utils.KongImage, excluded) {
   302  		var kongConfigBuf bytes.Buffer
   303  		if err := kongConfigTemplate.Execute(&kongConfigBuf, kongConfig{
   304  			GotrueId:      utils.GotrueId,
   305  			RestId:        utils.RestId,
   306  			RealtimeId:    utils.Config.Realtime.TenantId,
   307  			StorageId:     utils.StorageId,
   308  			PgmetaId:      utils.PgmetaId,
   309  			EdgeRuntimeId: utils.EdgeRuntimeId,
   310  			LogflareId:    utils.LogflareId,
   311  			ApiHost:       utils.Config.Hostname,
   312  			ApiPort:       utils.Config.Api.Port,
   313  		}); err != nil {
   314  			return errors.Errorf("failed to exec template: %w", err)
   315  		}
   316  
   317  		binds := []string{}
   318  		for id, tmpl := range utils.Config.Auth.Email.Template {
   319  			if len(tmpl.ContentPath) == 0 {
   320  				continue
   321  			}
   322  			hostPath := tmpl.ContentPath
   323  			if !filepath.IsAbs(tmpl.ContentPath) {
   324  				var err error
   325  				hostPath, err = filepath.Abs(hostPath)
   326  				if err != nil {
   327  					return errors.Errorf("failed to resolve absolute path: %w", err)
   328  				}
   329  			}
   330  			dockerPath := path.Join(nginxEmailTemplateDir, id+filepath.Ext(hostPath))
   331  			binds = append(binds, fmt.Sprintf("%s:%s:rw,z", hostPath, dockerPath))
   332  		}
   333  
   334  		if _, err := utils.DockerStart(
   335  			ctx,
   336  			container.Config{
   337  				Image: utils.KongImage,
   338  				Env: []string{
   339  					"KONG_DATABASE=off",
   340  					"KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml",
   341  					"KONG_DNS_ORDER=LAST,A,CNAME", // https://github.com/supabase/cli/issues/14
   342  					"KONG_PLUGINS=request-transformer,cors",
   343  					// Need to increase the nginx buffers in kong to avoid it rejecting the rather
   344  					// sizeable response headers azure can generate
   345  					// Ref: https://github.com/Kong/kong/issues/3974#issuecomment-482105126
   346  					"KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k",
   347  					"KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k",
   348  					"KONG_NGINX_WORKER_PROCESSES=1",
   349  				},
   350  				Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /home/kong/kong.yml && cat <<'EOF' > /home/kong/custom_nginx.template && ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template
   351  ` + kongConfigBuf.String() + `
   352  EOF
   353  ` + nginxConfigEmbed + `
   354  EOF
   355  `},
   356  			},
   357  			start.WithSyslogConfig(container.HostConfig{
   358  				Binds:         binds,
   359  				PortBindings:  nat.PortMap{"8000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Api.Port), 10)}}},
   360  				RestartPolicy: container.RestartPolicy{Name: "always"},
   361  			}),
   362  			network.NetworkingConfig{
   363  				EndpointsConfig: map[string]*network.EndpointSettings{
   364  					utils.NetId: {
   365  						Aliases: utils.KongAliases,
   366  					},
   367  				},
   368  			},
   369  			utils.KongId,
   370  		); err != nil {
   371  			return err
   372  		}
   373  		started = append(started, utils.KongId)
   374  	}
   375  
   376  	// Start GoTrue.
   377  	if utils.Config.Auth.Enabled && !isContainerExcluded(utils.Config.Auth.Image, excluded) {
   378  		var testOTP bytes.Buffer
   379  		if len(utils.Config.Auth.Sms.TestOTP) > 0 {
   380  			formatMapForEnvConfig(utils.Config.Auth.Sms.TestOTP, &testOTP)
   381  		}
   382  
   383  		env := []string{
   384  			fmt.Sprintf("API_EXTERNAL_URL=http://%s:%d", utils.Config.Hostname, utils.Config.Api.Port),
   385  
   386  			"GOTRUE_API_HOST=0.0.0.0",
   387  			"GOTRUE_API_PORT=9999",
   388  
   389  			"GOTRUE_DB_DRIVER=postgres",
   390  			fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
   391  
   392  			"GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl,
   393  			"GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","),
   394  			fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !utils.Config.Auth.EnableSignup),
   395  
   396  			"GOTRUE_JWT_ADMIN_ROLES=service_role",
   397  			"GOTRUE_JWT_AUD=authenticated",
   398  			"GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated",
   399  			fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry),
   400  			"GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   401  			fmt.Sprintf("GOTRUE_JWT_ISSUER=http://%s:%d/auth/v1", utils.Config.Hostname, utils.Config.Api.Port),
   402  
   403  			fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", utils.Config.Auth.Email.EnableSignup),
   404  			fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", utils.Config.Auth.Email.DoubleConfirmChanges),
   405  			fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !utils.Config.Auth.Email.EnableConfirmations),
   406  
   407  			fmt.Sprintf("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=%v", utils.Config.Auth.EnableAnonymousSignIns),
   408  
   409  			"GOTRUE_SMTP_HOST=" + utils.InbucketId,
   410  			"GOTRUE_SMTP_PORT=2500",
   411  			"GOTRUE_SMTP_ADMIN_EMAIL=admin@email.com",
   412  			fmt.Sprintf("GOTRUE_SMTP_MAX_FREQUENCY=%v", utils.Config.Auth.Email.MaxFrequency),
   413  			// TODO: To be reverted to `/auth/v1/verify` once
   414  			// https://github.com/supabase/supabase/issues/16100
   415  			// is fixed on upstream GoTrue.
   416  			fmt.Sprintf("GOTRUE_MAILER_URLPATHS_INVITE=http://%s:%d/auth/v1/verify", utils.Config.Hostname, utils.Config.Api.Port),
   417  			fmt.Sprintf("GOTRUE_MAILER_URLPATHS_CONFIRMATION=http://%s:%d/auth/v1/verify", utils.Config.Hostname, utils.Config.Api.Port),
   418  			fmt.Sprintf("GOTRUE_MAILER_URLPATHS_RECOVERY=http://%s:%d/auth/v1/verify", utils.Config.Hostname, utils.Config.Api.Port),
   419  			fmt.Sprintf("GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=http://%s:%d/auth/v1/verify", utils.Config.Hostname, utils.Config.Api.Port),
   420  			"GOTRUE_RATE_LIMIT_EMAIL_SENT=360000",
   421  
   422  			fmt.Sprintf("GOTRUE_EXTERNAL_PHONE_ENABLED=%v", utils.Config.Auth.Sms.EnableSignup),
   423  			fmt.Sprintf("GOTRUE_SMS_AUTOCONFIRM=%v", !utils.Config.Auth.Sms.EnableConfirmations),
   424  			fmt.Sprintf("GOTRUE_SMS_MAX_FREQUENCY=%v", utils.Config.Auth.Sms.MaxFrequency),
   425  			"GOTRUE_SMS_OTP_EXP=6000",
   426  			"GOTRUE_SMS_OTP_LENGTH=6",
   427  			fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template),
   428  			"GOTRUE_SMS_TEST_OTP=" + testOTP.String(),
   429  
   430  			fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation),
   431  			fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval),
   432  			fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking),
   433  		}
   434  
   435  		for id, tmpl := range utils.Config.Auth.Email.Template {
   436  			if len(tmpl.ContentPath) > 0 {
   437  				env = append(env, fmt.Sprintf("GOTRUE_MAILER_TEMPLATES_%s=http://%s:%d/email/%s",
   438  					strings.ToUpper(id),
   439  					utils.KongId,
   440  					nginxTemplateServerPort,
   441  					id+filepath.Ext(tmpl.ContentPath),
   442  				))
   443  			}
   444  			if len(tmpl.Subject) > 0 {
   445  				env = append(env, fmt.Sprintf("GOTRUE_MAILER_SUBJECTS_%s=%s",
   446  					strings.ToUpper(id),
   447  					tmpl.Subject,
   448  				))
   449  			}
   450  		}
   451  
   452  		if utils.Config.Auth.Sms.Twilio.Enabled {
   453  			env = append(
   454  				env,
   455  				"GOTRUE_SMS_PROVIDER=twilio",
   456  				"GOTRUE_SMS_TWILIO_ACCOUNT_SID="+utils.Config.Auth.Sms.Twilio.AccountSid,
   457  				"GOTRUE_SMS_TWILIO_AUTH_TOKEN="+utils.Config.Auth.Sms.Twilio.AuthToken,
   458  				"GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.Twilio.MessageServiceSid,
   459  			)
   460  		}
   461  		if utils.Config.Auth.Sms.TwilioVerify.Enabled {
   462  			env = append(
   463  				env,
   464  				"GOTRUE_SMS_PROVIDER=twilio_verify",
   465  				"GOTRUE_SMS_TWILIO_VERIFY_ACCOUNT_SID="+utils.Config.Auth.Sms.TwilioVerify.AccountSid,
   466  				"GOTRUE_SMS_TWILIO_VERIFY_AUTH_TOKEN="+utils.Config.Auth.Sms.TwilioVerify.AuthToken,
   467  				"GOTRUE_SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.TwilioVerify.MessageServiceSid,
   468  			)
   469  		}
   470  		if utils.Config.Auth.Sms.Messagebird.Enabled {
   471  			env = append(
   472  				env,
   473  				"GOTRUE_SMS_PROVIDER=messagebird",
   474  				"GOTRUE_SMS_MESSAGEBIRD_ACCESS_KEY="+utils.Config.Auth.Sms.Messagebird.AccessKey,
   475  				"GOTRUE_SMS_MESSAGEBIRD_ORIGINATOR="+utils.Config.Auth.Sms.Messagebird.Originator,
   476  			)
   477  		}
   478  		if utils.Config.Auth.Sms.Textlocal.Enabled {
   479  			env = append(
   480  				env,
   481  				"GOTRUE_SMS_PROVIDER=textlocal",
   482  				"GOTRUE_SMS_TEXTLOCAL_API_KEY="+utils.Config.Auth.Sms.Textlocal.ApiKey,
   483  				"GOTRUE_SMS_TEXTLOCAL_SENDER="+utils.Config.Auth.Sms.Textlocal.Sender,
   484  			)
   485  		}
   486  		if utils.Config.Auth.Sms.Vonage.Enabled {
   487  			env = append(
   488  				env,
   489  				"GOTRUE_SMS_PROVIDER=vonage",
   490  				"GOTRUE_SMS_VONAGE_API_KEY="+utils.Config.Auth.Sms.Vonage.ApiKey,
   491  				"GOTRUE_SMS_VONAGE_API_SECRET="+utils.Config.Auth.Sms.Vonage.ApiSecret,
   492  				"GOTRUE_SMS_VONAGE_FROM="+utils.Config.Auth.Sms.Vonage.From,
   493  			)
   494  		}
   495  		if utils.Config.Auth.Hook.MFAVerificationAttempt.Enabled {
   496  			env = append(
   497  				env,
   498  				"GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED=true",
   499  				"GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="+utils.Config.Auth.Hook.MFAVerificationAttempt.URI,
   500  				"GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS="+utils.Config.Auth.Hook.MFAVerificationAttempt.Secrets,
   501  			)
   502  		}
   503  
   504  		if utils.Config.Auth.Hook.PasswordVerificationAttempt.Enabled {
   505  			env = append(
   506  				env,
   507  				"GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED=true",
   508  				"GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="+utils.Config.Auth.Hook.PasswordVerificationAttempt.URI,
   509  				"GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS="+utils.Config.Auth.Hook.PasswordVerificationAttempt.Secrets,
   510  			)
   511  		}
   512  
   513  		if utils.Config.Auth.Hook.CustomAccessToken.Enabled {
   514  			env = append(
   515  				env,
   516  				"GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED=true",
   517  				"GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="+utils.Config.Auth.Hook.CustomAccessToken.URI,
   518  				"GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS="+utils.Config.Auth.Hook.CustomAccessToken.Secrets,
   519  			)
   520  		}
   521  
   522  		if utils.Config.Auth.Hook.SendSMS.Enabled {
   523  			env = append(
   524  				env,
   525  				"GOTRUE_HOOK_SEND_SMS_ENABLED=true",
   526  				"GOTRUE_HOOK_SEND_SMS_URI="+utils.Config.Auth.Hook.SendSMS.URI,
   527  				"GOTRUE_HOOK_SEND_SMS_SECRETS="+utils.Config.Auth.Hook.SendSMS.Secrets,
   528  			)
   529  		}
   530  
   531  		if utils.Config.Auth.Hook.SendEmail.Enabled {
   532  			env = append(
   533  				env,
   534  				"GOTRUE_HOOK_SEND_EMAIL_ENABLED=true",
   535  				"GOTRUE_HOOK_SEND_EMAIL_URI="+utils.Config.Auth.Hook.SendEmail.URI,
   536  				"GOTRUE_HOOK_SEND_EMAIL_SECRETS="+utils.Config.Auth.Hook.SendEmail.Secrets,
   537  			)
   538  		}
   539  
   540  		for name, config := range utils.Config.Auth.External {
   541  			env = append(
   542  				env,
   543  				fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled),
   544  				fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId),
   545  				fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret),
   546  				fmt.Sprintf("GOTRUE_EXTERNAL_%s_SKIP_NONCE_CHECK=%t", strings.ToUpper(name), config.SkipNonceCheck),
   547  			)
   548  
   549  			if config.RedirectUri != "" {
   550  				env = append(env,
   551  					fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), config.RedirectUri),
   552  				)
   553  			} else {
   554  				env = append(env,
   555  					fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=http://%s:%d/auth/v1/callback", strings.ToUpper(name), utils.Config.Hostname, utils.Config.Api.Port),
   556  				)
   557  			}
   558  
   559  			if config.Url != "" {
   560  				env = append(env,
   561  					fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url),
   562  				)
   563  			}
   564  		}
   565  
   566  		if _, err := utils.DockerStart(
   567  			ctx,
   568  			container.Config{
   569  				Image:        utils.Config.Auth.Image,
   570  				Env:          env,
   571  				ExposedPorts: nat.PortSet{"9999/tcp": {}},
   572  				Healthcheck: &container.HealthConfig{
   573  					Test:     []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9999/health"},
   574  					Interval: 10 * time.Second,
   575  					Timeout:  2 * time.Second,
   576  					Retries:  3,
   577  				},
   578  			},
   579  			start.WithSyslogConfig(container.HostConfig{
   580  				RestartPolicy: container.RestartPolicy{Name: "always"},
   581  			}),
   582  			network.NetworkingConfig{
   583  				EndpointsConfig: map[string]*network.EndpointSettings{
   584  					utils.NetId: {
   585  						Aliases: utils.GotrueAliases,
   586  					},
   587  				},
   588  			},
   589  			utils.GotrueId,
   590  		); err != nil {
   591  			return err
   592  		}
   593  		started = append(started, utils.GotrueId)
   594  	}
   595  
   596  	// Start Inbucket.
   597  	if utils.Config.Inbucket.Enabled && !isContainerExcluded(utils.InbucketImage, excluded) {
   598  		inbucketPortBindings := nat.PortMap{"9000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Port), 10)}}}
   599  		if utils.Config.Inbucket.SmtpPort != 0 {
   600  			inbucketPortBindings["2500/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.SmtpPort), 10)}}
   601  		}
   602  		if utils.Config.Inbucket.Pop3Port != 0 {
   603  			inbucketPortBindings["1100/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Pop3Port), 10)}}
   604  		}
   605  		if _, err := utils.DockerStart(
   606  			ctx,
   607  			container.Config{
   608  				Image: utils.InbucketImage,
   609  			},
   610  			container.HostConfig{
   611  				Binds: []string{
   612  					// Override default mount points to avoid creating multiple anonymous volumes
   613  					// Ref: https://github.com/inbucket/inbucket/blob/v3.0.4/Dockerfile#L52
   614  					utils.InbucketId + ":/config",
   615  					utils.InbucketId + ":/storage",
   616  				},
   617  				PortBindings:  inbucketPortBindings,
   618  				RestartPolicy: container.RestartPolicy{Name: "always"},
   619  			},
   620  			network.NetworkingConfig{
   621  				EndpointsConfig: map[string]*network.EndpointSettings{
   622  					utils.NetId: {
   623  						Aliases: utils.InbucketAliases,
   624  					},
   625  				},
   626  			},
   627  			utils.InbucketId,
   628  		); err != nil {
   629  			return err
   630  		}
   631  		started = append(started, utils.InbucketId)
   632  	}
   633  
   634  	// Start Realtime.
   635  	if utils.Config.Realtime.Enabled && !isContainerExcluded(utils.RealtimeImage, excluded) {
   636  		if _, err := utils.DockerStart(
   637  			ctx,
   638  			container.Config{
   639  				Image: utils.RealtimeImage,
   640  				Env: []string{
   641  					"PORT=4000",
   642  					"DB_HOST=" + dbConfig.Host,
   643  					fmt.Sprintf("DB_PORT=%d", dbConfig.Port),
   644  					"DB_USER=supabase_admin",
   645  					"DB_PASSWORD=" + dbConfig.Password,
   646  					"DB_NAME=" + dbConfig.Database,
   647  					"DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime",
   648  					"DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey,
   649  					"API_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   650  					"METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   651  					"FLY_APP_NAME=realtime",
   652  					"SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase,
   653  					"ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion),
   654  					"ENABLE_TAILSCALE=false",
   655  					"DNS_NODES=''",
   656  					"RLIMIT_NOFILE=",
   657  					fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength),
   658  				},
   659  				// TODO: remove this after deprecating PG14
   660  				Cmd: []string{
   661  					"/bin/sh", "-c",
   662  					"/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo)' && /app/bin/server",
   663  				},
   664  				ExposedPorts: nat.PortSet{"4000/tcp": {}},
   665  				Healthcheck: &container.HealthConfig{
   666  					Test: []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null", "-H", "Authorization: Bearer " + utils.Config.Auth.AnonKey,
   667  						fmt.Sprintf("http://127.0.0.1:4000/api/tenants/%s/health", utils.Config.Realtime.TenantId),
   668  					},
   669  					Interval: 10 * time.Second,
   670  					Timeout:  2 * time.Second,
   671  					Retries:  3,
   672  				},
   673  			},
   674  			start.WithSyslogConfig(container.HostConfig{
   675  				RestartPolicy: container.RestartPolicy{Name: "always"},
   676  			}),
   677  			network.NetworkingConfig{
   678  				EndpointsConfig: map[string]*network.EndpointSettings{
   679  					utils.NetId: {
   680  						Aliases: utils.RealtimeAliases,
   681  					},
   682  				},
   683  			},
   684  			utils.RealtimeId,
   685  		); err != nil {
   686  			return err
   687  		}
   688  		started = append(started, utils.RealtimeId)
   689  	}
   690  
   691  	// Start PostgREST.
   692  	if utils.Config.Api.Enabled && !isContainerExcluded(utils.Config.Api.Image, excluded) {
   693  		if _, err := utils.DockerStart(
   694  			ctx,
   695  			container.Config{
   696  				Image: utils.Config.Api.Image,
   697  				Env: []string{
   698  					fmt.Sprintf("PGRST_DB_URI=postgresql://authenticator:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
   699  					"PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","),
   700  					"PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","),
   701  					fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows),
   702  					"PGRST_DB_ANON_ROLE=anon",
   703  					"PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   704  					"PGRST_ADMIN_SERVER_PORT=3001",
   705  				},
   706  				// PostgREST does not expose a shell for health check
   707  			},
   708  			start.WithSyslogConfig(container.HostConfig{
   709  				RestartPolicy: container.RestartPolicy{Name: "always"},
   710  			}),
   711  			network.NetworkingConfig{
   712  				EndpointsConfig: map[string]*network.EndpointSettings{
   713  					utils.NetId: {
   714  						Aliases: utils.RestAliases,
   715  					},
   716  				},
   717  			},
   718  			utils.RestId,
   719  		); err != nil {
   720  			return err
   721  		}
   722  		started = append(started, utils.RestId)
   723  	}
   724  
   725  	// Start Storage.
   726  	if utils.Config.Storage.Enabled && !isContainerExcluded(utils.Config.Storage.Image, excluded) {
   727  		dockerStoragePath := "/mnt"
   728  		if _, err := utils.DockerStart(
   729  			ctx,
   730  			container.Config{
   731  				Image: utils.Config.Storage.Image,
   732  				Env: []string{
   733  					"ANON_KEY=" + utils.Config.Auth.AnonKey,
   734  					"SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey,
   735  					"AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret,
   736  					fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
   737  					fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
   738  					"STORAGE_BACKEND=file",
   739  					"FILE_STORAGE_BACKEND_PATH=" + dockerStoragePath,
   740  					"TENANT_ID=stub",
   741  					// TODO: https://github.com/supabase/storage-api/issues/55
   742  					"STORAGE_S3_REGION=" + utils.Config.Storage.S3Credentials.Region,
   743  					"GLOBAL_S3_BUCKET=stub",
   744  					fmt.Sprintf("ENABLE_IMAGE_TRANSFORMATION=%t", utils.Config.Storage.ImageTransformation.Enabled),
   745  					fmt.Sprintf("IMGPROXY_URL=http://%s:5001", utils.ImgProxyId),
   746  					"TUS_URL_PATH=/storage/v1/upload/resumable",
   747  					"S3_PROTOCOL_ACCESS_KEY_ID=" + utils.Config.Storage.S3Credentials.AccessKeyId,
   748  					"S3_PROTOCOL_ACCESS_KEY_SECRET=" + utils.Config.Storage.S3Credentials.SecretAccessKey,
   749  					"S3_PROTOCOL_PREFIX=/storage/v1",
   750  					"S3_ALLOW_FORWARDED_HEADER=true",
   751  					"UPLOAD_FILE_SIZE_LIMIT=52428800000",
   752  					"UPLOAD_FILE_SIZE_LIMIT_STANDARD=5242880000",
   753  				},
   754  				Healthcheck: &container.HealthConfig{
   755  					// For some reason, 127.0.0.1 resolves to IPv6 address on GitPod which breaks healthcheck.
   756  					Test:     []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5000/status"},
   757  					Interval: 10 * time.Second,
   758  					Timeout:  2 * time.Second,
   759  					Retries:  3,
   760  				},
   761  			},
   762  			start.WithSyslogConfig(container.HostConfig{
   763  				RestartPolicy: container.RestartPolicy{Name: "always"},
   764  				Binds:         []string{utils.StorageId + ":" + dockerStoragePath},
   765  			}),
   766  			network.NetworkingConfig{
   767  				EndpointsConfig: map[string]*network.EndpointSettings{
   768  					utils.NetId: {
   769  						Aliases: utils.StorageAliases,
   770  					},
   771  				},
   772  			},
   773  			utils.StorageId,
   774  		); err != nil {
   775  			return err
   776  		}
   777  		started = append(started, utils.StorageId)
   778  	}
   779  
   780  	// Start Storage ImgProxy.
   781  	if utils.Config.Storage.Enabled && utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.ImageProxyImage, excluded) {
   782  		if _, err := utils.DockerStart(
   783  			ctx,
   784  			container.Config{
   785  				Image: utils.ImageProxyImage,
   786  				Env: []string{
   787  					"IMGPROXY_BIND=:5001",
   788  					"IMGPROXY_LOCAL_FILESYSTEM_ROOT=/",
   789  					"IMGPROXY_USE_ETAG=/",
   790  				},
   791  				Healthcheck: &container.HealthConfig{
   792  					Test:     []string{"CMD", "imgproxy", "health"},
   793  					Interval: 10 * time.Second,
   794  					Timeout:  2 * time.Second,
   795  					Retries:  3,
   796  				},
   797  			},
   798  			container.HostConfig{
   799  				VolumesFrom:   []string{utils.StorageId},
   800  				RestartPolicy: container.RestartPolicy{Name: "always"},
   801  			},
   802  			network.NetworkingConfig{
   803  				EndpointsConfig: map[string]*network.EndpointSettings{
   804  					utils.NetId: {
   805  						Aliases: utils.ImgProxyAliases,
   806  					},
   807  				},
   808  			},
   809  			utils.ImgProxyId,
   810  		); err != nil {
   811  			return err
   812  		}
   813  		started = append(started, utils.ImgProxyId)
   814  	}
   815  
   816  	// Start all functions.
   817  	if !isContainerExcluded(utils.EdgeRuntimeImage, excluded) {
   818  		dbUrl := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database)
   819  		if err := serve.ServeFunctions(ctx, "", nil, "", dbUrl, w, fsys); err != nil {
   820  			return err
   821  		}
   822  		started = append(started, utils.EdgeRuntimeId)
   823  	}
   824  
   825  	// Start pg-meta.
   826  	if utils.Config.Studio.Enabled && !isContainerExcluded(utils.PgmetaImage, excluded) {
   827  		if _, err := utils.DockerStart(
   828  			ctx,
   829  			container.Config{
   830  				Image: utils.PgmetaImage,
   831  				Env: []string{
   832  					"PG_META_PORT=8080",
   833  					"PG_META_DB_HOST=" + dbConfig.Host,
   834  					"PG_META_DB_NAME=" + dbConfig.Database,
   835  					"PG_META_DB_USER=" + dbConfig.User,
   836  					fmt.Sprintf("PG_META_DB_PORT=%d", dbConfig.Port),
   837  					"PG_META_DB_PASSWORD=" + dbConfig.Password,
   838  				},
   839  				Healthcheck: &container.HealthConfig{
   840  					Test:     []string{"CMD", "node", "-e", "fetch('http://127.0.0.1:8080/health').then((r) => {if (r.status !== 200) throw new Error(r.status)})"},
   841  					Interval: 10 * time.Second,
   842  					Timeout:  2 * time.Second,
   843  					Retries:  3,
   844  				},
   845  			},
   846  			container.HostConfig{
   847  				RestartPolicy: container.RestartPolicy{Name: "always"},
   848  			},
   849  			network.NetworkingConfig{
   850  				EndpointsConfig: map[string]*network.EndpointSettings{
   851  					utils.NetId: {
   852  						Aliases: utils.PgmetaAliases,
   853  					},
   854  				},
   855  			},
   856  			utils.PgmetaId,
   857  		); err != nil {
   858  			return err
   859  		}
   860  		started = append(started, utils.PgmetaId)
   861  	}
   862  
   863  	// Start Studio.
   864  	if utils.Config.Studio.Enabled && !isContainerExcluded(utils.StudioImage, excluded) {
   865  		if _, err := utils.DockerStart(
   866  			ctx,
   867  			container.Config{
   868  				Image: utils.StudioImage,
   869  				Env: []string{
   870  					"STUDIO_PG_META_URL=http://" + utils.PgmetaId + ":8080",
   871  					"POSTGRES_PASSWORD=" + dbConfig.Password,
   872  					"SUPABASE_URL=http://" + utils.KongId + ":8000",
   873  					fmt.Sprintf("SUPABASE_PUBLIC_URL=%s:%v/", utils.Config.Studio.ApiUrl, utils.Config.Api.Port),
   874  					"SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey,
   875  					"SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey,
   876  					"LOGFLARE_API_KEY=" + utils.Config.Analytics.ApiKey,
   877  					"OPENAI_KEY=" + utils.Config.Studio.OpenaiApiKey,
   878  					fmt.Sprintf("LOGFLARE_URL=http://%v:4000", utils.LogflareId),
   879  					fmt.Sprintf("NEXT_PUBLIC_ENABLE_LOGS=%v", utils.Config.Analytics.Enabled),
   880  					fmt.Sprintf("NEXT_ANALYTICS_BACKEND_PROVIDER=%v", utils.Config.Analytics.Backend),
   881  					// Ref: https://github.com/vercel/next.js/issues/51684#issuecomment-1612834913
   882  					"HOSTNAME=0.0.0.0",
   883  				},
   884  				Healthcheck: &container.HealthConfig{
   885  					Test:     []string{"CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"},
   886  					Interval: 10 * time.Second,
   887  					Timeout:  2 * time.Second,
   888  					Retries:  3,
   889  				},
   890  			},
   891  			container.HostConfig{
   892  				PortBindings:  nat.PortMap{"3000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Studio.Port), 10)}}},
   893  				RestartPolicy: container.RestartPolicy{Name: "always"},
   894  			},
   895  			network.NetworkingConfig{
   896  				EndpointsConfig: map[string]*network.EndpointSettings{
   897  					utils.NetId: {
   898  						Aliases: utils.StudioAliases,
   899  					},
   900  				},
   901  			},
   902  			utils.StudioId,
   903  		); err != nil {
   904  			return err
   905  		}
   906  		started = append(started, utils.StudioId)
   907  	}
   908  
   909  	// Start pooler.
   910  	if utils.Config.Db.Pooler.Enabled && !isContainerExcluded(utils.PgbouncerImage, excluded) {
   911  		if _, err := utils.DockerStart(
   912  			ctx,
   913  			container.Config{
   914  				Image: utils.PgbouncerImage,
   915  				Env: []string{
   916  					"POSTGRESQL_HOST=" + dbConfig.Host,
   917  					fmt.Sprintf("POSTGRESQL_PORT=%d", dbConfig.Port),
   918  					"POSTGRESQL_USERNAME=pgbouncer",
   919  					"POSTGRESQL_PASSWORD=" + dbConfig.Password,
   920  					"POSTGRESQL_DATABASE=" + dbConfig.Database,
   921  					"PGBOUNCER_AUTH_USER=pgbouncer",
   922  					"PGBOUNCER_AUTH_QUERY=SELECT * FROM pgbouncer.get_auth($1)",
   923  					fmt.Sprintf("PGBOUNCER_POOL_MODE=%s", utils.Config.Db.Pooler.PoolMode),
   924  					fmt.Sprintf("PGBOUNCER_DEFAULT_POOL_SIZE=%d", utils.Config.Db.Pooler.DefaultPoolSize),
   925  					fmt.Sprintf("PGBOUNCER_MAX_CLIENT_CONN=%d", utils.Config.Db.Pooler.MaxClientConn),
   926  					// Default platform config: https://github.com/supabase/postgres/blob/develop/ansible/files/pgbouncer_config/pgbouncer.ini.j2
   927  					"PGBOUNCER_IGNORE_STARTUP_PARAMETERS=extra_float_digits",
   928  				},
   929  				Healthcheck: &container.HealthConfig{
   930  					Test:     []string{"CMD", "bash", "-c", "printf \\0 > /dev/tcp/127.0.0.1/6432"},
   931  					Interval: 10 * time.Second,
   932  					Timeout:  2 * time.Second,
   933  					Retries:  3,
   934  				},
   935  			},
   936  			container.HostConfig{
   937  				PortBindings:  nat.PortMap{"6432/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Db.Pooler.Port), 10)}}},
   938  				RestartPolicy: container.RestartPolicy{Name: "always"},
   939  			},
   940  			network.NetworkingConfig{
   941  				EndpointsConfig: map[string]*network.EndpointSettings{
   942  					utils.NetId: {
   943  						Aliases: utils.PoolerAliases,
   944  					},
   945  				},
   946  			},
   947  			utils.PoolerId,
   948  		); err != nil {
   949  			return err
   950  		}
   951  		started = append(started, utils.PoolerId)
   952  	}
   953  
   954  	p.Send(utils.StatusMsg("Waiting for health checks..."))
   955  	return reset.WaitForServiceReady(ctx, started)
   956  }
   957  
   958  func isContainerExcluded(imageName string, excluded map[string]bool) bool {
   959  	short := utils.ShortContainerImageName(imageName)
   960  	if val, ok := excluded[short]; ok && val {
   961  		return true
   962  	}
   963  	return false
   964  }
   965  
   966  func ExcludableContainers() []string {
   967  	names := []string{}
   968  	for _, image := range utils.ServiceImages {
   969  		names = append(names, utils.ShortContainerImageName(image))
   970  	}
   971  	return names
   972  }
   973  
   974  func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) {
   975  	numOfKeyPairs := len(input)
   976  	i := 0
   977  	for k, v := range input {
   978  		output.WriteString(k)
   979  		output.WriteString(":")
   980  		output.WriteString(v)
   981  		i++
   982  		if i < numOfKeyPairs {
   983  			output.WriteString(",")
   984  		}
   985  	}
   986  }