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.
|
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 isn’t present on the remote.
|
||||||
|
|
||||||
### logs
|
### logs
|
||||||
|
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -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")},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
Reference in New Issue
Block a user