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.
### `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 isnt present on the remote.
### logs

View File

@@ -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")},
}

View File

@@ -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 {