From 156af3d64c1f43f3cdc7d61f9e7c867af21d4957 Mon Sep 17 00:00:00 2001 From: Nick Goodall Date: Tue, 13 Jan 2026 13:14:02 +0000 Subject: [PATCH] distinct setup/deploy scripts --- README.md | 13 +++-- main.go | 3 +- deploy.go => setup.go | 133 ++++++++++++++++++++++++++++++++---------- 3 files changed, 112 insertions(+), 37 deletions(-) rename deploy.go => setup.go (76%) diff --git a/README.md b/README.md index 92d65a4..9219ea8 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,18 @@ Start a new PocketBase project (optionally provide a service name via `pb init < Run the local dev server. -### `deploy` +### `setup` -Deploys pocketbase to a remote server. This will: +Provision a remote PocketBase server. This will: 1. Setup up a firewall. 2. Install and setup Caddy. -3. Download pocketbase and setup a systemd service. -4. Copy pb_public, pb_migrations and pb_hooks. -5. Start the service. +3. Download pocketbase and configure the `.env`. +4. Configure the systemd service for the remote instance. + +### `deploy` + +Syncs `pb_public`, `pb_migrations`, and `pb_hooks`, then restarts the remote PocketBase service. The command will automatically run `setup` if the PocketBase binary isn’t present on the remote. ### logs diff --git a/main.go b/main.go index 8dad454..63da1ce 100644 --- a/main.go +++ b/main.go @@ -60,7 +60,8 @@ func defaultCommands() []command { return []command{ {name: "init", description: "start a new PocketBase project", action: runInit}, {name: "dev", description: "run the PocketBase binary locally", action: runDev}, - {name: "deploy", description: "deploy the PocketBase project", action: runDeploy}, + {name: "setup", description: "provision the remote server and install PocketBase", action: runSetup}, + {name: "deploy", description: "sync migrations/hooks/static assets (runs setup if needed)", action: runDeploy}, {name: "logs", description: "show PocketBase logs", action: placeholderAction("logs")}, {name: "secrets", description: "manage deployment secrets", action: placeholderAction("secrets")}, } diff --git a/deploy.go b/setup.go similarity index 76% rename from deploy.go rename to setup.go index 9ba7543..19ebcc3 100644 --- a/deploy.go +++ b/setup.go @@ -20,7 +20,7 @@ import ( const ( defaultServiceDirTemplate = "/root/pb/{service}" defaultEnvFileTemplate = "/root/pb/{service}/.env" - totalDeploySteps = 6 + totalSetupSteps = 5 ) type pbToml struct { @@ -39,37 +39,48 @@ type pocketBaseConfig struct { ServiceName string `toml:"service_name"` } -func runDeploy() error { +type deploymentContext struct { + serverIP string + domain string + port int + serviceName string + version string + serviceDir string + envFile string + unitServiceDir string + unitEnvFile string +} + +func buildDeploymentContext() (*deploymentContext, error) { cwd, err := os.Getwd() if err != nil { - return err + return nil, err } configPath := filepath.Join(cwd, "pb.toml") cfg, err := loadPBConfig(configPath) if err != nil { - return err + return nil, err } serviceName := cfg.PocketBase.ServiceName if serviceName == "" { - return fmt.Errorf("pb.toml missing [pocketbase].service_name") + return nil, fmt.Errorf("pb.toml missing [pocketbase].service_name") } serverIP := cfg.Server.IP if serverIP == "" { - return fmt.Errorf("pb.toml missing [server].ip") + return nil, fmt.Errorf("pb.toml missing [server].ip") } - defer closeSSHControlMaster(serverIP) domain := cfg.Server.Domain if domain == "" { - return fmt.Errorf("pb.toml missing [server].domain") + return nil, fmt.Errorf("pb.toml missing [server].domain") } port := cfg.Server.Port if port <= 0 { - return fmt.Errorf("pb.toml server.port must be greater than zero") + return nil, fmt.Errorf("pb.toml server.port must be greater than zero") } version := cfg.PocketBase.Version @@ -82,9 +93,38 @@ func runDeploy() error { unitServiceDir := renderServiceTemplate(defaultServiceDirTemplate, "%i") unitEnvFile := renderServiceTemplate(defaultEnvFileTemplate, "%i") + return &deploymentContext{ + serverIP: serverIP, + domain: domain, + port: port, + serviceName: serviceName, + version: version, + serviceDir: serviceDir, + envFile: envFile, + unitServiceDir: unitServiceDir, + unitEnvFile: unitEnvFile, + }, nil +} + +func runSetup() error { + ctx, err := buildDeploymentContext() + if err != nil { + return err + } + defer closeSSHControlMaster(ctx.serverIP) + + if err := performSetup(ctx); err != nil { + return err + } + + fmt.Printf("\nSetup complete; PocketBase should be reachable at https://%s\n", ctx.domain) + return nil +} + +func performSetup(ctx *deploymentContext) error { step := 1 - printStep(step, totalDeploySteps, "validating configuration") - remoteOS, err := runSSHOutput(serverIP, "uname -s") + printStep(step, totalSetupSteps, "validating configuration") + remoteOS, err := runSSHOutput(ctx.serverIP, "uname -s") if err != nil { return fmt.Errorf("failed to determine remote OS: %w", err) } @@ -92,50 +132,81 @@ func runDeploy() error { return fmt.Errorf("unsupported remote OS %q", remoteOS) } - arch, err := detectRemoteArch(serverIP) + arch, err := detectRemoteArch(ctx.serverIP) if err != nil { return err } - fmt.Printf("arch; %s", arch) - - assetName := pocketbaseAsset(version, "linux", arch) - assetURL := fmt.Sprintf("https://github.com/pocketbase/pocketbase/releases/download/v%s/%s", version, assetName) + assetName := pocketbaseAsset(ctx.version, "linux", arch) + assetURL := fmt.Sprintf("https://github.com/pocketbase/pocketbase/releases/download/v%s/%s", ctx.version, assetName) step++ - printStep(step, totalDeploySteps, "configuring firewall") - if err := runSSHCommand(serverIP, firewallScript(port)); err != nil { + printStep(step, totalSetupSteps, "configuring firewall") + if err := runSSHCommand(ctx.serverIP, firewallScript(ctx.port)); err != nil { return fmt.Errorf("firewall setup failed: %w", err) } step++ - printStep(step, totalDeploySteps, "installing caddy") - if err := runSSHCommand(serverIP, caddyScript(domain, port, serviceName)); err != nil { + printStep(step, totalSetupSteps, "installing caddy") + if err := runSSHCommand(ctx.serverIP, caddyScript(ctx.domain, ctx.port, ctx.serviceName)); err != nil { return fmt.Errorf("caddy setup failed: %w", err) } step++ - printStep(step, totalDeploySteps, "deploying PocketBase binary") - if err := runSSHCommand(serverIP, pocketbaseSetupScript(serviceDir, envFile, version, assetURL, port)); err != nil { + printStep(step, totalSetupSteps, "deploying PocketBase binary") + if err := runSSHCommand(ctx.serverIP, pocketbaseSetupScript(ctx.serviceDir, ctx.envFile, ctx.version, assetURL, ctx.port)); err != nil { return fmt.Errorf("PocketBase setup failed: %w", err) } step++ - printStep(step, totalDeploySteps, "syncing migrations/hooks/static assets") - if err := syncLocalDirectories(serverIP, serviceDir, []string{"pb_migrations", "pb_hooks", "pb_public"}); err != nil { - return fmt.Errorf("asset sync failed: %w", err) - } - - step++ - printStep(step, totalDeploySteps, "configuring systemd service") - if err := runSSHCommand(serverIP, systemdScript(unitServiceDir, unitEnvFile, serviceName)); err != nil { + printStep(step, totalSetupSteps, "configuring systemd service") + if err := runSSHCommand(ctx.serverIP, systemdScript(ctx.unitServiceDir, ctx.unitEnvFile, ctx.serviceName)); err != nil { return fmt.Errorf("systemd setup failed: %w", err) } - fmt.Printf("\nDeployment complete; PocketBase should be reachable at https://%s\n", domain) return nil } +func runDeploy() error { + ctx, err := buildDeploymentContext() + if err != nil { + return err + } + defer closeSSHControlMaster(ctx.serverIP) + + binaryPath := filepath.Join(ctx.serviceDir, "pocketbase") + exists, err := remoteBinaryExists(ctx.serverIP, binaryPath) + if err != nil { + return err + } + if !exists { + fmt.Println("PocketBase binary missing on remote; running setup") + if err := performSetup(ctx); err != nil { + return err + } + } + + if err := syncLocalDirectories(ctx.serverIP, ctx.serviceDir, []string{"pb_migrations", "pb_hooks", "pb_public"}); err != nil { + return fmt.Errorf("asset sync failed: %w", err) + } + + if err := runSSHCommand(ctx.serverIP, systemdScript(ctx.unitServiceDir, ctx.unitEnvFile, ctx.serviceName)); err != nil { + return fmt.Errorf("systemd restart failed: %w", err) + } + + fmt.Printf("\nDeployment complete; PocketBase should be reachable at https://%s\n", ctx.domain) + return nil +} + +func remoteBinaryExists(server, path string) (bool, error) { + script := fmt.Sprintf(`if [ -f %q ]; then printf yes; else printf no; fi`, path) + output, err := runSSHOutput(server, script) + if err != nil { + return false, err + } + return strings.TrimSpace(output) == "yes", nil +} + func loadPBConfig(path string) (*pbToml, error) { data, err := os.ReadFile(path) if err != nil {