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 }