Skip to content

Commit 2777a03

Browse files
authored
Fix Docker Compose validator deployment environment persistence (#4586)
## Problem Docker Compose validators lose configuration on restart because environment variables aren't persisted. The deploy-validator.sh script sets variables but doesn't save them to a .env file, causing validators to fail after docker-compose restart. ## Solution 1. **Modified deploy-validator.sh** to create a .env file with all configuration 2. **Created fix-validator-env.sh** migration script for existing validators 3. **Fixed log output contamination** in PUBLIC_KEY generation 4. **Added backup scripts** for validator keys trapped in Docker volumes ## Changes - `scripts/deploy-validator.sh`: Now creates .env file for Docker Compose persistence and redirects logs to stderr to prevent PUBLIC_KEY contamination - `scripts/fix-validator-env.sh`: Migration script to generate .env from legacy .deployment-info files (handles malformed PUBLIC_KEY with ANSI codes) - `scripts/backup-validator-keys.sh`: Comprehensive backup tool for validator keys - `scripts/extract-wallet-from-volume.sh`: Quick extraction tool for immediate key recovery ## Testing - Tested migration script with real .deployment-info files containing malformed PUBLIC_KEY - Verified .env file generation preserves all required variables - Confirmed Docker Compose restart maintains configuration with .env file ## Impact Critical fix for validator operators - prevents configuration loss on restart
1 parent b2b6cc5 commit 2777a03

File tree

5 files changed

+371
-20
lines changed

5 files changed

+371
-20
lines changed

docker/.env.production.template

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Production deployment environment variables
2+
# This file should be created by deploy-validator.sh and renamed to .env
3+
# These variables are critical for the validator to function properly after restart
4+
5+
# Domain configuration (set by deploy script)
6+
DOMAIN=${DOMAIN}
7+
ACME_EMAIL=${ACME_EMAIL}
8+
9+
# Genesis configuration (critical - validator won't work without this)
10+
GENESIS_URL=${GENESIS_URL}
11+
GENESIS_BUCKET=${GENESIS_BUCKET}
12+
GENESIS_PATH_PREFIX=${GENESIS_PATH_PREFIX}
13+
14+
# Validator configuration
15+
VALIDATOR_KEY=${VALIDATOR_KEY}
16+
VALIDATOR_NAME=${VALIDATOR_NAME}
17+
18+
# Network configuration
19+
FAUCET_URL=${FAUCET_URL}
20+
FAUCET_PORT=${FAUCET_PORT:-8080}
21+
22+
# Storage configuration
23+
LINERA_STORAGE_SERVICE_PORT=${LINERA_STORAGE_SERVICE_PORT:-1235}
24+
25+
# Docker image
26+
LINERA_IMAGE=${LINERA_IMAGE}
27+
28+
# Add any other deployment-specific variables here

scripts/backup-validator-keys.sh

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env bash
2+
# Script to backup validator keys and wallet from Docker volumes
3+
# CRITICAL: Run this regularly to protect against data loss
4+
5+
set -euo pipefail
6+
7+
BACKUP_DIR="validator-backup-$(date +%Y%m%d-%H%M%S)"
8+
DOCKER_COMPOSE_DIR="docker"
9+
10+
echo "🔑 Linera Validator Key Backup Tool"
11+
echo "===================================="
12+
echo ""
13+
14+
# Create backup directory
15+
mkdir -p "${BACKUP_DIR}"
16+
cd "${BACKUP_DIR}"
17+
18+
echo "📁 Created backup directory: ${BACKUP_DIR}"
19+
echo ""
20+
21+
# Function to backup from container
22+
backup_container_files() {
23+
local container="$1"
24+
local paths="$2"
25+
local description="$3"
26+
27+
echo "→ Checking ${description} in container: ${container}"
28+
29+
# Check if container exists and is running
30+
if ! docker ps --format '{{.Names}}' | grep -q "^${container}$"; then
31+
echo " ⚠️ Container ${container} not running, skipping..."
32+
return
33+
fi
34+
35+
# Create container backup directory
36+
mkdir -p "${container}"
37+
38+
# Try to backup each path
39+
for path in ${paths}; do
40+
# Check if path exists in container
41+
if docker exec "${container}" test -e "${path}" 2>/dev/null; then
42+
echo " ✓ Found ${path}"
43+
docker cp "${container}:${path}" "${container}/" 2>/dev/null || true
44+
fi
45+
done
46+
}
47+
48+
# Backup from proxy container
49+
backup_container_files "proxy" \
50+
"/linera/*.json /root/.config/linera /data /linera-storage" \
51+
"proxy wallet and storage"
52+
53+
# Backup from shard containers (there may be multiple)
54+
for shard in $(docker ps --format '{{.Names}}' | grep -E '^(docker-)?shard'); do
55+
backup_container_files "${shard}" \
56+
"/linera/*.json /root/.config/linera /data /linera-storage" \
57+
"shard wallet and storage"
58+
done
59+
60+
# Backup ScyllaDB data volume info (for reference)
61+
echo ""
62+
echo "→ Getting volume information..."
63+
docker volume inspect linera-scylla-data 2>/dev/null >scylla-volume-info.json ||
64+
echo " ⚠️ ScyllaDB volume not found"
65+
66+
# Backup docker-compose environment
67+
echo ""
68+
echo "→ Backing up deployment configuration..."
69+
if [ -f "../${DOCKER_COMPOSE_DIR}/.env" ]; then
70+
cp "../${DOCKER_COMPOSE_DIR}/.env" ./env-backup
71+
echo " ✓ Backed up .env file"
72+
elif [ -f "../${DOCKER_COMPOSE_DIR}/.deployment-info" ]; then
73+
cp "../${DOCKER_COMPOSE_DIR}/.deployment-info" ./deployment-info-backup
74+
echo " ✓ Backed up .deployment-info file"
75+
fi
76+
77+
# Get container configuration
78+
echo ""
79+
echo "→ Saving container configuration..."
80+
docker compose -f "../${DOCKER_COMPOSE_DIR}/docker-compose.yml" config >docker-compose-config.yml 2>/dev/null || true
81+
82+
# Create restore instructions
83+
cat >RESTORE_INSTRUCTIONS.md <<'EOF'
84+
# Validator Key Restoration Instructions
85+
86+
## ⚠️ CRITICAL FILES
87+
88+
Look for these files in your backup:
89+
- `wallet.json` - Your validator wallet
90+
- `keystore.json` - Your validator keystore
91+
- Any `.json` files containing keys
92+
- `.config/linera/` directory
93+
94+
## To Restore
95+
96+
1. Stop your validator:
97+
```bash
98+
cd docker && docker compose down
99+
```
100+
101+
2. Copy wallet files back to container volumes:
102+
```bash
103+
# After starting containers
104+
docker cp wallet.json proxy:/linera/
105+
docker cp keystore.json proxy:/linera/
106+
```
107+
108+
3. Restart services:
109+
```bash
110+
docker compose up -d
111+
```
112+
113+
## Emergency Recovery
114+
115+
If volumes are corrupted, you may need to:
116+
1. Delete the old volumes: `docker volume rm linera-scylla-data`
117+
2. Recreate from this backup
118+
3. Re-sync with the network
119+
120+
## Backup Contents
121+
122+
This backup was created: $(date)
123+
Host: $(hostname)
124+
EOF
125+
126+
# Create tarball of entire backup
127+
cd ..
128+
tar -czf "${BACKUP_DIR}.tar.gz" "${BACKUP_DIR}"
129+
130+
echo ""
131+
echo "✅ Backup completed successfully!"
132+
echo ""
133+
echo "📦 Backup saved to:"
134+
echo " Directory: $(pwd)/${BACKUP_DIR}/"
135+
echo " Archive: $(pwd)/${BACKUP_DIR}.tar.gz"
136+
echo ""
137+
echo "⚠️ IMPORTANT: Copy ${BACKUP_DIR}.tar.gz to a safe location OFF this server!"
138+
echo ""
139+
echo "Suggested backup locations:"
140+
echo " - Your local machine: scp $(hostname):$(pwd)/${BACKUP_DIR}.tar.gz ~/"
141+
echo " - Cloud storage: gsutil cp ${BACKUP_DIR}.tar.gz gs://your-backup-bucket/"
142+
echo " - Another server: rsync -av ${BACKUP_DIR}.tar.gz user@backup-server:~/"

scripts/deploy-validator.sh

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -501,10 +501,11 @@ generate_validator_keys() {
501501
local image="$1"
502502
local config_file="validator-config.toml"
503503

504-
log INFO "Generating validator keys..."
504+
# Log to stderr so it doesn't get captured in the output
505+
log INFO "Generating validator keys..." >&2
505506

506507
if [[ "${DRY_RUN:-0}" == "1" ]]; then
507-
log INFO "[DRY RUN] Would generate validator keys using image: ${image}"
508+
log INFO "[DRY RUN] Would generate validator keys using image: ${image}" >&2
508509
echo "DRY_RUN_PUBLIC_KEY"
509510
return 0
510511
fi
@@ -517,7 +518,7 @@ generate_validator_keys() {
517518
/linera-server generate --validators "${config_file}")
518519

519520
if [ -z "${public_key}" ]; then
520-
log ERROR "Failed to generate validator keys"
521+
log ERROR "Failed to generate validator keys" >&2
521522
return 1
522523
fi
523524

@@ -1014,7 +1015,7 @@ main() {
10141015
# Generate validator keys
10151016
local public_key
10161017
if ! public_key=$(generate_validator_keys "${LINERA_IMAGE}"); then
1017-
log ERROR "Failed to generate validator keys"
1018+
log ERROR "Failed to generate validator keys" >&2
10181019
exit 1
10191020
fi
10201021

@@ -1056,26 +1057,48 @@ main() {
10561057
log INFO " Restart services:"
10571058
log INFO " cd ${DOCKER_COMPOSE_DIR} && docker compose restart"
10581059

1059-
# Save deployment info
1060-
local deployment_info="${REPO_ROOT}/${DOCKER_COMPOSE_DIR}/.deployment-info"
1061-
cat >"${deployment_info}" <<EOF
1062-
# Deployment Information
1060+
# Create .env file for Docker Compose (this is the source of truth)
1061+
local env_file="${REPO_ROOT}/${DOCKER_COMPOSE_DIR}/.env"
1062+
cat >"${env_file}" <<EOF
1063+
# Validator Deployment Configuration
10631064
# Generated: $(date -Iseconds)
1064-
HOST=${host}
1065-
EMAIL=${ACME_EMAIL}
1066-
PUBLIC_KEY=${public_key}
1067-
BRANCH=${branch_name}
1068-
COMMIT=${git_commit}
1069-
IMAGE=${LINERA_IMAGE}
1070-
CUSTOM_TAG=${custom_tag:-N/A}
1065+
# This file is the source of truth for Docker Compose configuration
1066+
# It persists all settings across container restarts
1067+
1068+
# Deployment metadata
1069+
DEPLOYMENT_HOST=${host}
1070+
DEPLOYMENT_EMAIL=${ACME_EMAIL}
1071+
DEPLOYMENT_PUBLIC_KEY=${public_key}
1072+
DEPLOYMENT_BRANCH=${branch_name}
1073+
DEPLOYMENT_COMMIT=${git_commit}
1074+
DEPLOYMENT_CUSTOM_TAG=${custom_tag:-N/A}
1075+
DEPLOYMENT_DATE=$(date -Iseconds)
1076+
1077+
# Domain and SSL configuration (used by docker-compose.yml)
1078+
DOMAIN=${host}
1079+
ACME_EMAIL=${ACME_EMAIL}
1080+
1081+
# Genesis configuration (critical for validator operation)
1082+
GENESIS_URL=${genesis_url}
10711083
GENESIS_BUCKET=${genesis_bucket}
10721084
GENESIS_PATH_PREFIX=${genesis_path_prefix}
1073-
GENESIS_URL=${genesis_url}
1074-
SHARDS=${num_shards}
1075-
XFS_PATH=${xfs_path:-N/A}
1076-
CACHE_SIZE=${cache_size}
1085+
1086+
# Validator configuration
1087+
VALIDATOR_PUBLIC_KEY=${public_key}
1088+
1089+
# Docker image
1090+
LINERA_IMAGE=${LINERA_IMAGE}
1091+
1092+
# ScyllaDB configuration
1093+
NUM_SHARDS=${num_shards}
1094+
${xfs_path:+XFS_PATH=${xfs_path}}
1095+
${xfs_path:+CACHE_SIZE=${cache_size}}
1096+
1097+
# Network configuration
1098+
FAUCET_PORT=8080
1099+
LINERA_STORAGE_SERVICE_PORT=1235
10771100
EOF
1078-
log DEBUG "Deployment info saved to: ${deployment_info}"
1101+
log INFO "Environment variables saved to ${env_file} for persistence across restarts"
10791102
}
10801103

10811104
# Run main function with all arguments
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env bash
2+
# Quick script to extract wallet/keys from Docker volumes
3+
# For operators who need their keys NOW
4+
5+
set -euo pipefail
6+
7+
echo "🔑 Extracting Validator Keys from Docker Volumes"
8+
echo "================================================"
9+
echo ""
10+
11+
# Method 1: Direct copy from running containers
12+
echo "Method 1: Checking running containers..."
13+
for container in proxy shard; do
14+
if docker exec ${container} ls /linera/*.json 2>/dev/null; then
15+
echo "✓ Found keys in ${container}, extracting..."
16+
docker cp ${container}:/linera/. ./validator-keys-${container}/ 2>/dev/null || true
17+
fi
18+
done
19+
20+
# Method 2: Mount volume to temporary container
21+
echo ""
22+
echo "Method 2: Checking Docker volumes..."
23+
for vol in $(docker volume ls -q | grep -E 'linera|scylla'); do
24+
echo "→ Inspecting volume: ${vol}"
25+
# Create temp container to access volume
26+
docker run --rm -v ${vol}:/backup -v $(pwd):/output alpine sh -c "
27+
if find /backup -name '*.json' -o -name 'wallet*' -o -name 'keystore*' 2>/dev/null; then
28+
echo ' ✓ Found potential key files'
29+
cp -r /backup/* /output/volume-${vol}/ 2>/dev/null || true
30+
fi
31+
" 2>/dev/null || true
32+
done
33+
34+
# Method 3: Direct volume path access (requires root)
35+
echo ""
36+
echo "Method 3: Direct volume access (may require sudo)..."
37+
DOCKER_ROOT="/var/lib/docker/volumes"
38+
if [ -d "${DOCKER_ROOT}" ]; then
39+
for vol_path in ${DOCKER_ROOT}/*/; do
40+
vol_name=$(basename ${vol_path})
41+
if [[ ${vol_name} == *"linera"* ]] || [[ ${vol_name} == *"scylla"* ]]; then
42+
echo "→ Checking ${vol_name}"
43+
sudo find ${vol_path} -name "*.json" -o -name "wallet*" -o -name "keystore*" 2>/dev/null | while read f; do
44+
echo " ✓ Found: ${f}"
45+
sudo cp -p "${f}" "./direct-${vol_name}-$(basename ${f})" 2>/dev/null || true
46+
done
47+
fi
48+
done
49+
fi
50+
51+
echo ""
52+
echo "🔍 Searching complete. Check current directory for extracted files:"
53+
ls -la *.json wallet* keystore* 2>/dev/null || echo "No key files found in standard locations"
54+
echo ""
55+
echo "⚠️ If no files were found, try:"
56+
echo " 1. docker exec proxy cat /linera/wallet.json > wallet.json"
57+
echo " 2. docker exec proxy cat /linera/keystore.json > keystore.json"
58+
echo " 3. Check if keys are in environment: docker exec proxy env | grep -i key"

0 commit comments

Comments
 (0)