github.com/supabase/cli@v1.168.1/internal/db/start/start.go (about) 1 package start 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 "io" 8 "os" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/docker/docker/api/types/container" 14 "github.com/docker/docker/api/types/network" 15 "github.com/docker/docker/client" 16 "github.com/docker/go-connections/nat" 17 "github.com/go-errors/errors" 18 "github.com/jackc/pgconn" 19 "github.com/jackc/pgx/v4" 20 "github.com/spf13/afero" 21 "github.com/supabase/cli/internal/db/push" 22 "github.com/supabase/cli/internal/migration/apply" 23 "github.com/supabase/cli/internal/status" 24 "github.com/supabase/cli/internal/utils" 25 ) 26 27 var ( 28 ErrDatabase = errors.New("database is not healthy") 29 HealthTimeout = 120 * time.Second 30 //go:embed templates/schema.sql 31 initialSchema string 32 ) 33 34 func Run(ctx context.Context, fsys afero.Fs) error { 35 if err := utils.LoadConfigFS(fsys); err != nil { 36 return err 37 } 38 if err := utils.AssertSupabaseDbIsRunning(); err == nil { 39 fmt.Fprintln(os.Stderr, "Postgres database is already running.") 40 return nil 41 } else if !errors.Is(err, utils.ErrNotRunning) { 42 return err 43 } 44 // Skip logflare container in db start 45 utils.Config.Analytics.Enabled = false 46 err := StartDatabase(ctx, fsys, os.Stderr) 47 if err != nil { 48 if err := utils.DockerRemoveAll(context.Background(), io.Discard); err != nil { 49 fmt.Fprintln(os.Stderr, err) 50 } 51 } 52 return err 53 } 54 55 func NewContainerConfig() container.Config { 56 env := []string{ 57 "POSTGRES_PASSWORD=" + utils.Config.Db.Password, 58 "POSTGRES_HOST=/var/run/postgresql", 59 "POSTGRES_INITDB_ARGS=--lc-ctype=C.UTF-8", 60 "JWT_SECRET=" + utils.Config.Auth.JwtSecret, 61 fmt.Sprintf("JWT_EXP=%d", utils.Config.Auth.JwtExpiry), 62 } 63 if len(utils.Config.Experimental.OrioleDBVersion) > 0 { 64 env = append(env, 65 "POSTGRES_INITDB_ARGS=--lc-collate=C", 66 fmt.Sprintf("S3_ENABLED=%t", true), 67 "S3_HOST="+utils.Config.Experimental.S3Host, 68 "S3_REGION="+utils.Config.Experimental.S3Region, 69 "S3_ACCESS_KEY="+utils.Config.Experimental.S3AccessKey, 70 "S3_SECRET_KEY="+utils.Config.Experimental.S3SecretKey, 71 ) 72 } else { 73 env = append(env, "POSTGRES_INITDB_ARGS=--lc-collate=C.UTF-8") 74 } 75 config := container.Config{ 76 Image: utils.Config.Db.Image, 77 Env: env, 78 Healthcheck: &container.HealthConfig{ 79 Test: []string{"CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1", "-p", "5432"}, 80 Interval: 10 * time.Second, 81 Timeout: 2 * time.Second, 82 Retries: 3, 83 }, 84 Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/postgresql.schema.sql && cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && docker-entrypoint.sh postgres -D /etc/postgresql 85 ` + initialSchema + ` 86 EOF 87 ` + utils.Config.Db.RootKey + ` 88 EOF 89 `}, 90 } 91 if utils.Config.Db.MajorVersion >= 14 { 92 config.Cmd = []string{"postgres", 93 "-c", "config_file=/etc/postgresql/postgresql.conf", 94 // Ref: https://postgrespro.com/list/thread-id/2448092 95 "-c", `search_path="$user",public,extensions`, 96 } 97 } 98 return config 99 } 100 101 func NewHostConfig() container.HostConfig { 102 hostPort := strconv.FormatUint(uint64(utils.Config.Db.Port), 10) 103 hostConfig := WithSyslogConfig(container.HostConfig{ 104 PortBindings: nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}}, 105 RestartPolicy: container.RestartPolicy{Name: "always"}, 106 Binds: []string{ 107 utils.DbId + ":/var/lib/postgresql/data", 108 utils.ConfigId + ":/etc/postgresql-custom", 109 }, 110 ExtraHosts: []string{"host.docker.internal:host-gateway"}, 111 }) 112 return hostConfig 113 } 114 115 func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error { 116 config := NewContainerConfig() 117 hostConfig := NewHostConfig() 118 networkingConfig := network.NetworkingConfig{ 119 EndpointsConfig: map[string]*network.EndpointSettings{ 120 utils.NetId: { 121 Aliases: utils.DbAliases, 122 }, 123 }, 124 } 125 if utils.Config.Db.MajorVersion <= 14 { 126 config.Entrypoint = nil 127 hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""} 128 } 129 // Creating volume will not override existing volume, so we must inspect explicitly 130 _, err := utils.Docker.VolumeInspect(ctx, utils.DbId) 131 utils.NoBackupVolume = client.IsErrNotFound(err) 132 if utils.NoBackupVolume { 133 fmt.Fprintln(w, "Starting database...") 134 } else { 135 fmt.Fprintln(w, "Starting database from backup...") 136 } 137 if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil { 138 return err 139 } 140 if !WaitForHealthyService(ctx, utils.DbId, HealthTimeout) { 141 return errors.New(ErrDatabase) 142 } 143 // Initialize if we are on PG14 and there's no existing db volume 144 if utils.NoBackupVolume { 145 if err := setupDatabase(ctx, fsys, w, options...); err != nil { 146 return err 147 } 148 } 149 return initCurrentBranch(fsys) 150 } 151 152 func RetryEverySecond(ctx context.Context, callback func() bool, timeout time.Duration) bool { 153 now := time.Now() 154 expiry := now.Add(timeout) 155 ticker := time.NewTicker(time.Second) 156 defer ticker.Stop() 157 for t := now; t.Before(expiry) && ctx.Err() == nil; t = <-ticker.C { 158 if callback() { 159 return true 160 } 161 } 162 return false 163 } 164 165 func WaitForHealthyService(ctx context.Context, container string, timeout time.Duration) bool { 166 probe := func() bool { 167 return status.AssertContainerHealthy(ctx, container) == nil 168 } 169 return RetryEverySecond(ctx, probe, timeout) 170 } 171 172 func WithSyslogConfig(hostConfig container.HostConfig) container.HostConfig { 173 if utils.Config.Analytics.Enabled { 174 hostConfig.LogConfig.Type = "syslog" 175 hostConfig.LogConfig.Config = map[string]string{ 176 "syslog-address": fmt.Sprintf("tcp://%s:%d", utils.Config.Hostname, utils.Config.Analytics.VectorPort), 177 "tag": "{{.Name}}", 178 } 179 } 180 return hostConfig 181 } 182 183 func initCurrentBranch(fsys afero.Fs) error { 184 // Create _current_branch file to avoid breaking db branch commands 185 if _, err := fsys.Stat(utils.CurrBranchPath); err == nil { 186 return nil 187 } else if !errors.Is(err, os.ErrNotExist) { 188 return errors.Errorf("failed init current branch: %w", err) 189 } 190 return utils.WriteFile(utils.CurrBranchPath, []byte("main"), fsys) 191 } 192 193 func initSchema(ctx context.Context, conn *pgx.Conn, host string, w io.Writer) error { 194 fmt.Fprintln(w, "Setting up initial schema...") 195 if utils.Config.Db.MajorVersion <= 14 { 196 return initSchema14(ctx, conn) 197 } 198 return initSchema15(ctx, host) 199 } 200 201 func initSchema14(ctx context.Context, conn *pgx.Conn) error { 202 if err := apply.BatchExecDDL(ctx, conn, strings.NewReader(utils.GlobalsSql)); err != nil { 203 return err 204 } 205 return apply.BatchExecDDL(ctx, conn, strings.NewReader(utils.InitialSchemaSql)) 206 } 207 208 func initSchema15(ctx context.Context, host string) error { 209 // Apply service migrations 210 logger := utils.GetDebugLogger() 211 if err := utils.DockerRunOnceWithStream(ctx, utils.RealtimeImage, []string{ 212 "PORT=4000", 213 "DB_HOST=" + host, 214 "DB_PORT=5432", 215 "DB_USER=supabase_admin", 216 "DB_PASSWORD=" + utils.Config.Db.Password, 217 "DB_NAME=postgres", 218 "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime", 219 "DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey, 220 "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret, 221 "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret, 222 "FLY_APP_NAME=realtime", 223 "SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase, 224 "ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion), 225 "ENABLE_TAILSCALE=false", 226 "DNS_NODES=''", 227 "RLIMIT_NOFILE=", 228 fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength), 229 }, []string{"/bin/sh", "-c", fmt.Sprintf( 230 `/app/bin/migrate && /app/bin/realtime eval 'Realtime.Release.seeds(Realtime.Repo); Application.ensure_all_started(:realtime); Realtime.Tenants.health_check("%s")'`, 231 utils.Config.Realtime.TenantId, 232 )}, io.Discard, logger); err != nil { 233 return err 234 } 235 if err := utils.DockerRunOnceWithStream(ctx, utils.Config.Storage.Image, []string{ 236 "DB_INSTALL_ROLES=false", 237 "ANON_KEY=" + utils.Config.Auth.AnonKey, 238 "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey, 239 "PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret, 240 fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host), 241 fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit), 242 "STORAGE_BACKEND=file", 243 "TENANT_ID=stub", 244 // TODO: https://github.com/supabase/storage-api/issues/55 245 "REGION=stub", 246 "GLOBAL_S3_BUCKET=stub", 247 }, []string{"node", "dist/scripts/migrate-call.js"}, io.Discard, logger); err != nil { 248 return err 249 } 250 return utils.DockerRunOnceWithStream(ctx, utils.Config.Auth.Image, []string{ 251 fmt.Sprintf("API_EXTERNAL_URL=http://%s:%d", host, utils.Config.Api.Port), 252 "GOTRUE_LOG_LEVEL=error", 253 "GOTRUE_DB_DRIVER=postgres", 254 fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host), 255 "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl, 256 "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret, 257 }, []string{"gotrue", "migrate"}, io.Discard, logger) 258 } 259 260 func setupDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error { 261 conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...) 262 if err != nil { 263 return err 264 } 265 defer conn.Close(context.Background()) 266 if err := SetupDatabase(ctx, conn, utils.DbId, w, fsys); err != nil { 267 return err 268 } 269 return apply.MigrateAndSeed(ctx, "", conn, fsys) 270 } 271 272 func SetupDatabase(ctx context.Context, conn *pgx.Conn, host string, w io.Writer, fsys afero.Fs) error { 273 if err := initSchema(ctx, conn, host, w); err != nil { 274 return err 275 } 276 return push.CreateCustomRoles(ctx, conn, os.Stderr, fsys) 277 }