Compare commits

...

1 Commits

Author SHA1 Message Date
Shelley f974272551 fix: make build/deploy Go-native, Docker optional
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
micro build:
  - Default: builds Go binaries to ./bin/
  - Cross-compile with --os and --arch
  - Docker is optional via --docker flag

micro deploy:
  - Requires --ssh user@host
  - Copies pre-built binaries (if ./bin/ exists)
  - Or syncs source and builds on remote
  - No Docker dependency

Go binaries are self-contained. No runtime needed.

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 12:46:44 +00:00
4 changed files with 364 additions and 303 deletions
+5 -4
View File
@@ -178,12 +178,13 @@ The gateway runs on :8080 by default, so services should use other ports.
### Deployment
```bash
micro build # Build container images
micro build --compose # Generate docker-compose.yml
micro deploy # Deploy with docker-compose
micro deploy --ssh user@host # Deploy via SSH
micro build # Build Go binaries to ./bin/
micro build --os linux # Cross-compile for Linux
micro deploy --ssh user@host # Deploy via SSH
```
No Docker required. Go binaries are self-contained.
See [cmd/micro/README.md](cmd/micro/README.md) for full CLI documentation.
Docs: [`internal/website/docs`](internal/website/docs)
+159 -67
View File
@@ -1,4 +1,4 @@
// Package build provides the micro build command for building container images
// Package build provides the micro build command for building service binaries
package build
import (
@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/urfave/cli/v2"
@@ -13,24 +14,7 @@ import (
"go-micro.dev/v5/cmd/micro/run/config"
)
const dockerfileTemplate = `# Auto-generated by micro build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /service %s
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /service /app/service
EXPOSE %d
CMD ["/app/service"]
`
// Build builds container images for services
// Build builds Go binaries for services
func Build(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
@@ -48,26 +32,112 @@ func Build(c *cli.Context) error {
return fmt.Errorf("failed to load config: %w", err)
}
tag := c.String("tag")
if tag == "" {
tag = "latest"
// Output directory
outDir := c.String("output")
if outDir == "" {
outDir = filepath.Join(absDir, "bin")
}
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("failed to create output dir: %w", err)
}
registry := c.String("registry")
push := c.Bool("push")
// Target OS/ARCH
targetOS := c.String("os")
targetArch := c.String("arch")
if targetOS == "" {
targetOS = runtime.GOOS
}
if targetArch == "" {
targetArch = runtime.GOARCH
}
if cfg != nil && len(cfg.Services) > 0 {
// Build each service from config
for name, svc := range cfg.Services {
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
svcDir := filepath.Join(absDir, svc.Path)
if err := buildService(name, svcDir, svc.Port, tag, registry, push); err != nil {
return fmt.Errorf("failed to build %s: %w", name, err)
if err := buildService(svc.Name, svcDir, outDir, targetOS, targetArch); err != nil {
return fmt.Errorf("failed to build %s: %w", svc.Name, err)
}
}
} else {
// Build single service from current directory
name := filepath.Base(absDir)
if err := buildService(name, absDir, 8080, tag, registry, push); err != nil {
if err := buildService(name, absDir, outDir, targetOS, targetArch); err != nil {
return err
}
}
fmt.Printf("\n✓ Built to %s\n", outDir)
return nil
}
func buildService(name, dir, outDir, targetOS, targetArch string) error {
binName := name
if targetOS == "windows" {
binName += ".exe"
}
outPath := filepath.Join(outDir, binName)
fmt.Printf("Building %s (%s/%s)...\n", name, targetOS, targetArch)
// Build command
buildCmd := exec.Command("go", "build", "-o", outPath, ".")
buildCmd.Dir = dir
buildCmd.Env = append(os.Environ(),
"GOOS="+targetOS,
"GOARCH="+targetArch,
"CGO_ENABLED=0",
)
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
return fmt.Errorf("go build failed: %w", err)
}
fmt.Printf("✓ %s\n", outPath)
return nil
}
// Docker builds container images (optional)
func Docker(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
cfg, err := config.Load(absDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
tag := c.String("tag")
if tag == "" {
tag = "latest"
}
registry := c.String("registry")
push := c.Bool("push")
if cfg != nil && len(cfg.Services) > 0 {
for name, svc := range cfg.Services {
svcDir := filepath.Join(absDir, svc.Path)
if err := buildDockerImage(name, svcDir, svc.Port, tag, registry, push); err != nil {
return fmt.Errorf("failed to build %s: %w", name, err)
}
}
} else {
name := filepath.Base(absDir)
if err := buildDockerImage(name, absDir, 8080, tag, registry, push); err != nil {
return err
}
}
@@ -75,7 +145,21 @@ func Build(c *cli.Context) error {
return nil
}
func buildService(name, dir string, port int, tag, registry string, push bool) error {
const dockerfileTemplate = `FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /service .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /service /service
EXPOSE %d
CMD ["/service"]
`
func buildDockerImage(name, dir string, port int, tag, registry string, push bool) error {
if port == 0 {
port = 8080
}
@@ -84,23 +168,12 @@ func buildService(name, dir string, port int, tag, registry string, push bool) e
dockerfilePath := filepath.Join(dir, "Dockerfile")
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
fmt.Printf("Generating Dockerfile for %s...\n", name)
// Find the main package path
mainPath := "."
if _, err := os.Stat(filepath.Join(dir, "main.go")); os.IsNotExist(err) {
// Look for cmd/main.go or similar
if _, err := os.Stat(filepath.Join(dir, "cmd", "main.go")); err == nil {
mainPath = "./cmd"
}
}
dockerfile := fmt.Sprintf(dockerfileTemplate, mainPath, port)
dockerfile := fmt.Sprintf(dockerfileTemplate, port)
if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil {
return fmt.Errorf("failed to write Dockerfile: %w", err)
}
}
// Build image name
imageName := name + ":" + tag
if registry != "" {
imageName = registry + "/" + imageName
@@ -108,7 +181,6 @@ func buildService(name, dir string, port int, tag, registry string, push bool) e
fmt.Printf("Building %s...\n", imageName)
// Run docker build
buildCmd := exec.Command("docker", "build", "-t", imageName, dir)
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
@@ -118,7 +190,6 @@ func buildService(name, dir string, port int, tag, registry string, push bool) e
fmt.Printf("✓ Built %s\n", imageName)
// Push if requested
if push {
fmt.Printf("Pushing %s...\n", imageName)
pushCmd := exec.Command("docker", "push", imageName)
@@ -133,8 +204,8 @@ func buildService(name, dir string, port int, tag, registry string, push bool) e
return nil
}
// GenerateDockerCompose generates a docker-compose.yml from micro.mu config
func GenerateDockerCompose(c *cli.Context) error {
// Compose generates docker-compose.yml (optional)
func Compose(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
@@ -160,13 +231,10 @@ func GenerateDockerCompose(c *cli.Context) error {
tag = "latest"
}
// Generate docker-compose.yml
var sb strings.Builder
sb.WriteString("# Auto-generated by micro build --compose\n")
sb.WriteString("version: '3.8'\n\n")
sb.WriteString("services:\n")
sb.WriteString("# Generated by micro build --compose\n")
sb.WriteString("version: '3.8'\n\nservices:\n")
// Sort by dependencies
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
@@ -180,10 +248,9 @@ func GenerateDockerCompose(c *cli.Context) error {
sb.WriteString(fmt.Sprintf(" %s:\n", svc.Name))
sb.WriteString(fmt.Sprintf(" image: %s\n", imageName))
if svc.Port > 0 {
sb.WriteString(fmt.Sprintf(" ports:\n"))
sb.WriteString(fmt.Sprintf(" - \"%d:%d\"\n", svc.Port, svc.Port))
sb.WriteString(fmt.Sprintf(" ports:\n - \"%d:%d\"\n", svc.Port, svc.Port))
}
if len(svc.Depends) > 0 {
@@ -193,9 +260,7 @@ func GenerateDockerCompose(c *cli.Context) error {
}
}
sb.WriteString(" environment:\n")
sb.WriteString(" - MICRO_REGISTRY=mdns\n")
sb.WriteString("\n")
sb.WriteString(" environment:\n - MICRO_REGISTRY=mdns\n\n")
}
output := filepath.Join(absDir, "docker-compose.yml")
@@ -210,41 +275,68 @@ func GenerateDockerCompose(c *cli.Context) error {
func init() {
cmd.Register(&cli.Command{
Name: "build",
Usage: "Build container images for services",
Description: `Build creates Docker container images for your services.
Usage: "Build Go binaries for services",
Description: `Build compiles Go binaries for your services.
With a micro.mu config, builds all services. Without, builds the current directory.
Output goes to ./bin/ by default.
Examples:
micro build # Build all services
micro build --tag v1.0.0 # Build with specific tag
micro build --push # Build and push to registry
micro build --compose # Generate docker-compose.yml`,
micro build # Build for current OS/arch
micro build --os linux # Cross-compile for Linux
micro build --os linux --arch arm64 # For ARM64
micro build --output ./dist # Custom output directory
Docker (optional):
micro build --docker # Build container images
micro build --docker --push # Build and push
micro build --compose # Generate docker-compose.yml`,
Action: func(c *cli.Context) error {
if c.Bool("docker") {
return Docker(c)
}
if c.Bool("compose") {
return GenerateDockerCompose(c)
return Compose(c)
}
return Build(c)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output directory (default: ./bin)",
},
&cli.StringFlag{
Name: "os",
Usage: "Target OS (linux, darwin, windows)",
},
&cli.StringFlag{
Name: "arch",
Usage: "Target architecture (amd64, arm64)",
},
// Docker options (optional)
&cli.BoolFlag{
Name: "docker",
Usage: "Build Docker container images instead",
},
&cli.StringFlag{
Name: "tag",
Aliases: []string{"t"},
Usage: "Image tag (default: latest)",
Usage: "Docker image tag (default: latest)",
Value: "latest",
},
&cli.StringFlag{
Name: "registry",
Aliases: []string{"r"},
Usage: "Container registry (e.g., docker.io/myuser)",
Usage: "Docker registry (e.g., docker.io/myuser)",
},
&cli.BoolFlag{
Name: "push",
Usage: "Push images after building",
Usage: "Push Docker images after building",
},
&cli.BoolFlag{
Name: "compose",
Usage: "Generate docker-compose.yml instead of building",
Usage: "Generate docker-compose.yml",
},
},
})
+125 -100
View File
@@ -16,51 +16,11 @@ import (
// Deploy deploys services to a target
func Deploy(c *cli.Context) error {
sshTarget := c.String("ssh")
if sshTarget != "" {
return deploySSH(c, sshTarget)
if sshTarget == "" {
return fmt.Errorf("specify target with --ssh user@host")
}
// Default: docker-compose up
return deployCompose(c)
}
func deployCompose(c *cli.Context) error {
dir := c.Args().Get(0)
if dir == "" {
dir = "."
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
composePath := filepath.Join(absDir, "docker-compose.yml")
if _, err := os.Stat(composePath); os.IsNotExist(err) {
return fmt.Errorf("docker-compose.yml not found. Run 'micro build --compose' first")
}
fmt.Println("Deploying with docker-compose...")
args := []string{"compose", "-f", composePath, "up", "-d"}
if c.Bool("build") {
args = append(args, "--build")
}
cmd := exec.Command("docker", args...)
cmd.Dir = absDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("docker compose failed: %w", err)
}
fmt.Println("\n✓ Deployed successfully")
fmt.Println("\nView logs: docker compose logs -f")
fmt.Println("Stop: docker compose down")
return nil
return deploySSH(c, sshTarget)
}
func deploySSH(c *cli.Context, target string) error {
@@ -74,7 +34,7 @@ func deploySSH(c *cli.Context, target string) error {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Load config to get service info
// Load config
cfg, err := config.Load(absDir)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
@@ -85,99 +45,158 @@ func deploySSH(c *cli.Context, target string) error {
remotePath = "~/micro"
}
fmt.Printf("Deploying to %s...\n", target)
// Parse target: user@host or just host
var sshHost string
if strings.Contains(target, "@") {
sshHost = target
} else {
sshHost = target
// 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", target)
// Create remote directory
fmt.Println("Creating remote directory...")
if err := runSSH(sshHost, fmt.Sprintf("mkdir -p %s", remotePath)); err != nil {
if err := runSSH(target, fmt.Sprintf("mkdir -p %s/bin", remotePath)); err != nil {
return err
}
// Sync files using rsync
fmt.Println("Syncing files...")
if hasBinaries && !c.Bool("build") {
// Deploy pre-built binaries
fmt.Println("Copying binaries...")
if err := copyBinaries(target, binDir, remotePath); err != nil {
return err
}
} else {
// Sync source and build on remote
fmt.Println("Syncing source code...")
if err := syncSource(target, absDir, remotePath); err != nil {
return err
}
fmt.Println("Building on remote...")
if err := buildOnRemote(target, remotePath, cfg); err != nil {
return err
}
}
// Stop and start services
fmt.Println("Restarting services...")
if err := restartServices(target, remotePath, cfg); 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)
return nil
}
func copyBinaries(target, binDir, remotePath string) error {
// Use scp to copy binaries
scpArgs := []string{
"-r",
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/", sshHost, remotePath),
fmt.Sprintf("%s:%s/src/", target, remotePath),
}
rsyncCmd := exec.Command("rsync", rsyncArgs...)
rsyncCmd.Stdout = os.Stdout
rsyncCmd.Stderr = os.Stderr
if err := rsyncCmd.Run(); err != nil {
return fmt.Errorf("rsync failed: %w", err)
}
// Build and run on remote
fmt.Println("Building on remote...")
return rsyncCmd.Run()
}
func buildOnRemote(target, remotePath string, cfg *config.Config) error {
if cfg != nil && len(cfg.Services) > 0 {
// Build and run each service
sorted, err := cfg.TopologicalSort()
if err != nil {
return err
}
for _, svc := range sorted {
svcPath := filepath.Join(remotePath, svc.Path)
srcPath := filepath.Join(remotePath, "src", svc.Path)
binPath := filepath.Join(remotePath, "bin", svc.Name)
// Build
buildCmd := fmt.Sprintf("cd %s && go build -o %s .", svcPath, binPath)
if err := runSSH(sshHost, buildCmd); err != nil {
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)
}
// Stop existing if running
stopCmd := fmt.Sprintf("pkill -f '%s' || true", binPath)
runSSH(sshHost, stopCmd)
// Start in background
startCmd := fmt.Sprintf("nohup %s > %s/%s.log 2>&1 &", binPath, remotePath, svc.Name)
if err := runSSH(sshHost, startCmd); err != nil {
return fmt.Errorf("failed to start %s: %w", svc.Name, err)
}
fmt.Printf("✓ Deployed %s\n", svc.Name)
}
} else {
// Single service
name := filepath.Base(absDir)
binPath := filepath.Join(remotePath, "bin", name)
srcPath := filepath.Join(remotePath, "src")
binPath := filepath.Join(remotePath, "bin", "service")
buildCmd := fmt.Sprintf("cd %s && mkdir -p bin && go build -o %s .", remotePath, binPath)
if err := runSSH(sshHost, buildCmd); err != nil {
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)
}
}
stopCmd := fmt.Sprintf("pkill -f '%s' || true", binPath)
runSSH(sshHost, stopCmd)
return nil
}
startCmd := fmt.Sprintf("nohup %s > %s/%s.log 2>&1 &", binPath, remotePath, name)
if err := runSSH(sshHost, startCmd); err != nil {
func restartServices(target, remotePath string, cfg *config.Config) error {
// Create logs directory
runSSH(target, fmt.Sprintf("mkdir -p %s/logs", remotePath))
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.Printf("✓ Deployed %s\n", name)
fmt.Println(" ✓ service")
}
fmt.Printf("\n✓ Deployed to %s\n", target)
fmt.Printf("\nView logs: ssh %s 'tail -f %s/*.log'\n", sshHost, remotePath)
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
@@ -187,27 +206,33 @@ func runSSH(host, command string) error {
func init() {
cmd.Register(&cli.Command{
Name: "deploy",
Usage: "Deploy services to a target",
Description: `Deploy services using docker-compose or SSH.
Usage: "Deploy services via SSH",
Description: `Deploy copies binaries or source to a remote host and starts services.
If ./bin/ exists (from 'micro build'), copies binaries directly.
Otherwise, syncs source and builds on the remote host.
Examples:
micro deploy # Deploy with docker-compose
micro deploy --ssh user@host # Deploy via SSH
micro deploy --build # Rebuild before deploying`,
micro build --os linux # Build Linux binaries locally
micro deploy --ssh user@host # Copy binaries and restart
micro deploy --ssh user@host # Sync source, build on remote, restart
micro deploy --ssh user@host --build # Force rebuild on remote`,
Action: Deploy,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "ssh",
Usage: "Deploy via SSH to user@host",
Name: "ssh",
Usage: "Deploy to user@host via SSH",
Required: true,
},
&cli.StringFlag{
Name: "path",
Usage: "Remote path for SSH deploy (default: ~/micro)",
Usage: "Remote path (default: ~/micro)",
Value: "~/micro",
},
&cli.BoolFlag{
Name: "build",
Usage: "Rebuild before deploying",
Usage: "Force rebuild on remote (ignore local binaries)",
},
},
})
+75 -132
View File
@@ -4,19 +4,19 @@ layout: default
# Deployment
The `micro build` and `micro deploy` commands help you go from development to production.
Go produces self-contained binaries. No Docker required.
## Quick Start
```bash
# Build container images
micro build
# Build binaries
micro build --os linux
# Deploy with docker-compose
micro deploy
# Deploy to server
micro deploy --ssh user@host
```
## Building Images
## Building
### Basic Build
@@ -24,168 +24,111 @@ micro deploy
micro build
```
This:
1. Reads `micro.mu` (if present) to find services
2. Generates a `Dockerfile` for each service (if not present)
3. Runs `docker build` for each service
This builds Go binaries for all services in `micro.mu` (or the current directory) to `./bin/`.
### Build Options
### Cross-Compilation
```bash
micro build --tag v1.0.0 # Specific tag (default: latest)
micro build --registry docker.io/myuser # Push to registry
micro build --push # Build and push
micro build --os linux # For Linux servers
micro build --os linux --arch arm64 # For ARM64 (e.g., AWS Graviton)
micro build --os darwin # For macOS
micro build --os windows # For Windows (.exe)
```
### Generated Dockerfile
If no Dockerfile exists, one is generated:
```dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /service .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /service /app/service
EXPOSE 8080
CMD ["/app/service"]
```
Customize by creating your own `Dockerfile`.
## Generating docker-compose.yml
### Custom Output
```bash
micro build --compose
```
Generates a `docker-compose.yml` from your `micro.mu` config:
```yaml
version: '3.8'
services:
users:
image: users:latest
ports:
- "8081:8081"
environment:
- MICRO_REGISTRY=mdns
posts:
image: posts:latest
ports:
- "8082:8082"
depends_on:
- users
environment:
- MICRO_REGISTRY=mdns
micro build --output ./dist
```
## Deploying
### Docker Compose
```bash
micro deploy
```
Runs `docker compose up -d` using the generated `docker-compose.yml`.
```bash
micro deploy --build # Rebuild images first
```
### SSH Deploy
For simple deployments to a single server:
```bash
micro deploy --ssh user@host
```
This:
1. Creates `~/micro` on the remote host
2. Syncs your code using rsync
3. Builds each service on the remote host
4. Starts services in the background
1. Copies `./bin/*` to the remote host (if exists)
2. Or syncs source and builds on remote
3. Restarts services
### Workflow
**Option 1: Build locally, copy binaries**
```bash
micro deploy --ssh user@host --path /opt/myapp # Custom remote path
micro build --os linux # Build for target OS
micro deploy --ssh user@host # Copy and restart
```
**Option 2: Build on remote**
```bash
micro deploy --ssh user@host # Syncs source, builds there
```
### Remote Structure
```
~/micro/
├── bin/ # Service binaries
│ ├── users
│ ├── posts
│ └── web
├── logs/ # Service logs
│ ├── users.log
│ ├── posts.log
│ └── web.log
└── src/ # Source (if building on remote)
```
### View Logs
After deploying:
```bash
ssh user@host 'tail -f ~/micro/logs/*.log'
```
## Docker (Optional)
If you prefer containers:
```bash
# Docker Compose
docker compose logs -f
# SSH deploy
ssh user@host 'tail -f ~/micro/*.log'
micro build --docker # Build images
micro build --docker --push # Build and push to registry
micro build --compose # Generate docker-compose.yml
```
## Complete Workflow
Then deploy with docker-compose on your server:
```bash
# 1. Develop locally
micro run
# 2. Build images
micro build --tag v1.0.0
# 3. Generate compose file
micro build --compose
# 4. Deploy
micro deploy
scp docker-compose.yml user@host:~/
ssh user@host 'docker compose up -d'
```
Or for SSH:
## Complete Example
```bash
# 1. Develop locally
micro run
# Development
micro new myapp
cd myapp
micro run # Develop locally
# 2. Deploy to server
micro deploy --ssh user@host
# Build
micro build --os linux
# Deploy
micro deploy --ssh deploy@prod.example.com
# Check
ssh deploy@prod.example.com 'tail -f ~/micro/logs/*.log'
```
## Configuration
The `micro.mu` file drives both build and deploy:
```
service users
path ./users
port 8081
service posts
path ./posts
port 8082
depends users
service web
path ./web
port 8089
depends users posts
```
- `path` - Where to find the service code
- `port` - Exposed port (used in Dockerfile and compose)
- `depends` - Service dependencies (used in compose depends_on)
## Tips
1. **Version your images** - Use `--tag v1.0.0` not just `latest`
2. **Use a registry** - Push images with `--registry` for team sharing
3. **Custom Dockerfiles** - Override the generated one for complex builds
4. **SSH for simple deploys** - Great for single-server setups
5. **Compose for local prod** - Test production config locally
1. **Cross-compile locally** - Faster than building on remote
2. **Use `--os linux`** - Most servers are Linux
3. **Single binary** - Go's strength, no runtime needed
4. **Logs in ~/micro/logs/** - Easy to tail and rotate
5. **No Docker needed** - Unless you want it