package start import ( "bytes" "context" _ "embed" "fmt" "os" "path" "path/filepath" "strconv" "strings" "text/template" "time" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" "github.com/go-errors/errors" "github.com/jackc/pgconn" "github.com/jackc/pgx/v4" "github.com/spf13/afero" "github.com/supabase/cli/internal/db/start" "github.com/supabase/cli/internal/seed/buckets" "github.com/supabase/cli/internal/services" "github.com/supabase/cli/internal/status" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/config" "golang.org/x/mod/semver" ) func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error { // Sanity checks. { if err := flags.LoadConfig(fsys); err != nil { return err } if err := utils.AssertSupabaseDbIsRunning(); err == nil { fmt.Fprintln(os.Stderr, utils.Aqua("supabase start")+" is already running.") names := status.CustomName{} return status.Run(ctx, names, utils.OutputPretty, fsys) } else if !errors.Is(err, utils.ErrNotRunning) { return err } if err := flags.LoadProjectRef(fsys); err == nil { _ = services.CheckVersions(ctx, fsys) } } if err := utils.RunProgram(ctx, func(p utils.Program, ctx context.Context) error { dbConfig := pgconn.Config{ Host: utils.DbId, Port: 5432, User: "postgres", Password: utils.Config.Db.Password, Database: "postgres", } return run(p, ctx, fsys, excludedContainers, dbConfig) }); err != nil { if ignoreHealthCheck && start.IsUnhealthyError(err) { fmt.Fprintln(os.Stderr, err) } else { if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil { fmt.Fprintln(os.Stderr, err) } return err } } fmt.Fprintf(os.Stderr, "Started %s local development setup.\n\n", utils.Aqua("supabase")) status.PrettyPrint(os.Stdout, excludedContainers...) return nil } type kongConfig struct { GotrueId string RestId string RealtimeId string StorageId string PgmetaId string EdgeRuntimeId string LogflareId string PoolerId string ApiHost string ApiPort uint16 } // TODO: deprecate after removing storage headers from kong func StorageVersionBelow(target string) bool { parts := strings.Split(utils.Config.Storage.Image, ":v") return semver.Compare(parts[len(parts)-1], target) < 0 } var ( //go:embed templates/kong.yml kongConfigEmbed string kongConfigTemplate = template.Must(template.New("kongConfig").Funcs(template.FuncMap{ "StorageVersionBelow": StorageVersionBelow, }).Parse(kongConfigEmbed)) //go:embed templates/custom_nginx.template nginxConfigEmbed string // Hardcoded configs which match nginxConfigEmbed nginxEmailTemplateDir = "/home/kong/templates/email" nginxTemplateServerPort = 8088 ) type vectorConfig struct { ApiKey string VectorId string LogflareId string KongId string GotrueId string RestId string RealtimeId string StorageId string EdgeRuntimeId string DbId string } var ( //go:embed templates/vector.yaml vectorConfigEmbed string vectorConfigTemplate = template.Must(template.New("vectorConfig").Parse(vectorConfigEmbed)) ) type poolerTenant struct { DbHost string DbPort uint16 DbDatabase string DbPassword string ExternalId string ModeType config.PoolMode DefaultMaxClients uint DefaultPoolSize uint } var ( //go:embed templates/pooler.exs poolerTenantEmbed string poolerTenantTemplate = template.Must(template.New("poolerTenant").Parse(poolerTenantEmbed)) ) var serviceTimeout = 30 * time.Second func run(p utils.Program, ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error { excluded := make(map[string]bool) for _, name := range excludedContainers { excluded[name] = true } jwks, err := utils.Config.Auth.ResolveJWKS(ctx) if err != nil { return err } // Start Postgres. w := utils.StatusWriter{Program: p} if dbConfig.Host == utils.DbId { if err := start.StartDatabase(ctx, "", fsys, w, options...); err != nil { return err } } var started []string var isStorageEnabled = utils.Config.Storage.Enabled && !isContainerExcluded(utils.Config.Storage.Image, excluded) var isImgProxyEnabled = utils.Config.Storage.ImageTransformation != nil && utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.Config.Storage.ImgProxyImage, excluded) p.Send(utils.StatusMsg("Starting containers...")) // Start Kong. if !isContainerExcluded(utils.Config.Api.KongImage, excluded) { var kongConfigBuf bytes.Buffer if err := kongConfigTemplate.Option("missingkey=error").Execute(&kongConfigBuf, kongConfig{ GotrueId: utils.GotrueId, RestId: utils.RestId, RealtimeId: utils.Config.Realtime.TenantId, StorageId: utils.StorageId, PgmetaId: utils.PgmetaId, EdgeRuntimeId: utils.EdgeRuntimeId, LogflareId: utils.LogflareId, PoolerId: utils.PoolerId, ApiHost: utils.Config.Hostname, ApiPort: utils.Config.Api.Port, }); err != nil { return errors.Errorf("failed to exec template: %w", err) } binds := []string{} for id, tmpl := range utils.Config.Auth.Email.Template { if len(tmpl.ContentPath) == 0 { continue } hostPath := tmpl.ContentPath if !filepath.IsAbs(tmpl.ContentPath) { var err error hostPath, err = filepath.Abs(hostPath) if err != nil { return errors.Errorf("failed to resolve absolute path: %w", err) } } dockerPath := path.Join(nginxEmailTemplateDir, id+filepath.Ext(hostPath)) binds = append(binds, fmt.Sprintf("%s:%s:rw", hostPath, dockerPath)) } dockerPort := uint16(8000) if utils.Config.Api.Tls.Enabled { dockerPort = 8443 } if _, err := utils.DockerStart( ctx, container.Config{ Image: utils.Config.Api.KongImage, Env: []string{ "KONG_DATABASE=off", "KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml", "KONG_DNS_ORDER=LAST,A,CNAME", // https://github.com/supabase/cli/issues/14 "KONG_PLUGINS=request-transformer,cors", fmt.Sprintf("KONG_PORT_MAPS=%d:8000", utils.Config.Api.Port), // Need to increase the nginx buffers in kong to avoid it rejecting the rather // sizeable response headers azure can generate // Ref: https://github.com/Kong/kong/issues/3974#issuecomment-482105126 "KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k", "KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k", "KONG_NGINX_WORKER_PROCESSES=1", // Use modern TLS certificate "KONG_SSL_CERT=/home/kong/localhost.crt", "KONG_SSL_CERT_KEY=/home/kong/localhost.key", }, Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /home/kong/kong.yml && \ cat <<'EOF' > /home/kong/custom_nginx.template && \ cat <<'EOF' > /home/kong/localhost.crt && \ cat <<'EOF' > /home/kong/localhost.key && \ ./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template ` + kongConfigBuf.String() + ` EOF ` + nginxConfigEmbed + ` EOF ` + status.KongCert + ` EOF ` + status.KongKey + ` EOF `}, ExposedPorts: nat.PortSet{ "8000/tcp": {}, "8443/tcp": {}, nat.Port(fmt.Sprintf("%d/tcp", nginxTemplateServerPort)): {}, }, }, container.HostConfig{ Binds: binds, PortBindings: nat.PortMap{nat.Port(fmt.Sprintf("%d/tcp", dockerPort)): []nat.PortBinding{{ HostPort: strconv.FormatUint(uint64(utils.Config.Api.Port), 10)}, }}, RestartPolicy: container.RestartPolicy{Name: "always"}, }, network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ utils.NetId: { Aliases: utils.KongAliases, }, }, }, utils.KongId, ); err != nil { return err } started = append(started, utils.KongId) } // Start GoTrue. if utils.Config.Auth.Enabled && !isContainerExcluded(utils.Config.Auth.Image, excluded) { var testOTP bytes.Buffer if len(utils.Config.Auth.Sms.TestOTP) > 0 { formatMapForEnvConfig(utils.Config.Auth.Sms.TestOTP, &testOTP) } env := []string{ "API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl, "GOTRUE_API_HOST=0.0.0.0", "GOTRUE_API_PORT=9999", "GOTRUE_DB_DRIVER=postgres", fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database), "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl, "GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","), fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !utils.Config.Auth.EnableSignup), "GOTRUE_JWT_ADMIN_ROLES=service_role", "GOTRUE_JWT_AUD=authenticated", "GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated", fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry), "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret, "GOTRUE_JWT_ISSUER=" + utils.GetApiUrl("/auth/v1"), fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", utils.Config.Auth.Email.EnableSignup), fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", utils.Config.Auth.Email.DoubleConfirmChanges), fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !utils.Config.Auth.Email.EnableConfirmations), fmt.Sprintf("GOTRUE_MAILER_OTP_LENGTH=%v", utils.Config.Auth.Email.OtpLength), fmt.Sprintf("GOTRUE_MAILER_OTP_EXP=%v", utils.Config.Auth.Email.OtpExpiry), fmt.Sprintf("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=%v", utils.Config.Auth.EnableAnonymousSignIns), fmt.Sprintf("GOTRUE_SMTP_MAX_FREQUENCY=%v", utils.Config.Auth.Email.MaxFrequency), "GOTRUE_MAILER_URLPATHS_INVITE=" + utils.GetApiUrl("/auth/v1/verify"), "GOTRUE_MAILER_URLPATHS_CONFIRMATION=" + utils.GetApiUrl("/auth/v1/verify"), "GOTRUE_MAILER_URLPATHS_RECOVERY=" + utils.GetApiUrl("/auth/v1/verify"), "GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=" + utils.GetApiUrl("/auth/v1/verify"), "GOTRUE_RATE_LIMIT_EMAIL_SENT=360000", fmt.Sprintf("GOTRUE_EXTERNAL_PHONE_ENABLED=%v", utils.Config.Auth.Sms.EnableSignup), fmt.Sprintf("GOTRUE_SMS_AUTOCONFIRM=%v", !utils.Config.Auth.Sms.EnableConfirmations), fmt.Sprintf("GOTRUE_SMS_MAX_FREQUENCY=%v", utils.Config.Auth.Sms.MaxFrequency), "GOTRUE_SMS_OTP_EXP=6000", "GOTRUE_SMS_OTP_LENGTH=6", fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template), "GOTRUE_SMS_TEST_OTP=" + testOTP.String(), fmt.Sprintf("GOTRUE_PASSWORD_MIN_LENGTH=%v", utils.Config.Auth.MinimumPasswordLength), fmt.Sprintf("GOTRUE_PASSWORD_REQUIRED_CHARACTERS=%v", utils.Config.Auth.PasswordRequirements.ToChar()), fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation), fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval), fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking), fmt.Sprintf("GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=%v", utils.Config.Auth.Email.SecurePasswordChange), fmt.Sprintf("GOTRUE_MFA_PHONE_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.Phone.EnrollEnabled), fmt.Sprintf("GOTRUE_MFA_PHONE_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.Phone.VerifyEnabled), fmt.Sprintf("GOTRUE_MFA_TOTP_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.TOTP.EnrollEnabled), fmt.Sprintf("GOTRUE_MFA_TOTP_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.TOTP.VerifyEnabled), fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.EnrollEnabled), fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.VerifyEnabled), fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors), } if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.Enabled { env = append(env, fmt.Sprintf("GOTRUE_SMTP_HOST=%s", utils.Config.Auth.Email.Smtp.Host), fmt.Sprintf("GOTRUE_SMTP_PORT=%d", utils.Config.Auth.Email.Smtp.Port), fmt.Sprintf("GOTRUE_SMTP_USER=%s", utils.Config.Auth.Email.Smtp.User), fmt.Sprintf("GOTRUE_SMTP_PASS=%s", utils.Config.Auth.Email.Smtp.Pass.Value), fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Auth.Email.Smtp.AdminEmail), fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Auth.Email.Smtp.SenderName), ) } else if utils.Config.Inbucket.Enabled { env = append(env, "GOTRUE_SMTP_HOST="+utils.InbucketId, "GOTRUE_SMTP_PORT=2500", fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Inbucket.AdminEmail), fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Inbucket.SenderName), ) } if utils.Config.Auth.Sessions.Timebox > 0 { env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_TIMEBOX=%v", utils.Config.Auth.Sessions.Timebox)) } if utils.Config.Auth.Sessions.InactivityTimeout > 0 { env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_INACTIVITY_TIMEOUT=%v", utils.Config.Auth.Sessions.InactivityTimeout)) } for id, tmpl := range utils.Config.Auth.Email.Template { if len(tmpl.ContentPath) > 0 { env = append(env, fmt.Sprintf("GOTRUE_MAILER_TEMPLATES_%s=http://%s:%d/email/%s", strings.ToUpper(id), utils.KongId, nginxTemplateServerPort, id+filepath.Ext(tmpl.ContentPath), )) } if tmpl.Subject != nil { env = append(env, fmt.Sprintf("GOTRUE_MAILER_SUBJECTS_%s=%s", strings.ToUpper(id), *tmpl.Subject, )) } } switch { case utils.Config.Auth.Sms.Twilio.Enabled: env = append( env, "GOTRUE_SMS_PROVIDER=twilio", "GOTRUE_SMS_TWILIO_ACCOUNT_SID="+utils.Config.Auth.Sms.Twilio.AccountSid, "GOTRUE_SMS_TWILIO_AUTH_TOKEN="+utils.Config.Auth.Sms.Twilio.AuthToken.Value, "GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.Twilio.MessageServiceSid, ) case utils.Config.Auth.Sms.TwilioVerify.Enabled: env = append( env, "GOTRUE_SMS_PROVIDER=twilio_verify", "GOTRUE_SMS_TWILIO_VERIFY_ACCOUNT_SID="+utils.Config.Auth.Sms.TwilioVerify.AccountSid, "GOTRUE_SMS_TWILIO_VERIFY_AUTH_TOKEN="+utils.Config.Auth.Sms.TwilioVerify.AuthToken.Value, "GOTRUE_SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.TwilioVerify.MessageServiceSid, ) case utils.Config.Auth.Sms.Messagebird.Enabled: env = append( env, "GOTRUE_SMS_PROVIDER=messagebird", "GOTRUE_SMS_MESSAGEBIRD_ACCESS_KEY="+utils.Config.Auth.Sms.Messagebird.AccessKey.Value, "GOTRUE_SMS_MESSAGEBIRD_ORIGINATOR="+utils.Config.Auth.Sms.Messagebird.Originator, ) case utils.Config.Auth.Sms.Textlocal.Enabled: env = append( env, "GOTRUE_SMS_PROVIDER=textlocal", "GOTRUE_SMS_TEXTLOCAL_API_KEY="+utils.Config.Auth.Sms.Textlocal.ApiKey.Value, "GOTRUE_SMS_TEXTLOCAL_SENDER="+utils.Config.Auth.Sms.Textlocal.Sender, ) case utils.Config.Auth.Sms.Vonage.Enabled: env = append( env, "GOTRUE_SMS_PROVIDER=vonage", "GOTRUE_SMS_VONAGE_API_KEY="+utils.Config.Auth.Sms.Vonage.ApiKey, "GOTRUE_SMS_VONAGE_API_SECRET="+utils.Config.Auth.Sms.Vonage.ApiSecret.Value, "GOTRUE_SMS_VONAGE_FROM="+utils.Config.Auth.Sms.Vonage.From, ) } if captcha := utils.Config.Auth.Captcha; captcha != nil { env = append( env, fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_ENABLED=%v", captcha.Enabled), fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_PROVIDER=%v", captcha.Provider), fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_SECRET=%v", captcha.Secret.Value), ) } if hook := utils.Config.Auth.Hook.MFAVerificationAttempt; hook != nil && hook.Enabled { env = append( env, "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED=true", "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="+hook.URI, "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value, ) } if hook := utils.Config.Auth.Hook.PasswordVerificationAttempt; hook != nil && hook.Enabled { env = append( env, "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED=true", "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="+hook.URI, "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value, ) } if hook := utils.Config.Auth.Hook.CustomAccessToken; hook != nil && hook.Enabled { env = append( env, "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED=true", "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="+hook.URI, "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS="+hook.Secrets.Value, ) } if hook := utils.Config.Auth.Hook.SendSMS; hook != nil && hook.Enabled { env = append( env, "GOTRUE_HOOK_SEND_SMS_ENABLED=true", "GOTRUE_HOOK_SEND_SMS_URI="+hook.URI, "GOTRUE_HOOK_SEND_SMS_SECRETS="+hook.Secrets.Value, ) } if hook := utils.Config.Auth.Hook.SendEmail; hook != nil && hook.Enabled { env = append( env, "GOTRUE_HOOK_SEND_EMAIL_ENABLED=true", "GOTRUE_HOOK_SEND_EMAIL_URI="+hook.URI, "GOTRUE_HOOK_SEND_EMAIL_SECRETS="+hook.Secrets.Value, ) } if utils.Config.Auth.MFA.Phone.EnrollEnabled || utils.Config.Auth.MFA.Phone.VerifyEnabled { env = append( env, "GOTRUE_MFA_PHONE_TEMPLATE="+utils.Config.Auth.MFA.Phone.Template, fmt.Sprintf("GOTRUE_MFA_PHONE_OTP_LENGTH=%v", utils.Config.Auth.MFA.Phone.OtpLength), fmt.Sprintf("GOTRUE_MFA_PHONE_MAX_FREQUENCY=%v", utils.Config.Auth.MFA.Phone.MaxFrequency), ) } for name, config := range utils.Config.Auth.External { env = append( env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled), fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId), fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret.Value), fmt.Sprintf("GOTRUE_EXTERNAL_%s_SKIP_NONCE_CHECK=%t", strings.ToUpper(name), config.SkipNonceCheck), ) redirectUri := config.RedirectUri if redirectUri == "" { redirectUri = utils.GetApiUrl("/auth/v1/callback") } env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), redirectUri)) if config.Url != "" { env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url)) } } if _, err := utils.DockerStart( ctx, container.Config{ Image: utils.Config.Auth.Image, Env: env, ExposedPorts: nat.PortSet{"9999/tcp": {}}, Healthcheck: &container.HealthConfig{ Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9999/health", }, Interval: 10 * time.Second, Timeout: 2 * time.Second, Retries: 3, }, }, container.HostConfig{ RestartPolicy: container.RestartPolicy{Name: "always"}, }, network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ utils.NetId: { Aliases: utils.GotrueAliases, }, }, }, utils.GotrueId, ); err != nil { return err } started = append(started, utils.GotrueId) } // Start PostgREST. if utils.Config.Api.Enabled && !isContainerExcluded(utils.Config.Api.Image, excluded) { if _, err := utils.DockerStart( ctx, container.Config{ Image: utils.Config.Api.Image, Env: []string{ fmt.Sprintf("PGRST_DB_URI=postgresql://authenticator:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database), "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","), "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","), fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows), "PGRST_DB_ANON_ROLE=anon", fmt.Sprintf("PGRST_JWT_SECRET=%s", jwks), "PGRST_ADMIN_SERVER_PORT=3001", }, // PostgREST does not expose a shell for health check }, container.HostConfig{ RestartPolicy: container.RestartPolicy{Name: "always"}, }, network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ utils.NetId: { Aliases: utils.RestAliases, }, }, }, utils.RestId, ); err != nil { return err } started = append(started, utils.RestId) } // Start Storage. if isStorageEnabled { dockerStoragePath := "/mnt" if _, err := utils.DockerStart( ctx, container.Config{ Image: utils.Config.Storage.Image, Env: []string{ "ANON_KEY=" + utils.Config.Auth.AnonKey, "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey, "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret, fmt.Sprintf("AUTH_JWT_JWKS=%s", jwks), fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database), fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit), "STORAGE_BACKEND=file", "FILE_STORAGE_BACKEND_PATH=" + dockerStoragePath, "TENANT_ID=stub", // TODO: https://github.com/supabase/storage-api/issues/55 "STORAGE_S3_REGION=" + utils.Config.Storage.S3Credentials.Region, "GLOBAL_S3_BUCKET=stub", fmt.Sprintf("ENABLE_IMAGE_TRANSFORMATION=%t", isImgProxyEnabled), fmt.Sprintf("IMGPROXY_URL=http://%s:5001", utils.ImgProxyId), "TUS_URL_PATH=/storage/v1/upload/resumable", "S3_PROTOCOL_ACCESS_KEY_ID=" + utils.Config.Storage.S3Credentials.AccessKeyId, "S3_PROTOCOL_ACCESS_KEY_SECRET=" + utils.Config.Storage.S3Credentials.SecretAccessKey, "S3_PROTOCOL_PREFIX=/storage/v1", fmt.Sprintf("S3_ALLOW_FORWARDED_HEADER=%v", StorageVersionBelow("1.10.1")), "UPLOAD_FILE_SIZE_LIMIT=52428800000", "UPLOAD_FILE_SIZE_LIMIT_STANDARD=5242880000", }, Healthcheck: &container.HealthConfig{ // For some reason, localhost resolves to IPv6 address on GitPod which breaks healthcheck. Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5000/status", }, Interval: 10 * time.Second, Timeout: 2 * time.Second, Retries: 3, }, }, container.HostConfig{ RestartPolicy: container.RestartPolicy{Name: "always"}, Binds: []string{utils.StorageId + ":" + dockerStoragePath}, }, network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ utils.NetId: { Aliases: utils.StorageAliases, }, }, }, utils.StorageId, ); err != nil { return err } started = append(started, utils.StorageId) } p.Send(utils.StatusMsg("Waiting for health checks...")) if utils.NoBackupVolume && utils.SliceContains(started, utils.StorageId) { if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.StorageId); err != nil { return err } // Disable prompts when seeding if err := buckets.Run(ctx, "", false, fsys); err != nil { return err } } return start.WaitForHealthyService(ctx, serviceTimeout, started...) } func isContainerExcluded(imageName string, excluded map[string]bool) bool { short := utils.ShortContainerImageName(imageName) val, ok := excluded[short] return ok && val } func ExcludableContainers() []string { names := []string{} for _, image := range config.Images.Services() { names = append(names, utils.ShortContainerImageName(image)) } return names } func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) { numOfKeyPairs := len(input) i := 0 for k, v := range input { output.WriteString(k) output.WriteString(":") output.WriteString(v) i++ if i < numOfKeyPairs { output.WriteString(",") } } }