garage-installer

Architecture Deep Dive

← Back to Documentation Index Main README

This document provides a comprehensive technical overview of the Garage Installer’s architecture, design decisions, and implementation details.

Table of Contents


Design Philosophy

Why Deno?

The installer is built with Deno for several strategic reasons:

Single Binary Distribution

TypeScript Native

Dependency Management

Security by Default

Why Docker?

Docker provides the deployment foundation:

Non-Root Execution

Dependency Isolation

Easy Cleanup

Version Control


Module Architecture

The installer is organized into focused modules with clear responsibilities:

src/wizard.ts

Role: Orchestration and user interaction

Responsibilities:

Key Interfaces:

interface NodeConfig {
  name: string;
  host: string;
  port: number;
  username: string;
  authMethod: "key" | "password";
  keyPath?: string;
  password?: string;
}

interface ClusterConfig {
  rpcSecret: string;
  adminToken: string;
  capacityPerNode: string;
  workdir: string;
  garageVersion: string;
  replicationFactor: number;
  ports: { s3Api, rpc, s3Web, admin };
}

Phases:

  1. Node Configuration - Collect SSH details
  2. Connectivity Test - Verify SSH access
  3. Preflight Checks - System validation
  4. Cluster Configuration - Capacity, version, ports
  5. Deployment - Docker and Garage setup
  6. Cluster Setup - Connect nodes, apply layout
  7. Validation - Health checks and AWS CLI test

src/ssh/connection.ts

Role: SSH communication layer

Responsibilities:

Features:

Example:

const ssh = new SSHConnection(nodeConfig);
await ssh.connect();
const result = await ssh.exec("docker ps", { timeout: 10000 });
await ssh.writeFile("/path/to/file", content);
await ssh.disconnect();

src/checks/system.ts

Role: Preflight validation

Responsibilities:

Check Interface:

interface CheckResult {
  name: string;
  passed: boolean;
  message: string;
  autoFix?: (ssh: SSHConnection) => Promise<void>;
}

Current Checks:

src/docker/manager.ts

Role: Docker operations

Responsibilities:

Key Features:

Example:

const docker = new DockerManager(ssh);
await docker.pullImage("dxflrs/garage:v2.1.0");
await docker.deployWithCompose(composeYaml, "/home/user/garage");
const logs = await docker.getContainerLogs("garage", 50);

src/garage/cluster.ts

Role: Garage-specific operations

Responsibilities:

Configuration Generation:

# garage.toml structure
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
replication_factor = 2

[rpc]
rpc_secret = "<cryptographically-random-secret>"
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "<node-hostname>:3901"
bootstrap_peers = ["<peer1-id>@<peer1-host>:3901", ...]

[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"

[s3_web]
bind_addr = "[::]:3902"

[admin]
api_bind_addr = "[::]:3903"
admin_token = "<admin-token>"

Bootstrap Peers:

src/state.ts

Role: State persistence and resume capability

Responsibilities:

State Structure:

{
  "version": "1.0.0",
  "nodes": [
    {
      "name": "node1",
      "host": "192.168.1.100",
      "username": "ubuntu",
      "authMethod": "key"
    }
  ],
  "cluster": {
    "garageVersion": "v2.1.0",
    "workdir": "/home/ubuntu/garage",
    "replicationFactor": 2,
    "rpcSecret": "...",
    "capacity": "100G"
  },
  "phases": {
    "nodeConfig": "completed",
    "connectivity": "completed",
    "preflightChecks": "in-progress"
  },
  "lastUpdated": "2025-11-16T10:30:00Z"
}

See State Persistence & Resume for details.

src/cleanup.ts

Role: Rollback and cleanup management

Responsibilities:

Tracked Resources:

Example:

cleanupManager.trackContainer("node1", "garage");
cleanupManager.trackFile("node1", "/home/user/garage/garage.toml");
await cleanupManager.cleanupNode("node1");

src/logger.ts

Role: Logging and audit trail

Responsibilities:

Log Levels:

Example Log:

2025-11-16T10:30:15.234Z [INFO] === Garage Installer Started ===
2025-11-16T10:30:16.123Z [INFO] Connecting to node1 (192.168.1.100:22)
2025-11-16T10:30:17.456Z [INFO] SSH connection successful
2025-11-16T10:30:18.789Z [ERROR] Preflight check failed: Docker not installed

src/ui/display.ts & src/ui/spinner.ts

Role: User interface and feedback

Responsibilities:

Features:


Core Systems

State Management System

The state management system enables resume capability and tracks installation progress:

Key Features:

  1. Checkpoint Persistence - Saves state after each major phase
  2. Resume Detection - Detects incomplete installations on startup
  3. Phase Tracking - Tracks completion status per phase
  4. Configuration Storage - Preserves node and cluster config

States:

Resume Flow:

  1. Detect .garage-installer-state.json
  2. Load previous state
  3. Show user last completed phase
  4. Offer to resume or start fresh
  5. Skip completed phases
  6. Continue from last checkpoint

Cleanup System

The cleanup system provides automatic rollback on failure:

Tracking:

Cleanup Triggers:

Cleanup Sequence:

  1. Stop and remove containers
  2. Remove configuration files
  3. Remove data directories
  4. Clean up Docker volumes
  5. Report summary of cleaned resources

Logging System

Comprehensive logging for troubleshooting and auditing:

Log File: garage-installer.log (current directory)

What’s Logged:

Usage:

# View real-time logs
tail -f garage-installer.log

# Search for errors
grep ERROR garage-installer.log

# View specific phase
grep "Phase: deployment" garage-installer.log

Error Handling

Consistent error handling throughout:

Error Contexts:

Recovery Strategies:

  1. Retry with exponential backoff - Transient network issues
  2. Suggest manual fix - Permission issues, missing software
  3. Cleanup and exit - Fatal errors
  4. Resume from checkpoint - Partial failures

Deployment Flow

Complete Installation Sequence

┌─────────────────────────────────────────────────┐
│ 1. Node Configuration                           │
│    - Collect SSH details for both nodes         │
│    - Validate input format                      │
│    - Store in state                             │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────┐
│ 2. SSH Connectivity Test                        │
│    - Attempt SSH connection                     │
│    - Test authentication                        │
│    - Verify command execution                   │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────┐
│ 3. Preflight Checks (per node)                  │
│    - Check OS compatibility                     │
│    - Verify Docker installed                    │
│    - Check Docker permissions                   │
│    - Validate disk space (16GB+)                │
│    - Test port availability (3900-3903)         │
│    - Confirm Docker Compose present             │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────┐
│ 4. Cluster Configuration                        │
│    - Generate RPC secret (crypto random)        │
│    - Generate admin token                       │
│    - Select Garage version                      │
│    - Set capacity per node                      │
│    - Configure ports (or use defaults)          │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────┐
│ 5. Deployment (per node)                        │
│    - Create working directory                   │
│    - Generate garage.toml (no bootstrap peers)  │
│    - Generate docker-compose.yml                │
│    - Pull Garage Docker image                   │
│    - Deploy container                           │
│    - Wait for container healthy                 │
│    - Get node ID                                │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────┐
│ 6. Cluster Setup                                │
│    - Update configs with bootstrap peers        │
│    - Restart containers                         │
│    - Connect nodes via garage CLI               │
│    - Apply cluster layout                       │
│    - Wait for layout convergence                │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▼────────────────────────────────┐
│ 7. Validation                                   │
│    - Check cluster status                       │
│    - Verify both nodes connected                │
│    - Create admin key                           │
│    - Test S3 API with AWS CLI                   │
│    - Display success message and credentials    │
└─────────────────────────────────────────────────┘

Docker Compose Deployment Strategy

Initial Deployment (no bootstrap peers):

services:
  garage:
    image: dxflrs/garage:v2.1.0
    container_name: garage
    restart: unless-stopped
    network_mode: host
    user: "1000:1000"
    volumes:
      - ./garage.toml:/etc/garage.toml:ro
      - /home/user/garage/meta:/var/lib/garage/meta
      - /home/user/garage/data:/var/lib/garage/data
    environment:
      - RUST_LOG=garage=info
    command: ["/garage", "server"]

Key Design Decisions:

Bootstrap Peer Update:

  1. Deploy containers without bootstrap_peers
  2. Retrieve node IDs using docker exec garage /garage node id
  3. Update garage.toml with bootstrap_peers list
  4. Restart containers with docker compose restart

This two-phase approach avoids chicken-and-egg problem of needing node IDs before deployment.


Configuration Management

Garage Configuration (garage.toml)

Dynamic Elements:

Static Elements:

Layout Configuration

Two-Node Layout:

# Both nodes get same capacity and zone
garage layout assign -z dc1 -c 100G <node1-id>
garage layout assign -z dc1 -c 100G <node2-id>

# Apply with version increment
garage layout apply --version 1

Version Handling:

Capacity Parsing:


Security Architecture

Secrets Generation

RPC Secret (inter-node authentication):

const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
const secret = Array.from(bytes)
  .map(b => b.toString(16).padStart(2, '0'))
  .join('');

Admin Token (Admin API access):

SSH Security

Key-Based Authentication (preferred):

Password Authentication (fallback):

Connection Security:

Container Security

Non-Root Execution:

Read-Only Configuration:

Network Isolation:

Credential Handling

What’s Stored in State File:

What’s Never Stored:

State File Protection:


File Structure

Installer Source Code

garage-installer/
├── mod.ts                      # Entry point
├── deno.json                   # Deno configuration & tasks
├── README.md                   # Main documentation
├── FUTURES.md                  # Roadmap & planned features
├── DOC_UPDATES.md             # Documentation improvement plan
│
├── src/
│   ├── wizard.ts              # Main orchestration (2200+ lines)
│   ├── constants.ts           # Configuration constants
│   ├── state.ts               # State persistence system
│   ├── cleanup.ts             # Cleanup & rollback manager
│   ├── logger.ts              # Logging system
│   │
│   ├── ssh/
│   │   └── connection.ts      # SSH communication layer
│   │
│   ├── checks/
│   │   └── system.ts          # Preflight validation checks
│   │
│   ├── docker/
│   │   └── manager.ts         # Docker operations wrapper
│   │
│   ├── garage/
│   │   └── cluster.ts         # Garage-specific operations
│   │
│   └── ui/
│       ├── display.ts         # Output formatting
│       └── spinner.ts         # Progress indicators
│
├── scripts/
│   └── build.sh               # Cross-platform build script
│
└── docs/
    ├── README.md              # Documentation index
    ├── architecture.md        # This file
    ├── troubleshooting.md     # Comprehensive troubleshooting
    ├── aws-cli-configuration.md
    ├── nodejs-express-integration.md
    └── state-persistence.md

Deployed Structure (per node)

~/garage/                       # Working directory
├── docker-compose.yml          # Docker Compose config
├── garage.toml                 # Garage configuration
├── meta/                       # Metadata directory (Docker volume)
└── data/                       # Data directory (Docker volume)

Container Filesystem:

/etc/garage.toml               # Config (mounted read-only)
/var/lib/garage/
  ├── meta/                    # Metadata storage
  └── data/                    # Data storage
/garage                        # Garage binary

Performance Considerations

SSH Connection Management

Connection Pooling:

Timeout Configuration:

Parallel Execution

Where Used:

Where Serial:

Resource Usage

Installer:

Deployed Garage:


Extension Points

Adding New Checks

To add a system check:

// In src/checks/system.ts
private async checkMemory(): Promise<CheckResult> {
  const result = await this.ssh.exec("free -g | awk '/^Mem:/{print $2}'");
  const memoryGB = parseInt(result.stdout.trim());
  
  return {
    name: "System Memory",
    passed: memoryGB >= 2,
    message: memoryGB >= 2 
      ? `${memoryGB}GB RAM available` 
      : `Only ${memoryGB}GB RAM (need 2GB+)`,
  };
}

// Add to runAll()
async runAll(): Promise<CheckResult[]> {
  const checks = [
    // ... existing checks
    this.checkMemory(),
  ];
  return await Promise.all(checks);
}

Adding New Phases

To add a phase to the installation:

// In src/wizard.ts
async runMyNewPhase() {
  this.stateManager.startPhase("myNewPhase");
  
  try {
    // Your phase logic here
    
    this.stateManager.completePhase("myNewPhase");
  } catch (error) {
    this.stateManager.failPhase("myNewPhase");
    throw error;
  }
}

// Add to main run() method
await this.runMyNewPhase();


← Back to Documentation Index Main README