distinct setup/deploy scripts

This commit is contained in:
2026-01-13 13:14:02 +00:00
parent 86c5a4ef8d
commit 156af3d64c
3 changed files with 112 additions and 37 deletions

View File

@@ -12,15 +12,18 @@ Start a new PocketBase project (optionally provide a service name via `pb init <
Run the local dev server. 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. 1. Setup up a firewall.
2. Install and setup Caddy. 2. Install and setup Caddy.
3. Download pocketbase and setup a systemd service. 3. Download pocketbase and configure the `.env`.
4. Copy pb_public, pb_migrations and pb_hooks. 4. Configure the systemd service for the remote instance.
5. Start the service.
### `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 isnt present on the remote.
### logs ### logs

View File

@@ -60,7 +60,8 @@ func defaultCommands() []command {
return []command{ return []command{
{name: "init", description: "start a new PocketBase project", action: runInit}, {name: "init", description: "start a new PocketBase project", action: runInit},
{name: "dev", description: "run the PocketBase binary locally", action: runDev}, {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: "logs", description: "show PocketBase logs", action: placeholderAction("logs")},
{name: "secrets", description: "manage deployment secrets", action: placeholderAction("secrets")}, {name: "secrets", description: "manage deployment secrets", action: placeholderAction("secrets")},
} }

View File

@@ -20,7 +20,7 @@ import (
const ( const (
defaultServiceDirTemplate = "/root/pb/{service}" defaultServiceDirTemplate = "/root/pb/{service}"
defaultEnvFileTemplate = "/root/pb/{service}/.env" defaultEnvFileTemplate = "/root/pb/{service}/.env"
totalDeploySteps = 6 totalSetupSteps = 5
) )
type pbToml struct { type pbToml struct {
@@ -39,37 +39,48 @@ type pocketBaseConfig struct {
ServiceName string `toml:"service_name"` 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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return err return nil, err
} }
configPath := filepath.Join(cwd, "pb.toml") configPath := filepath.Join(cwd, "pb.toml")
cfg, err := loadPBConfig(configPath) cfg, err := loadPBConfig(configPath)
if err != nil { if err != nil {
return err return nil, err
} }
serviceName := cfg.PocketBase.ServiceName serviceName := cfg.PocketBase.ServiceName
if 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 serverIP := cfg.Server.IP
if serverIP == "" { 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 domain := cfg.Server.Domain
if domain == "" { if domain == "" {
return fmt.Errorf("pb.toml missing [server].domain") return nil, fmt.Errorf("pb.toml missing [server].domain")
} }
port := cfg.Server.Port port := cfg.Server.Port
if port <= 0 { 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 version := cfg.PocketBase.Version
@@ -82,9 +93,38 @@ func runDeploy() error {
unitServiceDir := renderServiceTemplate(defaultServiceDirTemplate, "%i") unitServiceDir := renderServiceTemplate(defaultServiceDirTemplate, "%i")
unitEnvFile := renderServiceTemplate(defaultEnvFileTemplate, "%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 step := 1
printStep(step, totalDeploySteps, "validating configuration") printStep(step, totalSetupSteps, "validating configuration")
remoteOS, err := runSSHOutput(serverIP, "uname -s") remoteOS, err := runSSHOutput(ctx.serverIP, "uname -s")
if err != nil { if err != nil {
return fmt.Errorf("failed to determine remote OS: %w", err) 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) return fmt.Errorf("unsupported remote OS %q", remoteOS)
} }
arch, err := detectRemoteArch(serverIP) arch, err := detectRemoteArch(ctx.serverIP)
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("arch; %s", arch) assetName := pocketbaseAsset(ctx.version, "linux", arch)
assetURL := fmt.Sprintf("https://github.com/pocketbase/pocketbase/releases/download/v%s/%s", ctx.version, assetName)
assetName := pocketbaseAsset(version, "linux", arch)
assetURL := fmt.Sprintf("https://github.com/pocketbase/pocketbase/releases/download/v%s/%s", version, assetName)
step++ step++
printStep(step, totalDeploySteps, "configuring firewall") printStep(step, totalSetupSteps, "configuring firewall")
if err := runSSHCommand(serverIP, firewallScript(port)); err != nil { if err := runSSHCommand(ctx.serverIP, firewallScript(ctx.port)); err != nil {
return fmt.Errorf("firewall setup failed: %w", err) return fmt.Errorf("firewall setup failed: %w", err)
} }
step++ step++
printStep(step, totalDeploySteps, "installing caddy") printStep(step, totalSetupSteps, "installing caddy")
if err := runSSHCommand(serverIP, caddyScript(domain, port, serviceName)); err != nil { if err := runSSHCommand(ctx.serverIP, caddyScript(ctx.domain, ctx.port, ctx.serviceName)); err != nil {
return fmt.Errorf("caddy setup failed: %w", err) return fmt.Errorf("caddy setup failed: %w", err)
} }
step++ step++
printStep(step, totalDeploySteps, "deploying PocketBase binary") printStep(step, totalSetupSteps, "deploying PocketBase binary")
if err := runSSHCommand(serverIP, pocketbaseSetupScript(serviceDir, envFile, version, assetURL, port)); err != nil { 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) return fmt.Errorf("PocketBase setup failed: %w", err)
} }
step++ step++
printStep(step, totalDeploySteps, "syncing migrations/hooks/static assets") printStep(step, totalSetupSteps, "configuring systemd service")
if err := syncLocalDirectories(serverIP, serviceDir, []string{"pb_migrations", "pb_hooks", "pb_public"}); err != nil { if err := runSSHCommand(ctx.serverIP, systemdScript(ctx.unitServiceDir, ctx.unitEnvFile, ctx.serviceName)); 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 {
return fmt.Errorf("systemd setup failed: %w", err) return fmt.Errorf("systemd setup failed: %w", err)
} }
fmt.Printf("\nDeployment complete; PocketBase should be reachable at https://%s\n", domain)
return nil 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) { func loadPBConfig(path string) (*pbToml, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {