| ← Back to Documentation Index | Main README |
This document provides a comprehensive technical overview of the Garage Installer’s architecture, design decisions, and implementation details.
The installer is built with Deno for several strategic reasons:
Single Binary Distribution
deno compileTypeScript Native
Dependency Management
Security by Default
Docker provides the deployment foundation:
Non-Root Execution
Dependency Isolation
Easy Cleanup
Version Control
The installer is organized into focused modules with clear responsibilities:
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:
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();
Role: Preflight validation
Responsibilities:
Check Interface:
interface CheckResult {
name: string;
passed: boolean;
message: string;
autoFix?: (ssh: SSHConnection) => Promise<void>;
}
Current Checks:
Role: Docker operations
Responsibilities:
Key Features:
sudo docker when neededExample:
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);
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:
Role: State persistence and resume capability
Responsibilities:
.garage-installer-state.jsonState 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.
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");
Role: Logging and audit trail
Responsibilities:
garage-installer.logLog 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
Role: User interface and feedback
Responsibilities:
Features:
The state management system enables resume capability and tracks installation progress:
Key Features:
States:
not-started - Phase not yet begunin-progress - Phase currently executingcompleted - Phase successfully finishedfailed - Phase encountered errorResume Flow:
.garage-installer-state.jsonThe cleanup system provides automatic rollback on failure:
Tracking:
docker stop && docker rmrm -f <file>rm -rf <directory>Cleanup Triggers:
Cleanup Sequence:
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
Consistent error handling throughout:
Error Contexts:
Recovery Strategies:
┌─────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────┘
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:
network_mode: host - Simplifies port management, avoids Docker networkinguser: "UID:GID" - Non-root container for securityBootstrap Peer Update:
docker exec garage /garage node iddocker compose restartThis two-phase approach avoids chicken-and-egg problem of needing node IDs before deployment.
Dynamic Elements:
rpc_secret - Generated using crypto.getRandomValues() (32 bytes hex)admin_token - Generated similarlybootstrap_peers - Populated after node ID retrievalrpc_public_addr - Uses user-provided hostname or IPStatic Elements:
[::] bind addresses)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:
garage layout showCapacity Parsing:
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):
Key-Based Authentication (preferred):
Password Authentication (fallback):
Connection Security:
Non-Root Execution:
Read-Only Configuration:
Network Isolation:
What’s Stored in State File:
What’s Never Stored:
State File Protection:
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
~/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
Connection Pooling:
Timeout Configuration:
Where Used:
Where Serial:
Installer:
Deployed Garage:
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);
}
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 |