distinct setup/deploy scripts
This commit is contained in:
13
README.md
13
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
|
||||
|
||||
|
||||
3
main.go
3
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")},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
Reference in New Issue
Block a user