Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1af588df2f | |||
| 2d65ceac6b | |||
| f9236b4e6b | |||
| 45feec63aa | |||
| b27dca0a69 | |||
| de7a34b16e | |||
| 603c5db778 | |||
| 239dbfc27e |
@@ -177,13 +177,33 @@ The gateway runs on :8080 by default, so services should use other ports.
|
||||
|
||||
### Deployment
|
||||
|
||||
Deploy to any Linux server with systemd:
|
||||
|
||||
```bash
|
||||
micro build # Build Go binaries to ./bin/
|
||||
micro build --os linux # Cross-compile for Linux
|
||||
micro deploy --ssh user@host # Deploy via SSH
|
||||
# On your server (one-time setup)
|
||||
curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
sudo micro init --server
|
||||
|
||||
# From your laptop
|
||||
micro deploy user@your-server
|
||||
```
|
||||
|
||||
No Docker required. Go binaries are self-contained.
|
||||
The deploy command:
|
||||
1. Builds binaries for Linux
|
||||
2. Copies via SSH to the server
|
||||
3. Sets up systemd services
|
||||
4. Verifies services are healthy
|
||||
|
||||
Manage deployed services:
|
||||
```bash
|
||||
micro status --remote user@server # Check status
|
||||
micro logs --remote user@server # View logs
|
||||
micro logs myservice --remote user@server -f # Follow specific service
|
||||
```
|
||||
|
||||
No Docker required. No Kubernetes. Just systemd.
|
||||
|
||||
See [docs/deployment.md](docs/deployment.md) for full deployment guide.
|
||||
|
||||
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
|
||||
|
||||
|
||||
@@ -272,6 +272,74 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
## Building and Deployment
|
||||
|
||||
### Build Binaries
|
||||
|
||||
Build Go binaries for deployment:
|
||||
|
||||
```bash
|
||||
micro build # Build for current OS
|
||||
micro build --os linux # Cross-compile for Linux
|
||||
micro build --os linux --arch arm64 # For ARM64
|
||||
micro build --output ./dist # Custom output directory
|
||||
```
|
||||
|
||||
### Deploy to Server
|
||||
|
||||
Deploy to any Linux server with systemd:
|
||||
|
||||
```bash
|
||||
# First time: set up the server
|
||||
ssh user@server
|
||||
curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
sudo micro init --server
|
||||
exit
|
||||
|
||||
# Deploy from your laptop
|
||||
micro deploy user@server
|
||||
```
|
||||
|
||||
The deploy command:
|
||||
1. Builds binaries for linux/amd64
|
||||
2. Copies via SSH to `/opt/micro/bin/`
|
||||
3. Sets up systemd services (`micro@<service>`)
|
||||
4. Restarts and verifies services are running
|
||||
|
||||
### Named Deploy Targets
|
||||
|
||||
Add deploy targets to `micro.mu`:
|
||||
|
||||
```
|
||||
deploy prod
|
||||
ssh deploy@prod.example.com
|
||||
|
||||
deploy staging
|
||||
ssh deploy@staging.example.com
|
||||
```
|
||||
|
||||
Then:
|
||||
```bash
|
||||
micro deploy prod # Deploy to production
|
||||
micro deploy staging # Deploy to staging
|
||||
```
|
||||
|
||||
### Managing Deployed Services
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
micro status --remote user@server
|
||||
|
||||
# View logs
|
||||
micro logs --remote user@server
|
||||
micro logs myservice --remote user@server -f
|
||||
|
||||
# Stop a service
|
||||
micro stop myservice --remote user@server
|
||||
```
|
||||
|
||||
See [docs/deployment.md](../../docs/deployment.md) for the full deployment guide.
|
||||
|
||||
## Protobuf
|
||||
|
||||
Use protobuf for code generation with [protoc-gen-micro](https://github.com/micro/go-micro/tree/master/cmd/protoc-gen-micro)
|
||||
|
||||
+11
-190
@@ -1,16 +1,11 @@
|
||||
package microcli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v5/client"
|
||||
@@ -21,6 +16,12 @@ import (
|
||||
|
||||
"go-micro.dev/v5/cmd/micro/cli/new"
|
||||
"go-micro.dev/v5/cmd/micro/cli/util"
|
||||
|
||||
// Import packages that register commands via init()
|
||||
_ "go-micro.dev/v5/cmd/micro/cli/build"
|
||||
_ "go-micro.dev/v5/cmd/micro/cli/deploy"
|
||||
_ "go-micro.dev/v5/cmd/micro/cli/init"
|
||||
_ "go-micro.dev/v5/cmd/micro/cli/remote"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -57,55 +58,6 @@ func genTextHandler(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func lastNonEmptyLine(s string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
if strings.TrimSpace(lines[i]) != "" {
|
||||
return lines[i]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func lastLogLine(path string) string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
var last string
|
||||
scan := bufio.NewScanner(f)
|
||||
for scan.Scan() {
|
||||
if strings.TrimSpace(scan.Text()) != "" {
|
||||
last = scan.Text()
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
func waitAndCleanup(procs []*exec.Cmd, pidFiles []string) {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt)
|
||||
go func() {
|
||||
<-ch
|
||||
for _, proc := range procs {
|
||||
if proc.Process != nil {
|
||||
_ = proc.Process.Kill()
|
||||
}
|
||||
}
|
||||
for _, pf := range pidFiles {
|
||||
_ = os.Remove(pf)
|
||||
}
|
||||
os.Exit(1)
|
||||
}()
|
||||
for i, proc := range procs {
|
||||
_ = proc.Wait()
|
||||
if proc.Process != nil {
|
||||
_ = os.Remove(pidFiles[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.Register([]*cli.Command{
|
||||
{
|
||||
@@ -202,142 +154,11 @@ func init() {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "status",
|
||||
Usage: "Check status of running services",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home dir: %w", err)
|
||||
}
|
||||
runDir := filepath.Join(homeDir, "micro", "run")
|
||||
files, err := os.ReadDir(runDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read run dir: %w", err)
|
||||
}
|
||||
fmt.Printf("%-20s %-8s %-8s %s\n", "SERVICE", "PID", "STATUS", "DIRECTORY")
|
||||
for _, f := range files {
|
||||
if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") {
|
||||
continue
|
||||
}
|
||||
service := f.Name()[:len(f.Name())-4]
|
||||
pidFilePath := filepath.Join(runDir, f.Name())
|
||||
pidFile, err := os.Open(pidFilePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var pid int
|
||||
var dir string
|
||||
scanner := bufio.NewScanner(pidFile)
|
||||
if scanner.Scan() {
|
||||
fmt.Sscanf(scanner.Text(), "%d", &pid)
|
||||
}
|
||||
if scanner.Scan() {
|
||||
dir = scanner.Text()
|
||||
}
|
||||
pidFile.Close()
|
||||
status := "stopped"
|
||||
if pid > 0 {
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err == nil {
|
||||
if err := proc.Signal(syscall.Signal(0)); err == nil {
|
||||
status = "running"
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("%-20s %-8d %-8s %-40s %s\n", service, pid, status, "", dir)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stop",
|
||||
Usage: "Stop a running service",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
if ctx.Args().Len() != 1 {
|
||||
return fmt.Errorf("Usage: micro stop [service]")
|
||||
}
|
||||
service := ctx.Args().Get(0)
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home dir: %w", err)
|
||||
}
|
||||
runDir := filepath.Join(homeDir, "micro", "run")
|
||||
pidFilePath := filepath.Join(runDir, service+".pid")
|
||||
pidFile, err := os.Open(pidFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("no pid file for service %s", service)
|
||||
}
|
||||
var pid int
|
||||
var dir string
|
||||
scanner := bufio.NewScanner(pidFile)
|
||||
if scanner.Scan() {
|
||||
fmt.Sscanf(scanner.Text(), "%d", &pid)
|
||||
}
|
||||
if scanner.Scan() {
|
||||
dir = scanner.Text()
|
||||
}
|
||||
pidFile.Close()
|
||||
if pid <= 0 {
|
||||
_ = os.Remove(pidFilePath)
|
||||
return fmt.Errorf("service %s is not running", service)
|
||||
}
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
_ = os.Remove(pidFilePath)
|
||||
return fmt.Errorf("could not find process for %s", service)
|
||||
}
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
_ = os.Remove(pidFilePath)
|
||||
return fmt.Errorf("failed to stop service %s: %v", service, err)
|
||||
}
|
||||
_ = os.Remove(pidFilePath)
|
||||
fmt.Printf("Stopped service %s (pid %d) in directory %s\n", service, pid, dir)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "logs",
|
||||
Usage: "Show logs for a service, or list available logs if no service is specified",
|
||||
Action: func(ctx *cli.Context) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home dir: %w", err)
|
||||
}
|
||||
logsDir := filepath.Join(homeDir, "micro", "logs")
|
||||
if ctx.Args().Len() == 0 {
|
||||
// List available logs
|
||||
dirEntries, err := os.ReadDir(logsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not list logs directory: %v", err)
|
||||
}
|
||||
fmt.Println("Available logs:")
|
||||
found := false
|
||||
for _, entry := range dirEntries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
|
||||
fmt.Println(" ", strings.TrimSuffix(entry.Name(), ".log"))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
fmt.Println(" (no logs found)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
service := ctx.Args().Get(0)
|
||||
logFilePath := filepath.Join(logsDir, service+".log")
|
||||
f, err := os.Open(logFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open log file for service %s: %v", service, err)
|
||||
}
|
||||
defer f.Close()
|
||||
scan := bufio.NewScanner(f)
|
||||
for scan.Scan() {
|
||||
fmt.Println(scan.Text())
|
||||
}
|
||||
return scan.Err()
|
||||
},
|
||||
},
|
||||
// Note: The following commands are registered in their respective packages:
|
||||
// - status, logs, stop: remote/remote.go
|
||||
// - build: build/build.go
|
||||
// - deploy: deploy/deploy.go
|
||||
// - init: init/init.go
|
||||
}...)
|
||||
|
||||
cmd.App().Action = func(c *cli.Context) error {
|
||||
|
||||
+353
-145
@@ -6,25 +6,84 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v5/cmd"
|
||||
"go-micro.dev/v5/cmd/micro/run/config"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRemotePath = "/opt/micro"
|
||||
)
|
||||
|
||||
// Deploy deploys services to a target
|
||||
func Deploy(c *cli.Context) error {
|
||||
sshTarget := c.String("ssh")
|
||||
if sshTarget == "" {
|
||||
return fmt.Errorf("specify target with --ssh user@host")
|
||||
// Get target from args or flag
|
||||
target := c.Args().First()
|
||||
if target == "" {
|
||||
target = c.String("ssh")
|
||||
}
|
||||
|
||||
return deploySSH(c, sshTarget)
|
||||
// Load config to check for deploy targets
|
||||
dir := "."
|
||||
absDir, _ := filepath.Abs(dir)
|
||||
cfg, _ := config.Load(absDir)
|
||||
|
||||
// If still no target, check config for named targets
|
||||
if target == "" && cfg != nil && len(cfg.Deploy) > 0 {
|
||||
// Show available targets
|
||||
return showDeployTargets(cfg)
|
||||
}
|
||||
|
||||
if target == "" {
|
||||
return showDeployHelp()
|
||||
}
|
||||
|
||||
// Check if target is a named target from config
|
||||
if cfg != nil {
|
||||
if dt, ok := cfg.Deploy[target]; ok {
|
||||
target = dt.SSH
|
||||
}
|
||||
}
|
||||
|
||||
return deploySSH(c, target, cfg)
|
||||
}
|
||||
|
||||
func deploySSH(c *cli.Context, target string) error {
|
||||
dir := c.Args().Get(0)
|
||||
func showDeployHelp() error {
|
||||
return fmt.Errorf(`No deployment target specified.
|
||||
|
||||
To deploy, you need a server running micro. Quick setup:
|
||||
|
||||
1. On your server (Ubuntu/Debian):
|
||||
ssh user@your-server
|
||||
curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
sudo micro init --server
|
||||
|
||||
2. Then deploy from here:
|
||||
micro deploy user@your-server
|
||||
|
||||
Or add to micro.mu:
|
||||
deploy prod
|
||||
ssh user@your-server
|
||||
|
||||
Run 'micro deploy --help' for more options.`)
|
||||
}
|
||||
|
||||
func showDeployTargets(cfg *config.Config) error {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Available deploy targets:\n\n")
|
||||
for name, dt := range cfg.Deploy {
|
||||
sb.WriteString(fmt.Sprintf(" %s -> %s\n", name, dt.SSH))
|
||||
}
|
||||
sb.WriteString("\nDeploy with: micro deploy <target>")
|
||||
return fmt.Errorf("%s", sb.String())
|
||||
}
|
||||
|
||||
func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
|
||||
dir := c.Args().Get(1)
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
@@ -34,205 +93,354 @@ func deploySSH(c *cli.Context, target string) error {
|
||||
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
// Load config
|
||||
cfg, err := config.Load(absDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
// Load config if not passed
|
||||
if cfg == nil {
|
||||
cfg, _ = config.Load(absDir)
|
||||
}
|
||||
|
||||
remotePath := c.String("path")
|
||||
if remotePath == "" {
|
||||
remotePath = "~/micro"
|
||||
remotePath = defaultRemotePath
|
||||
}
|
||||
|
||||
// Check if we have pre-built binaries
|
||||
binDir := filepath.Join(absDir, "bin")
|
||||
hasBinaries := false
|
||||
if _, err := os.Stat(binDir); err == nil {
|
||||
hasBinaries = true
|
||||
}
|
||||
fmt.Printf("Deploying to %s...\n\n", target)
|
||||
|
||||
fmt.Printf("Deploying to %s...\n", target)
|
||||
|
||||
// Create remote directory
|
||||
fmt.Println("Creating remote directory...")
|
||||
if err := runSSH(target, fmt.Sprintf("mkdir -p %s/bin", remotePath)); err != nil {
|
||||
// Step 1: Check SSH connectivity
|
||||
fmt.Print(" Checking SSH connection... ")
|
||||
if err := checkSSH(target); err != nil {
|
||||
fmt.Println("\u2717")
|
||||
return err
|
||||
}
|
||||
fmt.Println("\u2713")
|
||||
|
||||
if hasBinaries && !c.Bool("build") {
|
||||
// Deploy pre-built binaries
|
||||
fmt.Println("Copying binaries...")
|
||||
if err := copyBinaries(target, binDir, remotePath); err != nil {
|
||||
// Step 2: Check server is initialized
|
||||
fmt.Print(" Checking server setup... ")
|
||||
if err := checkServerInit(target, remotePath); err != nil {
|
||||
fmt.Println("\u2717")
|
||||
return err
|
||||
}
|
||||
fmt.Println("\u2713")
|
||||
|
||||
// Step 3: Build binaries
|
||||
var services []string
|
||||
if cfg != nil && len(cfg.Services) > 0 {
|
||||
sorted, err := cfg.TopologicalSort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, svc := range sorted {
|
||||
services = append(services, svc.Name)
|
||||
}
|
||||
} else {
|
||||
// Sync source and build on remote
|
||||
fmt.Println("Syncing source code...")
|
||||
if err := syncSource(target, absDir, remotePath); err != nil {
|
||||
return err
|
||||
}
|
||||
services = []string{filepath.Base(absDir)}
|
||||
}
|
||||
|
||||
fmt.Println("Building on remote...")
|
||||
if err := buildOnRemote(target, remotePath, cfg); err != nil {
|
||||
return err
|
||||
fmt.Printf(" Building binaries... ")
|
||||
if err := buildBinaries(absDir, cfg, c.Bool("build")); err != nil {
|
||||
fmt.Println("\u2717")
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\u2713 %s\n", strings.Join(services, ", "))
|
||||
|
||||
// Step 4: Copy binaries
|
||||
fmt.Printf(" Copying binaries... ")
|
||||
if err := copyBinaries(target, filepath.Join(absDir, "bin"), remotePath); err != nil {
|
||||
fmt.Println("\u2717")
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\u2713 %d services\n", len(services))
|
||||
|
||||
// Step 5: Setup and restart services via systemd
|
||||
fmt.Printf(" Updating systemd... ")
|
||||
if err := setupSystemdServices(target, remotePath, services); err != nil {
|
||||
fmt.Println("\u2717")
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\u2713 %s\n", strings.Join(prefixServices(services), ", "))
|
||||
|
||||
// Step 6: Restart services
|
||||
fmt.Printf(" Restarting services... ")
|
||||
if err := restartServices(target, services); err != nil {
|
||||
fmt.Println("\u2717")
|
||||
return err
|
||||
}
|
||||
fmt.Println("\u2713")
|
||||
|
||||
// Step 7: Check health
|
||||
fmt.Printf(" Checking health... ")
|
||||
time.Sleep(2 * time.Second) // Give services time to start
|
||||
healthy, unhealthy := checkServicesHealth(target, services)
|
||||
if len(unhealthy) > 0 {
|
||||
fmt.Printf("\u26a0 %d/%d healthy\n", len(healthy), len(services))
|
||||
} else {
|
||||
fmt.Println("\u2713 all healthy")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("\u2713 Deployed to %s\n", target)
|
||||
fmt.Println()
|
||||
fmt.Printf(" Status: micro status --remote %s\n", target)
|
||||
fmt.Printf(" Logs: micro logs --remote %s\n", target)
|
||||
|
||||
if len(unhealthy) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Printf("\u26a0 Some services may have issues: %s\n", strings.Join(unhealthy, ", "))
|
||||
fmt.Printf(" Check logs: micro logs %s --remote %s\n", unhealthy[0], target)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prefixServices(services []string) []string {
|
||||
result := make([]string, len(services))
|
||||
for i, s := range services {
|
||||
result[i] = "micro@" + s
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func checkSSH(host string) error {
|
||||
testCmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", host, "echo ok")
|
||||
output, err := testCmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(`
|
||||
\u2717 Cannot connect to %s
|
||||
|
||||
SSH connection failed. Check that:
|
||||
\u2022 The server is reachable: ping %s
|
||||
\u2022 SSH is configured: ssh %s
|
||||
\u2022 Your key is added: ssh-add -l
|
||||
|
||||
Common fixes:
|
||||
\u2022 Add SSH key: ssh-copy-id %s
|
||||
\u2022 Check hostname in ~/.ssh/config
|
||||
|
||||
Error: %s`, host, host, host, host, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkServerInit(host, remotePath string) error {
|
||||
checkCmd := fmt.Sprintf("test -f %s/.micro-initialized", remotePath)
|
||||
sshCmd := exec.Command("ssh", host, checkCmd)
|
||||
if err := sshCmd.Run(); err != nil {
|
||||
return fmt.Errorf(`
|
||||
\u2717 Server not initialized
|
||||
|
||||
micro is not set up on %s.
|
||||
|
||||
Run this on the server:
|
||||
ssh %s
|
||||
curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
sudo micro init --server
|
||||
|
||||
Or initialize remotely (requires sudo):
|
||||
micro init --server --remote %s`, host, host, host)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool) error {
|
||||
binDir := filepath.Join(absDir, "bin")
|
||||
|
||||
// Check if we already have binaries and don't need to rebuild
|
||||
if !forceBuild {
|
||||
if _, err := os.Stat(binDir); err == nil {
|
||||
// Check if binaries are for linux
|
||||
// For now, just rebuild to be safe
|
||||
}
|
||||
}
|
||||
|
||||
// Stop and start services
|
||||
fmt.Println("Restarting services...")
|
||||
if err := restartServices(target, remotePath, cfg); err != nil {
|
||||
// Always build for linux/amd64
|
||||
targetOS := "linux"
|
||||
targetArch := "amd64"
|
||||
|
||||
if err := os.MkdirAll(binDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("\n✓ Deployed to %s\n", target)
|
||||
fmt.Printf("\nView logs: ssh %s 'tail -f %s/logs/*.log'\n", target, remotePath)
|
||||
if cfg != nil && len(cfg.Services) > 0 {
|
||||
sorted, err := cfg.TopologicalSort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, svc := range sorted {
|
||||
svcDir := filepath.Join(absDir, svc.Path)
|
||||
outPath := filepath.Join(binDir, svc.Name)
|
||||
|
||||
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
|
||||
buildCmd.Dir = svcDir
|
||||
buildCmd.Env = append(os.Environ(),
|
||||
"GOOS="+targetOS,
|
||||
"GOARCH="+targetArch,
|
||||
"CGO_ENABLED=0",
|
||||
)
|
||||
|
||||
if output, err := buildCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to build %s:\n%s", svc.Name, string(output))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
name := filepath.Base(absDir)
|
||||
outPath := filepath.Join(binDir, name)
|
||||
|
||||
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
|
||||
buildCmd.Dir = absDir
|
||||
buildCmd.Env = append(os.Environ(),
|
||||
"GOOS="+targetOS,
|
||||
"GOARCH="+targetArch,
|
||||
"CGO_ENABLED=0",
|
||||
)
|
||||
|
||||
if output, err := buildCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to build:\n%s", string(output))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyBinaries(target, binDir, remotePath string) error {
|
||||
// Use scp to copy binaries
|
||||
scpArgs := []string{
|
||||
"-r",
|
||||
// Ensure remote bin directory exists
|
||||
mkdirCmd := exec.Command("ssh", target, fmt.Sprintf("mkdir -p %s/bin", remotePath))
|
||||
if err := mkdirCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create remote directory: %w", err)
|
||||
}
|
||||
|
||||
// Use rsync for efficient copy
|
||||
// --omit-dir-times avoids permission errors on directory timestamps
|
||||
rsyncArgs := []string{
|
||||
"-avz", "--delete", "--omit-dir-times",
|
||||
binDir + "/",
|
||||
fmt.Sprintf("%s:%s/bin/", target, remotePath),
|
||||
}
|
||||
scpCmd := exec.Command("scp", scpArgs...)
|
||||
scpCmd.Stdout = os.Stdout
|
||||
scpCmd.Stderr = os.Stderr
|
||||
return scpCmd.Run()
|
||||
}
|
||||
|
||||
func syncSource(target, absDir, remotePath string) error {
|
||||
rsyncArgs := []string{
|
||||
"-avz", "--delete",
|
||||
"--exclude", ".git",
|
||||
"--exclude", "bin",
|
||||
"--exclude", "node_modules",
|
||||
"--exclude", "vendor",
|
||||
absDir + "/",
|
||||
fmt.Sprintf("%s:%s/src/", target, remotePath),
|
||||
}
|
||||
rsyncCmd := exec.Command("rsync", rsyncArgs...)
|
||||
rsyncCmd.Stdout = os.Stdout
|
||||
rsyncCmd.Stderr = os.Stderr
|
||||
return rsyncCmd.Run()
|
||||
}
|
||||
|
||||
func buildOnRemote(target, remotePath string, cfg *config.Config) error {
|
||||
if cfg != nil && len(cfg.Services) > 0 {
|
||||
sorted, err := cfg.TopologicalSort()
|
||||
if err != nil {
|
||||
return err
|
||||
output, err := rsyncCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
outputStr := string(output)
|
||||
// Fall back to scp if rsync not available
|
||||
if strings.Contains(outputStr, "command not found") {
|
||||
scpCmd := exec.Command("scp", "-r", binDir+"/", fmt.Sprintf("%s:%s/bin/", target, remotePath))
|
||||
if scpOutput, scpErr := scpCmd.CombinedOutput(); scpErr != nil {
|
||||
return fmt.Errorf("copy failed: %s", string(scpOutput))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, svc := range sorted {
|
||||
srcPath := filepath.Join(remotePath, "src", svc.Path)
|
||||
binPath := filepath.Join(remotePath, "bin", svc.Name)
|
||||
|
||||
buildCmd := fmt.Sprintf("cd %s && go build -o %s .", srcPath, binPath)
|
||||
fmt.Printf(" Building %s...\n", svc.Name)
|
||||
if err := runSSH(target, buildCmd); err != nil {
|
||||
return fmt.Errorf("failed to build %s: %w", svc.Name, err)
|
||||
// rsync exit code 23 means some files failed to transfer, but if we see our files listed, it's ok
|
||||
// rsync exit code 24 means some files vanished during transfer (harmless)
|
||||
exitErr, ok := err.(*exec.ExitError)
|
||||
if ok && (exitErr.ExitCode() == 23 || exitErr.ExitCode() == 24) {
|
||||
// Check if it's just permission warnings on metadata, not actual file transfer failures
|
||||
if !strings.Contains(outputStr, "Permission denied (13)") ||
|
||||
strings.Contains(outputStr, "failed to set times") ||
|
||||
strings.Contains(outputStr, "chgrp") {
|
||||
// These are acceptable warnings
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single service
|
||||
srcPath := filepath.Join(remotePath, "src")
|
||||
binPath := filepath.Join(remotePath, "bin", "service")
|
||||
|
||||
buildCmd := fmt.Sprintf("cd %s && go build -o %s .", srcPath, binPath)
|
||||
if err := runSSH(target, buildCmd); err != nil {
|
||||
return fmt.Errorf("build failed: %w", err)
|
||||
}
|
||||
return fmt.Errorf("copy failed: %s", outputStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartServices(target, remotePath string, cfg *config.Config) error {
|
||||
// Create logs directory
|
||||
runSSH(target, fmt.Sprintf("mkdir -p %s/logs", remotePath))
|
||||
func setupSystemdServices(target, remotePath string, services []string) error {
|
||||
for _, svc := range services {
|
||||
// Enable the service using the template
|
||||
enableCmd := fmt.Sprintf("sudo systemctl enable micro@%s 2>/dev/null || true", svc)
|
||||
sshCmd := exec.Command("ssh", target, enableCmd)
|
||||
sshCmd.Run() // Ignore errors, service might already be enabled
|
||||
}
|
||||
|
||||
if cfg != nil && len(cfg.Services) > 0 {
|
||||
sorted, err := cfg.TopologicalSort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, svc := range sorted {
|
||||
binPath := filepath.Join(remotePath, "bin", svc.Name)
|
||||
logPath := filepath.Join(remotePath, "logs", svc.Name+".log")
|
||||
|
||||
// Stop existing
|
||||
stopCmd := fmt.Sprintf("pkill -f '%s' 2>/dev/null || true", binPath)
|
||||
runSSH(target, stopCmd)
|
||||
|
||||
// Start new
|
||||
startCmd := fmt.Sprintf("nohup %s >> %s 2>&1 &", binPath, logPath)
|
||||
if err := runSSH(target, startCmd); err != nil {
|
||||
return fmt.Errorf("failed to start %s: %w", svc.Name, err)
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ %s\n", svc.Name)
|
||||
}
|
||||
} else {
|
||||
binPath := filepath.Join(remotePath, "bin", "service")
|
||||
logPath := filepath.Join(remotePath, "logs", "service.log")
|
||||
|
||||
runSSH(target, fmt.Sprintf("pkill -f '%s' 2>/dev/null || true", binPath))
|
||||
|
||||
startCmd := fmt.Sprintf("nohup %s >> %s 2>&1 &", binPath, logPath)
|
||||
if err := runSSH(target, startCmd); err != nil {
|
||||
return fmt.Errorf("start failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(" ✓ service")
|
||||
// Reload systemd
|
||||
reloadCmd := exec.Command("ssh", target, "sudo systemctl daemon-reload")
|
||||
if err := reloadCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to reload systemd: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSSH(host, command string) error {
|
||||
// Expand ~ on remote
|
||||
command = strings.Replace(command, "~/", "$HOME/", -1)
|
||||
cmd := exec.Command("ssh", host, command)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
func restartServices(target string, services []string) error {
|
||||
for _, svc := range services {
|
||||
restartCmd := fmt.Sprintf("sudo systemctl restart micro@%s", svc)
|
||||
sshCmd := exec.Command("ssh", target, restartCmd)
|
||||
if output, err := sshCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to restart %s: %s", svc, string(output))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkServicesHealth(target string, services []string) (healthy, unhealthy []string) {
|
||||
for _, svc := range services {
|
||||
checkCmd := fmt.Sprintf("systemctl is-active micro@%s", svc)
|
||||
sshCmd := exec.Command("ssh", target, checkCmd)
|
||||
if err := sshCmd.Run(); err != nil {
|
||||
unhealthy = append(unhealthy, svc)
|
||||
} else {
|
||||
healthy = append(healthy, svc)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure we're not on Windows for deploy
|
||||
func checkPlatform() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return fmt.Errorf("micro deploy requires SSH and rsync, which work best on Linux/macOS.\nConsider using WSL on Windows.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.Register(&cli.Command{
|
||||
Name: "deploy",
|
||||
Usage: "Deploy services via SSH",
|
||||
Description: `Deploy copies binaries or source to a remote host and starts services.
|
||||
Usage: "Deploy services to a remote server",
|
||||
Description: `Deploy copies binaries to a remote server and manages them with systemd.
|
||||
|
||||
If ./bin/ exists (from 'micro build'), copies binaries directly.
|
||||
Otherwise, syncs source and builds on the remote host.
|
||||
Before deploying, initialize the server:
|
||||
ssh user@server 'curl -fsSL https://go-micro.dev/install.sh | sh && sudo micro init --server'
|
||||
|
||||
Examples:
|
||||
micro build --os linux # Build Linux binaries locally
|
||||
micro deploy --ssh user@host # Copy binaries and restart
|
||||
Then deploy:
|
||||
micro deploy user@server
|
||||
|
||||
micro deploy --ssh user@host # Sync source, build on remote, restart
|
||||
micro deploy --ssh user@host --build # Force rebuild on remote`,
|
||||
Action: Deploy,
|
||||
With a micro.mu config, you can define named targets:
|
||||
deploy prod
|
||||
ssh user@prod.example.com
|
||||
|
||||
deploy staging
|
||||
ssh user@staging.example.com
|
||||
|
||||
Then: micro deploy prod
|
||||
|
||||
The deploy process:
|
||||
1. Builds binaries for linux/amd64
|
||||
2. Copies to /opt/micro/bin/ via rsync
|
||||
3. Enables and restarts systemd services
|
||||
4. Verifies services are healthy`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := checkPlatform(); err != nil {
|
||||
return err
|
||||
}
|
||||
return Deploy(c)
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "ssh",
|
||||
Usage: "Deploy to user@host via SSH",
|
||||
Required: true,
|
||||
Name: "ssh",
|
||||
Usage: "Deploy target as user@host (can also be positional arg)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "Remote path (default: ~/micro)",
|
||||
Value: "~/micro",
|
||||
Usage: "Remote path (default: /opt/micro)",
|
||||
Value: "/opt/micro",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "build",
|
||||
Usage: "Force rebuild on remote (ignore local binaries)",
|
||||
Usage: "Force rebuild of binaries",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
// Package initcmd provides the micro init command for server setup
|
||||
package initcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v5/cmd"
|
||||
)
|
||||
|
||||
const systemdTemplate = `[Unit]
|
||||
Description=Micro service: %%i
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=%s
|
||||
Group=%s
|
||||
WorkingDirectory=%s
|
||||
ExecStart=%s/bin/%%i
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
EnvironmentFile=-%s/config/%%i.env
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=micro-%%i
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=false
|
||||
ReadWritePaths=%s/data
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
// Init initializes a server to receive micro deployments
|
||||
func Init(c *cli.Context) error {
|
||||
if !c.Bool("server") {
|
||||
return fmt.Errorf("usage: micro init --server\n\nInitialize this machine to receive micro deployments")
|
||||
}
|
||||
|
||||
// Check if we're on Linux
|
||||
if runtime.GOOS != "linux" {
|
||||
return fmt.Errorf("micro init --server is only supported on Linux")
|
||||
}
|
||||
|
||||
// Check for remote init
|
||||
remoteHost := c.String("remote")
|
||||
if remoteHost != "" {
|
||||
return initRemote(c, remoteHost)
|
||||
}
|
||||
|
||||
basePath := c.String("path")
|
||||
userName := c.String("user")
|
||||
|
||||
fmt.Println("Initializing micro server...")
|
||||
fmt.Println()
|
||||
|
||||
// Check if running as root (needed for systemd and creating users)
|
||||
if os.Geteuid() != 0 {
|
||||
return fmt.Errorf(`micro init --server requires root privileges.
|
||||
|
||||
Run with sudo:
|
||||
sudo micro init --server`)
|
||||
}
|
||||
|
||||
// Create user if needed
|
||||
if userName == "micro" {
|
||||
if err := createMicroUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create directories
|
||||
fmt.Println("Creating directories:")
|
||||
dirs := []string{
|
||||
filepath.Join(basePath, "bin"),
|
||||
filepath.Join(basePath, "data"),
|
||||
filepath.Join(basePath, "config"),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", dir, err)
|
||||
}
|
||||
fmt.Printf(" ✓ %s\n", dir)
|
||||
}
|
||||
|
||||
// Set ownership
|
||||
if userName != "root" {
|
||||
u, err := user.Lookup(userName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user %s not found: %w", userName, err)
|
||||
}
|
||||
|
||||
// chown -R user:user /opt/micro
|
||||
chownCmd := exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.Username, u.Username), basePath)
|
||||
if err := chownCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to set ownership: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
// Create systemd template
|
||||
fmt.Println("Creating systemd template:")
|
||||
unitContent := fmt.Sprintf(systemdTemplate, userName, userName, basePath, basePath, basePath, basePath)
|
||||
unitPath := "/etc/systemd/system/micro@.service"
|
||||
|
||||
if err := os.WriteFile(unitPath, []byte(unitContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write systemd unit: %w", err)
|
||||
}
|
||||
fmt.Printf(" ✓ %s\n", unitPath)
|
||||
|
||||
// Reload systemd
|
||||
reloadCmd := exec.Command("systemctl", "daemon-reload")
|
||||
if err := reloadCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to reload systemd: %w", err)
|
||||
}
|
||||
fmt.Println(" ✓ systemd daemon-reload")
|
||||
|
||||
// Write marker file so deploy can detect initialization
|
||||
markerPath := filepath.Join(basePath, ".micro-initialized")
|
||||
if err := os.WriteFile(markerPath, []byte("1\n"), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write marker: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Server ready!")
|
||||
fmt.Println()
|
||||
fmt.Println(" Deploy from your machine:")
|
||||
fmt.Printf(" micro deploy user@%s\n", getHostname())
|
||||
fmt.Println()
|
||||
fmt.Println(" Manage services:")
|
||||
fmt.Println(" sudo systemctl status micro@myservice")
|
||||
fmt.Println(" sudo journalctl -u micro@myservice -f")
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createMicroUser() error {
|
||||
// Check if user exists
|
||||
if _, err := user.Lookup("micro"); err == nil {
|
||||
return nil // user already exists
|
||||
}
|
||||
|
||||
fmt.Println("Creating micro user:")
|
||||
createCmd := exec.Command("useradd", "--system", "--no-create-home", "--shell", "/bin/false", "micro")
|
||||
if err := createCmd.Run(); err != nil {
|
||||
// Check if it's just because user already exists
|
||||
if _, lookupErr := user.Lookup("micro"); lookupErr == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to create micro user: %w", err)
|
||||
}
|
||||
fmt.Println(" ✓ Created user 'micro'")
|
||||
return nil
|
||||
}
|
||||
|
||||
func initRemote(c *cli.Context, host string) error {
|
||||
fmt.Printf("Initializing micro on %s...\n\n", host)
|
||||
|
||||
// Check SSH connectivity first
|
||||
if err := checkSSH(host); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
basePath := c.String("path")
|
||||
userName := c.String("user")
|
||||
|
||||
// Run micro init --server on remote
|
||||
initCmd := fmt.Sprintf("sudo micro init --server --path %s --user %s", basePath, userName)
|
||||
|
||||
sshCmd := exec.Command("ssh", host, initCmd)
|
||||
sshCmd.Stdout = os.Stdout
|
||||
sshCmd.Stderr = os.Stderr
|
||||
|
||||
if err := sshCmd.Run(); err != nil {
|
||||
return fmt.Errorf("remote init failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSSH(host string) error {
|
||||
// Quick SSH test
|
||||
testCmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", host, "echo ok")
|
||||
output, err := testCmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf(`✗ Cannot connect to %s
|
||||
|
||||
SSH connection failed. Check that:
|
||||
• The server is reachable: ping %s
|
||||
• SSH is configured: ssh %s
|
||||
• Your key is added: ssh-add -l
|
||||
|
||||
Common fixes:
|
||||
• Add SSH key: ssh-copy-id %s
|
||||
• Check hostname in ~/.ssh/config
|
||||
|
||||
Error: %s`, host, host, host, host, strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHostname() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "this-server"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.Register(&cli.Command{
|
||||
Name: "init",
|
||||
Usage: "Initialize micro for development or server deployment",
|
||||
Description: `Initialize micro on a server to receive deployments.
|
||||
|
||||
Server setup:
|
||||
sudo micro init --server
|
||||
|
||||
This creates:
|
||||
• /opt/micro/bin/ - service binaries
|
||||
• /opt/micro/data/ - persistent data
|
||||
• /opt/micro/config/ - environment files
|
||||
• systemd template for managing services
|
||||
|
||||
Remote setup:
|
||||
micro init --server --remote user@host
|
||||
|
||||
After init, deploy with:
|
||||
micro deploy user@host`,
|
||||
Action: Init,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "server",
|
||||
Usage: "Initialize as a deployment server",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "Base path for micro (default: /opt/micro)",
|
||||
Value: "/opt/micro",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user",
|
||||
Usage: "User to run services as (default: micro)",
|
||||
Value: "micro",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "remote",
|
||||
Usage: "Initialize a remote server via SSH",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
// Package remote provides remote server operations for micro
|
||||
package remote
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v5/cmd"
|
||||
)
|
||||
|
||||
const defaultRemotePath = "/opt/micro"
|
||||
|
||||
// Status shows status of services (local or remote)
|
||||
func Status(c *cli.Context) error {
|
||||
remoteHost := c.String("remote")
|
||||
if remoteHost != "" {
|
||||
return remoteStatus(remoteHost)
|
||||
}
|
||||
return localStatus(c)
|
||||
}
|
||||
|
||||
func localStatus(c *cli.Context) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home dir: %w", err)
|
||||
}
|
||||
runDir := filepath.Join(homeDir, "micro", "run")
|
||||
files, err := os.ReadDir(runDir)
|
||||
if err != nil {
|
||||
fmt.Println("No services running locally.")
|
||||
fmt.Println("\nStart services with: micro run")
|
||||
return nil
|
||||
}
|
||||
|
||||
var hasServices bool
|
||||
fmt.Printf("%-20s %-10s %-8s %s\n", "SERVICE", "STATUS", "PID", "DIRECTORY")
|
||||
fmt.Println(strings.Repeat("-", 70))
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") {
|
||||
continue
|
||||
}
|
||||
hasServices = true
|
||||
service := f.Name()[:len(f.Name())-4]
|
||||
pidFilePath := filepath.Join(runDir, f.Name())
|
||||
pidFile, err := os.Open(pidFilePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var pid int
|
||||
var dir string
|
||||
scanner := bufio.NewScanner(pidFile)
|
||||
if scanner.Scan() {
|
||||
fmt.Sscanf(scanner.Text(), "%d", &pid)
|
||||
}
|
||||
if scanner.Scan() {
|
||||
dir = scanner.Text()
|
||||
}
|
||||
pidFile.Close()
|
||||
|
||||
status := "\u2717 stopped"
|
||||
if pid > 0 {
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err == nil {
|
||||
if err := proc.Signal(syscall.Signal(0)); err == nil {
|
||||
status = "\u25cf running"
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("%-20s %-10s %-8d %s\n", service, status, pid, dir)
|
||||
}
|
||||
|
||||
if !hasServices {
|
||||
fmt.Println("No services running locally.")
|
||||
fmt.Println("\nStart services with: micro run")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func remoteStatus(host string) error {
|
||||
// Get list of micro services via systemctl
|
||||
listCmd := exec.Command("ssh", host, "systemctl list-units 'micro@*' --no-legend --no-pager 2>/dev/null || true")
|
||||
output, err := listCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get status from %s: %w", host, err)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
|
||||
fmt.Printf("%s\n", host)
|
||||
fmt.Println(strings.Repeat("\u2501", 50))
|
||||
fmt.Println("\nNo services deployed.")
|
||||
fmt.Println("\nDeploy with: micro deploy " + host)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", host)
|
||||
fmt.Println(strings.Repeat("\u2501", 50))
|
||||
fmt.Println()
|
||||
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
unit := parts[0]
|
||||
loadState := parts[1]
|
||||
activeState := parts[2]
|
||||
subState := parts[3]
|
||||
|
||||
// Extract service name from micro@servicename.service
|
||||
serviceName := strings.TrimPrefix(unit, "micro@")
|
||||
serviceName = strings.TrimSuffix(serviceName, ".service")
|
||||
|
||||
// Get more details
|
||||
statusIcon := "\u25cf"
|
||||
statusText := subState
|
||||
if activeState != "active" || subState != "running" {
|
||||
statusIcon = "\u2717"
|
||||
}
|
||||
|
||||
_ = loadState // unused but parsed
|
||||
|
||||
fmt.Printf(" %-15s %s %s\n", serviceName, statusIcon, statusText)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logs shows logs for services (local or remote)
|
||||
func Logs(c *cli.Context) error {
|
||||
remoteHost := c.String("remote")
|
||||
service := c.Args().First()
|
||||
follow := c.Bool("follow") || c.Bool("f")
|
||||
lines := c.Int("lines")
|
||||
|
||||
if remoteHost != "" {
|
||||
return remoteLogs(remoteHost, service, follow, lines)
|
||||
}
|
||||
return localLogs(c, service, follow, lines)
|
||||
}
|
||||
|
||||
func localLogs(c *cli.Context, service string, follow bool, lines int) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home dir: %w", err)
|
||||
}
|
||||
logDir := filepath.Join(homeDir, "micro", "logs")
|
||||
|
||||
if service == "" {
|
||||
// List available logs
|
||||
files, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
fmt.Println("No logs available.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Available logs:")
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f.Name(), ".log") {
|
||||
name := strings.TrimSuffix(f.Name(), ".log")
|
||||
fmt.Printf(" %s\n", name)
|
||||
}
|
||||
}
|
||||
fmt.Println("\nView logs: micro logs <service>")
|
||||
return nil
|
||||
}
|
||||
|
||||
logPath := filepath.Join(logDir, service+".log")
|
||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("no logs for service '%s'", service)
|
||||
}
|
||||
|
||||
if follow {
|
||||
cmd := exec.Command("tail", "-f", logPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
if lines == 0 {
|
||||
lines = 100
|
||||
}
|
||||
cmd := exec.Command("tail", "-n", fmt.Sprintf("%d", lines), logPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func remoteLogs(host, service string, follow bool, lines int) error {
|
||||
var journalCmd string
|
||||
|
||||
if service == "" {
|
||||
// All micro services
|
||||
journalCmd = "journalctl -u 'micro@*'"
|
||||
} else {
|
||||
journalCmd = fmt.Sprintf("journalctl -u 'micro@%s'", service)
|
||||
}
|
||||
|
||||
if follow {
|
||||
journalCmd += " -f"
|
||||
} else {
|
||||
if lines == 0 {
|
||||
lines = 100
|
||||
}
|
||||
journalCmd += fmt.Sprintf(" -n %d", lines)
|
||||
}
|
||||
|
||||
journalCmd += " --no-pager"
|
||||
|
||||
sshCmd := exec.Command("ssh", host, journalCmd)
|
||||
sshCmd.Stdout = os.Stdout
|
||||
sshCmd.Stderr = os.Stderr
|
||||
return sshCmd.Run()
|
||||
}
|
||||
|
||||
// Stop stops a running service
|
||||
func Stop(c *cli.Context) error {
|
||||
if c.Args().Len() != 1 {
|
||||
return fmt.Errorf("Usage: micro stop <service>")
|
||||
}
|
||||
|
||||
service := c.Args().First()
|
||||
remoteHost := c.String("remote")
|
||||
|
||||
if remoteHost != "" {
|
||||
return remoteStop(remoteHost, service)
|
||||
}
|
||||
return localStop(service)
|
||||
}
|
||||
|
||||
func localStop(service string) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home dir: %w", err)
|
||||
}
|
||||
|
||||
runDir := filepath.Join(homeDir, "micro", "run")
|
||||
pidFilePath := filepath.Join(runDir, service+".pid")
|
||||
|
||||
pidFile, err := os.Open(pidFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service '%s' is not running", service)
|
||||
}
|
||||
|
||||
var pid int
|
||||
scanner := bufio.NewScanner(pidFile)
|
||||
if scanner.Scan() {
|
||||
fmt.Sscanf(scanner.Text(), "%d", &pid)
|
||||
}
|
||||
pidFile.Close()
|
||||
|
||||
if pid <= 0 {
|
||||
_ = os.Remove(pidFilePath)
|
||||
return fmt.Errorf("service '%s' is not running", service)
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
_ = os.Remove(pidFilePath)
|
||||
return fmt.Errorf("could not find process for '%s'", service)
|
||||
}
|
||||
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
_ = os.Remove(pidFilePath)
|
||||
return fmt.Errorf("failed to stop service '%s': %v", service, err)
|
||||
}
|
||||
|
||||
_ = os.Remove(pidFilePath)
|
||||
fmt.Printf("Stopped %s (pid %d)\n", service, pid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func remoteStop(host, service string) error {
|
||||
stopCmd := fmt.Sprintf("sudo systemctl stop micro@%s", service)
|
||||
sshCmd := exec.Command("ssh", host, stopCmd)
|
||||
if output, err := sshCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to stop %s: %s", service, string(output))
|
||||
}
|
||||
fmt.Printf("Stopped %s on %s\n", service, host)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.Register(&cli.Command{
|
||||
Name: "status",
|
||||
Usage: "Check status of running services",
|
||||
Description: `Show status of running services.
|
||||
|
||||
Local status:
|
||||
micro status
|
||||
|
||||
Remote status:
|
||||
micro status --remote user@host`,
|
||||
Action: Status,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "remote",
|
||||
Usage: "Check status on remote server",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cmd.Register(&cli.Command{
|
||||
Name: "logs",
|
||||
Usage: "Show logs for a service",
|
||||
Description: `View service logs.
|
||||
|
||||
Local logs:
|
||||
micro logs # list available logs
|
||||
micro logs myservice # show logs for myservice
|
||||
micro logs myservice -f # follow logs
|
||||
|
||||
Remote logs:
|
||||
micro logs --remote user@host
|
||||
micro logs myservice --remote user@host -f`,
|
||||
Action: Logs,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "remote",
|
||||
Usage: "View logs on remote server",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "follow",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Follow log output",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "lines",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Number of lines to show (default: 100)",
|
||||
Value: 100,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cmd.Register(&cli.Command{
|
||||
Name: "stop",
|
||||
Usage: "Stop a running service",
|
||||
Description: `Stop a running service.
|
||||
|
||||
Local:
|
||||
micro stop myservice
|
||||
|
||||
Remote:
|
||||
micro stop myservice --remote user@host`,
|
||||
Action: Stop,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "remote",
|
||||
Usage: "Stop service on remote server",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -15,6 +15,14 @@ import (
|
||||
type Config struct {
|
||||
Services map[string]*Service `json:"services"`
|
||||
Envs map[string]map[string]string `json:"env"`
|
||||
Deploy map[string]*DeployTarget `json:"deploy"`
|
||||
}
|
||||
|
||||
// DeployTarget represents a deployment target configuration
|
||||
type DeployTarget struct {
|
||||
Name string `json:"-"`
|
||||
SSH string `json:"ssh"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// Service represents a service configuration
|
||||
@@ -87,11 +95,13 @@ func ParseMu(path string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
Services: make(map[string]*Service),
|
||||
Envs: make(map[string]map[string]string),
|
||||
Deploy: make(map[string]*DeployTarget),
|
||||
}
|
||||
|
||||
var currentService *Service
|
||||
var currentEnv string
|
||||
var currentEnvMap map[string]string
|
||||
var currentDeploy *DeployTarget
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
lineNum := 0
|
||||
@@ -137,9 +147,21 @@ func ParseMu(path string) (*Config, error) {
|
||||
cfg.Envs[currentEnv] = currentEnvMap
|
||||
}
|
||||
currentService = nil
|
||||
currentDeploy = nil
|
||||
currentEnv = name
|
||||
currentEnvMap = make(map[string]string)
|
||||
|
||||
case "deploy":
|
||||
// Save previous env if any
|
||||
if currentEnv != "" && currentEnvMap != nil {
|
||||
cfg.Envs[currentEnv] = currentEnvMap
|
||||
}
|
||||
currentService = nil
|
||||
currentEnv = ""
|
||||
currentEnvMap = nil
|
||||
currentDeploy = &DeployTarget{Name: name}
|
||||
cfg.Deploy[name] = currentDeploy
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%s:%d: unknown keyword '%s'", path, lineNum, keyword)
|
||||
}
|
||||
@@ -168,11 +190,20 @@ func ParseMu(path string) (*Config, error) {
|
||||
default:
|
||||
return nil, fmt.Errorf("%s:%d: unknown service property '%s'", path, lineNum, key)
|
||||
}
|
||||
} else if currentDeploy != nil {
|
||||
switch key {
|
||||
case "ssh":
|
||||
currentDeploy.SSH = value
|
||||
case "path":
|
||||
currentDeploy.Path = value
|
||||
default:
|
||||
return nil, fmt.Errorf("%s:%d: unknown deploy property '%s'", path, lineNum, key)
|
||||
}
|
||||
} else if currentEnvMap != nil {
|
||||
// Environment variable
|
||||
currentEnvMap[key] = value
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s:%d: property outside of service or env block", path, lineNum)
|
||||
return nil, fmt.Errorf("%s:%d: property outside of service, deploy, or env block", path, lineNum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
# Deploying Go Micro Services
|
||||
|
||||
This guide covers deploying go-micro services to a Linux server using systemd.
|
||||
|
||||
## Overview
|
||||
|
||||
go-micro provides a simple deployment workflow:
|
||||
|
||||
1. **Develop locally** with `micro run`
|
||||
2. **Build binaries** with `micro build`
|
||||
3. **Deploy to server** with `micro deploy`
|
||||
|
||||
On the server, services are managed by systemd - the standard Linux process supervisor.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Prepare Your Server
|
||||
|
||||
On your server (Ubuntu, Debian, or any systemd-based Linux):
|
||||
|
||||
```bash
|
||||
# Install micro
|
||||
curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
|
||||
# Initialize for deployment
|
||||
sudo micro init --server
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `/opt/micro/bin/` - where service binaries live
|
||||
- `/opt/micro/data/` - persistent data directory
|
||||
- `/opt/micro/config/` - environment files
|
||||
- systemd template for managing services
|
||||
|
||||
### 2. Deploy from Your Machine
|
||||
|
||||
```bash
|
||||
# From your project directory
|
||||
micro deploy user@your-server
|
||||
```
|
||||
|
||||
That's it! The deploy command:
|
||||
1. Builds your services for Linux
|
||||
2. Copies binaries to the server
|
||||
3. Configures and starts systemd services
|
||||
4. Verifies everything is running
|
||||
|
||||
## Detailed Setup
|
||||
|
||||
### Server Requirements
|
||||
|
||||
- Linux with systemd (Ubuntu 16.04+, Debian 8+, CentOS 7+, etc.)
|
||||
- SSH access
|
||||
- Go installed (only if building on server)
|
||||
|
||||
### Server Initialization Options
|
||||
|
||||
```bash
|
||||
# Basic setup (creates 'micro' user)
|
||||
sudo micro init --server
|
||||
|
||||
# Custom installation path
|
||||
sudo micro init --server --path /home/deploy/micro
|
||||
|
||||
# Run services as existing user
|
||||
sudo micro init --server --user deploy
|
||||
|
||||
# Initialize remotely (from your laptop)
|
||||
micro init --server --remote user@your-server
|
||||
```
|
||||
|
||||
### What Gets Created
|
||||
|
||||
**Directories:**
|
||||
```
|
||||
/opt/micro/
|
||||
├── bin/ # Service binaries
|
||||
├── data/ # Persistent data (databases, files)
|
||||
└── config/ # Environment files (*.env)
|
||||
```
|
||||
|
||||
**Systemd Template** (`/etc/systemd/system/micro@.service`):
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Micro service: %i
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=micro
|
||||
WorkingDirectory=/opt/micro
|
||||
ExecStart=/opt/micro/bin/%i
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
EnvironmentFile=-/opt/micro/config/%i.env
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
The `%i` is replaced with the service name. So `micro@users.service` runs `/opt/micro/bin/users`.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Basic Deploy
|
||||
|
||||
```bash
|
||||
micro deploy user@server
|
||||
```
|
||||
|
||||
### Deploy Specific Service
|
||||
|
||||
```bash
|
||||
micro deploy user@server --service users
|
||||
```
|
||||
|
||||
### Force Rebuild
|
||||
|
||||
```bash
|
||||
micro deploy user@server --build
|
||||
```
|
||||
|
||||
### Named Deploy Targets
|
||||
|
||||
Add to your `micro.mu`:
|
||||
|
||||
```
|
||||
service users
|
||||
path ./users
|
||||
port 8081
|
||||
|
||||
service web
|
||||
path ./web
|
||||
port 8080
|
||||
|
||||
deploy prod
|
||||
ssh deploy@prod.example.com
|
||||
|
||||
deploy staging
|
||||
ssh deploy@staging.example.com
|
||||
```
|
||||
|
||||
Then:
|
||||
```bash
|
||||
micro deploy prod # deploys to prod.example.com
|
||||
micro deploy staging # deploys to staging.example.com
|
||||
```
|
||||
|
||||
## Managing Services
|
||||
|
||||
### Check Status
|
||||
|
||||
```bash
|
||||
# Local services
|
||||
micro status
|
||||
|
||||
# Remote services
|
||||
micro status --remote user@server
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
server.example.com
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
users ● running pid 1234
|
||||
posts ● running pid 1235
|
||||
web ● running pid 1236
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
micro logs --remote user@server
|
||||
|
||||
# Specific service
|
||||
micro logs users --remote user@server
|
||||
|
||||
# Follow logs
|
||||
micro logs users --remote user@server -f
|
||||
```
|
||||
|
||||
### Stop Services
|
||||
|
||||
```bash
|
||||
micro stop users --remote user@server
|
||||
```
|
||||
|
||||
### Direct systemctl Access
|
||||
|
||||
You can also manage services directly on the server:
|
||||
|
||||
```bash
|
||||
# Status
|
||||
sudo systemctl status micro@users
|
||||
|
||||
# Restart
|
||||
sudo systemctl restart micro@users
|
||||
|
||||
# Stop
|
||||
sudo systemctl stop micro@users
|
||||
|
||||
# Logs
|
||||
journalctl -u micro@users -f
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create environment files at `/opt/micro/config/<service>.env`:
|
||||
|
||||
```bash
|
||||
# /opt/micro/config/users.env
|
||||
DATABASE_URL=postgres://localhost/users
|
||||
REDIS_URL=redis://localhost:6379
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
These are automatically loaded by systemd when the service starts.
|
||||
|
||||
## SSH Setup
|
||||
|
||||
### Key-Based Authentication
|
||||
|
||||
```bash
|
||||
# Generate key (if you don't have one)
|
||||
ssh-keygen -t ed25519
|
||||
|
||||
# Copy to server
|
||||
ssh-copy-id user@server
|
||||
```
|
||||
|
||||
### SSH Config
|
||||
|
||||
Add to `~/.ssh/config`:
|
||||
|
||||
```
|
||||
Host prod
|
||||
HostName prod.example.com
|
||||
User deploy
|
||||
IdentityFile ~/.ssh/deploy_key
|
||||
|
||||
Host staging
|
||||
HostName staging.example.com
|
||||
User deploy
|
||||
IdentityFile ~/.ssh/deploy_key
|
||||
```
|
||||
|
||||
Then deploy with:
|
||||
```bash
|
||||
micro deploy prod
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot connect to server"
|
||||
|
||||
```
|
||||
✗ Cannot connect to myserver
|
||||
|
||||
SSH connection failed. Check that:
|
||||
• The server is reachable: ping myserver
|
||||
• SSH is configured: ssh user@myserver
|
||||
• Your key is added: ssh-add -l
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Test SSH connection
|
||||
ssh user@server
|
||||
|
||||
# Add SSH key
|
||||
ssh-copy-id user@server
|
||||
|
||||
# Check SSH agent
|
||||
eval $(ssh-agent)
|
||||
ssh-add
|
||||
```
|
||||
|
||||
### "Server not initialized"
|
||||
|
||||
```
|
||||
✗ Server not initialized
|
||||
|
||||
micro is not set up on myserver.
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
ssh user@server 'sudo micro init --server'
|
||||
```
|
||||
|
||||
### "Service failed to start"
|
||||
|
||||
Check the logs:
|
||||
```bash
|
||||
micro logs myservice --remote user@server
|
||||
|
||||
# Or on the server:
|
||||
journalctl -u micro@myservice -n 50
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- Missing environment variables
|
||||
- Port already in use
|
||||
- Database not reachable
|
||||
- Binary permissions issue
|
||||
|
||||
### "Permission denied"
|
||||
|
||||
Ensure your user can write to `/opt/micro/bin/`:
|
||||
|
||||
```bash
|
||||
# On server
|
||||
sudo chown -R deploy:deploy /opt/micro
|
||||
|
||||
# Or add user to micro group
|
||||
sudo usermod -aG micro deploy
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Use a dedicated deploy user** - Don't deploy as root
|
||||
2. **Use SSH keys** - Disable password authentication
|
||||
3. **Restrict sudo** - Only allow necessary commands
|
||||
4. **Firewall** - Only expose needed ports
|
||||
5. **Secrets** - Use environment files with restricted permissions (0600)
|
||||
|
||||
### Minimal sudo access
|
||||
|
||||
Add to `/etc/sudoers.d/micro`:
|
||||
```
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl enable micro@*
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart micro@*
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl stop micro@*
|
||||
deploy ALL=(ALL) NOPASSWD: /bin/systemctl status micro@*
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [micro run](./micro-run.md) - Local development
|
||||
- [micro.mu configuration](./config.md) - Configuration file format
|
||||
- [Health checks](./health.md) - Service health endpoints
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# Install script for micro CLI
|
||||
# Usage: curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
|
||||
set -e
|
||||
|
||||
VERSION="${MICRO_VERSION:-latest}"
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
# Normalize architecture
|
||||
case $ARCH in
|
||||
x86_64|amd64) ARCH="amd64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
armv7l) ARCH="arm" ;;
|
||||
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Normalize OS
|
||||
case $OS in
|
||||
darwin) OS="darwin" ;;
|
||||
linux) OS="linux" ;;
|
||||
*) echo "Unsupported OS: $OS"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Determine install directory
|
||||
if [ "$EUID" -eq 0 ] || [ "$(id -u)" -eq 0 ]; then
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
else
|
||||
INSTALL_DIR="$HOME/.local/bin"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
echo "Installing micro ${VERSION} for ${OS}/${ARCH}..."
|
||||
|
||||
# Download URL
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
URL="https://github.com/micro/go-micro/releases/latest/download/micro-${OS}-${ARCH}"
|
||||
else
|
||||
URL="https://github.com/micro/go-micro/releases/download/${VERSION}/micro-${OS}-${ARCH}"
|
||||
fi
|
||||
|
||||
# Download
|
||||
TMP_FILE=$(mktemp)
|
||||
if command -v curl &> /dev/null; then
|
||||
curl -fsSL "$URL" -o "$TMP_FILE"
|
||||
elif command -v wget &> /dev/null; then
|
||||
wget -q "$URL" -O "$TMP_FILE"
|
||||
else
|
||||
echo "Error: curl or wget required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install
|
||||
chmod +x "$TMP_FILE"
|
||||
mv "$TMP_FILE" "$INSTALL_DIR/micro"
|
||||
|
||||
echo ""
|
||||
echo "✓ Installed micro to $INSTALL_DIR/micro"
|
||||
echo ""
|
||||
|
||||
# Verify
|
||||
if command -v micro &> /dev/null; then
|
||||
micro --version
|
||||
else
|
||||
echo "Note: Add $INSTALL_DIR to your PATH:"
|
||||
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Get started:"
|
||||
echo " micro new myservice # Create a new service"
|
||||
echo " micro run # Run locally"
|
||||
echo " micro deploy # Deploy to server"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user