| ← Back to Documentation Index | Main README |
The Garage Installer includes comprehensive state persistence and checkpoint/resume capability, making it resilient to failures and enabling recovery from any point in the installation process.
1. Automatic State Saving
2. Resume from Checkpoint
3. Uninstall Auto-Load
4. Rollback Support
State is persisted after completing each of these operations:
State is cleared when:
Path: .garage-installer-state.json (in current directory)
Permissions: Readable by user only (recommended: chmod 600)
Version Control: Automatically added to .gitignore
{
"version": "1.0.0",
"nodes": [
{
"name": "node1",
"host": "192.168.1.100",
"port": 22,
"username": "ubuntu",
"authMethod": "key",
"keyPath": "/home/user/.ssh/id_rsa"
},
{
"name": "node2",
"host": "192.168.1.101",
"port": 22,
"username": "ubuntu",
"authMethod": "password"
}
],
"cluster": {
"rpcSecret": "abc123...",
"adminToken": "def456...",
"capacityPerNode": "100G",
"dataDir": "/home/ubuntu/garage/data",
"metaDir": "/home/ubuntu/garage/meta",
"workdir": "/home/ubuntu/garage",
"garageVersion": "v2.1.0",
"replicationFactor": 2,
"ports": {
"s3Api": 3900,
"rpc": 3901,
"s3Web": 3902,
"admin": 3903
}
},
"phases": {
"nodeConfig": "completed",
"connectivity": "completed",
"preflightChecks": "completed",
"clusterConfig": "completed",
"deployment": "in-progress",
"configuration": "not-started",
"postInstall": "not-started"
},
"nodeState": {
"node1": {
"containerDeployed": true,
"configWritten": true,
"nodeIdRetrieved": true,
"nodeId": "abc123456789...",
"clusterConfigured": false
},
"node2": {
"containerDeployed": false,
"configWritten": false,
"nodeIdRetrieved": false,
"nodeId": null,
"clusterConfigured": false
}
},
"lastUpdated": "2025-11-16T10:30:00.000Z"
}
version
1.0.0nodes[]
authMethod: Either "key" or "password"keyPath: Path to SSH private key (if key auth)password field is never storedcluster
rpcSecret: Generated cryptographically (32-byte hex)adminToken: Admin API bearer tokencapacityPerNode: Human-readable format (100G, 1T, etc.)phases
"not-started", "in-progress", "completed", "failed"nodeState
lastUpdated
The installer tracks progress through 7 distinct phases:
Purpose: Collect SSH details for both nodes
Saved:
Resume Skip: If completed, restored from state
Purpose: Test SSH connections to both nodes
Saved:
Resume Skip: If completed, connections re-established but not re-tested
Purpose: Validate system requirements on both nodes
Checks:
Resume Skip: If completed, checks not re-run (assumes still valid)
Purpose: Configure cluster parameters
Saved:
Resume Skip: If completed, configuration restored from state
Purpose: Deploy Garage containers on both nodes
Per-Node Operations:
Saved (per node):
containerDeployed: Container runningconfigWritten: Files creatednodeIdRetrieved: Node ID obtainednodeId: Actual node ID valueResume Behavior: Skips completed nodes, continues with incomplete
Purpose: Configure Garage cluster
Operations:
Saved (per node):
clusterConfigured: Layout appliedResume Behavior: Re-applies if incomplete
Purpose: Validate installation and test
Operations:
Saved: Completion status
Resume Skip: Re-runs validation even if previously completed
┌─────────────────────────────────────┐
│ Installer Starts │
└──────────────┬──────────────────────┘
│
┌──────▼──────┐
│ Check for │
│ state file │
└──────┬──────┘
│
┌──────▼──────┐
│ Exists? │
└──────┬──────┘
╱ ╲
No Yes
│ │
▼ ▼
┌──────────┐ ┌─────────────┐
│ Normal │ │ Load state │
│ install │ │ Display info│
└──────────┘ └──────┬──────┘
│
┌──────▼──────┐
│ Prompt user │
└──────┬──────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌───▼────┐ ┌───▼────┐
│ Resume │ │ Fresh │ │ Cancel │
└────┬────┘ └───┬────┘ └───┬────┘
│ │ │
│ ┌─────▼─────┐ │
│ │ Clear │ │
│ │ state │ │
│ └─────┬─────┘ │
│ │ │
└────────────┼────────────┘
│
┌─────▼──────┐
│ Continue │
│installation│
└────────────┘
On Startup with Existing State:
⚠️ Found previous installation in progress
Last completed: deployment
Next step: configuration
Last updated: 2025-11-16T10:30:00Z
What would you like to do?
> Resume installation from last checkpoint
Start fresh (clear previous state)
Cancel
If Resume Selected:
📂 Loading saved state...
✓ Restored node configurations (2 nodes)
✓ Restored cluster configuration
🔐 Re-entering credentials...
Enter password for ubuntu@node1: ********
⏭ Skipping completed phases:
✓ Node Configuration
✓ Connectivity Test
✓ Preflight Checks
✓ Cluster Configuration
✓ Deployment (node1)
▶️ Resuming from: Deployment (node2)
Automatically:
Re-Prompted:
What Happened:
State Captured:
{
"phases": {
"deployment": "in-progress"
},
"nodeState": {
"node1": {"containerDeployed": true, "nodeId": "abc123"},
"node2": {"containerDeployed": false, "nodeId": null}
}
}
Recovery:
What Happened:
State Captured:
{
"phases": {
"nodeConfig": "completed",
"connectivity": "completed",
"preflightChecks": "in-progress"
}
}
Recovery:
What Happened:
State Captured:
{
"phases": {
"preflightChecks": "failed"
}
}
Recovery:
sudo usermod -aG docker $USERnewgrp docker)What Happened:
State Captured:
{
"phases": {
"deployment": "completed",
"configuration": "failed"
},
"nodeState": {
"node1": {"containerDeployed": true, "clusterConfigured": false},
"node2": {"containerDeployed": true, "clusterConfigured": false}
}
}
Recovery:
View State File:
cat .garage-installer-state.json | jq .
Check Last Phase:
cat .garage-installer-state.json | jq '.phases'
Extract Node Info:
cat .garage-installer-state.json | jq '.nodes'
Clear State Manually:
rm .garage-installer-state.json
Backup State:
cp .garage-installer-state.json state-backup-$(date +%Y%m%d).json
Symptoms:
Recovery:
# Option 1: Delete and start fresh
rm .garage-installer-state.json
# Option 2: Try to repair with jq
cat .garage-installer-state.json | jq . > state-fixed.json
mv state-fixed.json .garage-installer-state.json
# Option 3: Restore from backup
cp state-backup-20251116.json .garage-installer-state.json
For developers extending the installer:
import { StateManager } from "./src/state.ts";
const stateManager = new StateManager();
// Initialize new state
await stateManager.initializeState();
// Check if state exists
const hasState = await stateManager.exists();
// Load existing state
await stateManager.load();
// Update nodes
stateManager.updateNodes(nodeConfigs);
// Update cluster config
stateManager.updateCluster(clusterConfig);
// Mark phase as started
stateManager.startPhase("deployment");
// Mark phase as completed
stateManager.completePhase("deployment");
// Mark phase as failed
stateManager.failPhase("deployment");
// Update per-node state
stateManager.updateNodeState("node1", {
containerDeployed: true,
configWritten: true
});
// Save to disk
await stateManager.save();
// Get status
const isComplete = stateManager.isComplete();
const isInProgress = stateManager.isInProgress();
const lastPhase = stateManager.getLastCompletedPhase();
const nextPhase = stateManager.getNextPendingPhase();
// Clear state
await stateManager.clear();
✅ Stored in State File:
❌ NOT Stored:
File Permissions:
# Restrict to owner only
chmod 600 .garage-installer-state.json
# Verify
ls -la .garage-installer-state.json
# Should show: -rw------- 1 user user
Secrets in State:
Best Practices:
Example: Extract Node IDs:
jq -r '.nodeState | to_entries[] | "\(.key): \(.value.nodeId)"' .garage-installer-state.json
Example: Check Progress:
jq '.phases | to_entries[] | select(.value == "completed") | .key' .garage-installer-state.json
Example: Get Cluster Endpoint:
NODE1=$(jq -r '.nodes[0].host' .garage-installer-state.json)
PORT=$(jq -r '.cluster.ports.s3Api' .garage-installer-state.json)
echo "S3 Endpoint: http://$NODE1:$PORT"
If state format changes between versions:
// Hypothetical migration function
function migrateState(oldState: any): State {
if (oldState.version === "1.0.0") {
// Add new fields with defaults
oldState.version = "1.1.0";
oldState.newField = "default";
}
return oldState as State;
}
Current Limitation: One state file per directory
Workaround: Use different directories:
mkdir ~/cluster1
cd ~/cluster1
./garage-installer
mkdir ~/cluster2
cd ~/cluster2
./garage-installer
Each maintains independent state.
Cause: Installer crashed before save
Solution: No recovery - start fresh installation
Cause: Phase status not properly updated
Solution:
# Edit with jq
jq '.phases.deployment = "completed"' .garage-installer-state.json > tmp.json
mv tmp.json .garage-installer-state.json
Cause: One node keeps failing, want to continue without it
Solution: Not supported - installer requires both nodes. Either:
| ← Back to Documentation Index | Main README |
nodeConfig - Node configuration collectionconnectivity - SSH connectivity testingpreflightChecks - System requirements validationclusterConfig - Cluster configurationdeployment - Container deploymentconfiguration - Garage cluster setuppostInstall - Validation testingStored in .garage-installer-state.json (gitignored):
{
"version": "1.0.0",
"nodes": [
{
"name": "node1",
"host": "192.168.1.100",
"port": 22,
"username": "ubuntu",
"authMethod": "key",
"keyPath": "/path/to/key"
}
],
"cluster": {
"garageVersion": "v2.1.0",
"workdir": "/home/ubuntu/garage",
"dataDir": "/home/ubuntu/garage/data",
"metaDir": "/home/ubuntu/garage/meta",
"replicationFactor": 2,
"rpcSecret": "...",
"capacity": "1T",
"ports": { ... }
},
"phases": {
"nodeConfig": "completed",
"connectivity": "completed",
"preflightChecks": "in-progress",
...
},
"nodeState": {
"node1": {
"containerDeployed": true,
"configWritten": false,
"clusterConfigured": false
}
},
"lastUpdated": "2024-01-15T10:30:00.000Z"
}
Security Note: Passwords are NOT stored in the state file. On resume, user is prompted to re-enter passwords.
deno task start
# Select: Install
# Follow prompts...
# Installation completes successfully
# State file automatically deleted
deno task start
# If state found:
# ✓ Found previous installation state:
# Phase: deployment (failed)
# Nodes: node1, node2
#
# What would you like to do?
# 1. Resume installation
# 2. Start fresh
# 3. Cancel
#
# Select: 1 (Resume)
# Prompts for passwords
# Continues from deployment phase
deno task start
# Select: Uninstall
# ✓ Found saved installation state
# • node1 (192.168.1.100)
# • node2 (192.168.1.101)
#
# Use these nodes for uninstall? Yes
# Enter passwords...
# Uninstallation proceeds
src/state.ts)initializeState() - Create new state structureload() - Load from disksave() - Save to diskclear() - Delete state fileupdatePhase() - Mark phase statusupdateNodes() - Save node configsupdateCluster() - Save cluster configupdateNodeState() - Track per-node deploymentgetLastCompletedPhase() - Find checkpointgetNextPendingPhase() - Determine resume pointisComplete() - Check if all phases doneisInProgress() - Check if installation activesrc/wizard.ts)stateManager propertyrun():
resumeInstallation() or continues normallyresumeInstallation() method:
runUninstall():
Run the state manager test:
./test_state.ts
Expected output:
| ← Back to Documentation Index | Main README |