| 1 |
#!/bin/bash |
| 2 |
|
| 3 |
set -euo pipefail |
| 4 |
|
| 5 |
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 6 |
STACK_NAME="zephyrfs" |
| 7 |
COMPOSE_FILE="$DEPLOY_DIR/deploy/production.yml" |
| 8 |
ENV_FILE="$DEPLOY_DIR/.env.production" |
| 9 |
|
| 10 |
log() { |
| 11 |
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" |
| 12 |
} |
| 13 |
|
| 14 |
error() { |
| 15 |
log "ERROR: $*" >&2 |
| 16 |
exit 1 |
| 17 |
} |
| 18 |
|
| 19 |
check_prerequisites() { |
| 20 |
log "Checking prerequisites..." |
| 21 |
|
| 22 |
command -v docker >/dev/null 2>&1 || error "Docker is not installed" |
| 23 |
command -v docker-compose >/dev/null 2>&1 || error "Docker Compose is not installed" |
| 24 |
|
| 25 |
if ! docker info >/dev/null 2>&1; then |
| 26 |
error "Docker daemon is not running" |
| 27 |
fi |
| 28 |
|
| 29 |
if ! docker swarm ca >/dev/null 2>&1; then |
| 30 |
log "Initializing Docker Swarm..." |
| 31 |
docker swarm init |
| 32 |
fi |
| 33 |
|
| 34 |
if [[ ! -f "$ENV_FILE" ]]; then |
| 35 |
error "Environment file not found: $ENV_FILE" |
| 36 |
fi |
| 37 |
|
| 38 |
log "Prerequisites check passed" |
| 39 |
} |
| 40 |
|
| 41 |
build_images() { |
| 42 |
log "Building Docker images..." |
| 43 |
|
| 44 |
# Build client image |
| 45 |
docker build -t zephyrfs/web-client:latest \ |
| 46 |
-f "$DEPLOY_DIR/client/Dockerfile" \ |
| 47 |
"$DEPLOY_DIR/client" |
| 48 |
|
| 49 |
# Build server image |
| 50 |
docker build -t zephyrfs/web-server:latest \ |
| 51 |
-f "$DEPLOY_DIR/server/Dockerfile" \ |
| 52 |
"$DEPLOY_DIR/server" |
| 53 |
|
| 54 |
log "Images built successfully" |
| 55 |
} |
| 56 |
|
| 57 |
setup_secrets() { |
| 58 |
log "Setting up Docker secrets..." |
| 59 |
|
| 60 |
if ! docker secret inspect jwt_secret >/dev/null 2>&1; then |
| 61 |
if [[ -n "${JWT_SECRET:-}" ]]; then |
| 62 |
echo "$JWT_SECRET" | docker secret create jwt_secret - |
| 63 |
else |
| 64 |
openssl rand -base64 32 | docker secret create jwt_secret - |
| 65 |
fi |
| 66 |
log "JWT secret created" |
| 67 |
fi |
| 68 |
} |
| 69 |
|
| 70 |
setup_volumes() { |
| 71 |
log "Setting up volumes and directories..." |
| 72 |
|
| 73 |
local data_path="${DATA_PATH:-/opt/zephyrfs/data}" |
| 74 |
local ssl_path="${SSL_CERTS_PATH:-/opt/zephyrfs/ssl}" |
| 75 |
|
| 76 |
sudo mkdir -p "$data_path" "$ssl_path" |
| 77 |
sudo chown -R 1000:1000 "$data_path" |
| 78 |
|
| 79 |
log "Volumes configured" |
| 80 |
} |
| 81 |
|
| 82 |
deploy_stack() { |
| 83 |
log "Deploying Docker stack..." |
| 84 |
|
| 85 |
docker stack deploy \ |
| 86 |
--compose-file "$COMPOSE_FILE" \ |
| 87 |
--with-registry-auth \ |
| 88 |
"$STACK_NAME" |
| 89 |
|
| 90 |
log "Stack deployment initiated" |
| 91 |
} |
| 92 |
|
| 93 |
wait_for_services() { |
| 94 |
log "Waiting for services to be ready..." |
| 95 |
|
| 96 |
local max_wait=300 |
| 97 |
local wait_time=0 |
| 98 |
|
| 99 |
while [[ $wait_time -lt $max_wait ]]; do |
| 100 |
if docker service ls --filter name="${STACK_NAME}_" --format "{{.Replicas}}" | grep -q "0/"; then |
| 101 |
log "Services still starting... (${wait_time}s)" |
| 102 |
sleep 10 |
| 103 |
wait_time=$((wait_time + 10)) |
| 104 |
else |
| 105 |
log "All services are running" |
| 106 |
return 0 |
| 107 |
fi |
| 108 |
done |
| 109 |
|
| 110 |
error "Services failed to start within ${max_wait} seconds" |
| 111 |
} |
| 112 |
|
| 113 |
health_check() { |
| 114 |
log "Performing health checks..." |
| 115 |
|
| 116 |
local endpoints=( |
| 117 |
"http://localhost/api/health" |
| 118 |
"http://localhost:9090/-/healthy" |
| 119 |
"http://localhost:3001/api/health" |
| 120 |
) |
| 121 |
|
| 122 |
for endpoint in "${endpoints[@]}"; do |
| 123 |
if curl -f -s "$endpoint" >/dev/null; then |
| 124 |
log "Health check passed: $endpoint" |
| 125 |
else |
| 126 |
log "WARNING: Health check failed: $endpoint" |
| 127 |
fi |
| 128 |
done |
| 129 |
} |
| 130 |
|
| 131 |
update_stack() { |
| 132 |
log "Performing rolling update..." |
| 133 |
|
| 134 |
# Update images |
| 135 |
build_images |
| 136 |
|
| 137 |
# Deploy with updated images |
| 138 |
deploy_stack |
| 139 |
|
| 140 |
# Wait for rolling update to complete |
| 141 |
wait_for_services |
| 142 |
|
| 143 |
log "Rolling update completed" |
| 144 |
} |
| 145 |
|
| 146 |
rollback_stack() { |
| 147 |
log "Rolling back to previous version..." |
| 148 |
|
| 149 |
docker service rollback "${STACK_NAME}_web-client" |
| 150 |
docker service rollback "${STACK_NAME}_web-server" |
| 151 |
|
| 152 |
wait_for_services |
| 153 |
|
| 154 |
log "Rollback completed" |
| 155 |
} |
| 156 |
|
| 157 |
cleanup() { |
| 158 |
log "Cleaning up unused resources..." |
| 159 |
|
| 160 |
docker system prune -f |
| 161 |
docker volume prune -f |
| 162 |
|
| 163 |
log "Cleanup completed" |
| 164 |
} |
| 165 |
|
| 166 |
main() { |
| 167 |
local action="${1:-deploy}" |
| 168 |
|
| 169 |
case "$action" in |
| 170 |
deploy) |
| 171 |
check_prerequisites |
| 172 |
build_images |
| 173 |
setup_secrets |
| 174 |
setup_volumes |
| 175 |
deploy_stack |
| 176 |
wait_for_services |
| 177 |
health_check |
| 178 |
;; |
| 179 |
update) |
| 180 |
check_prerequisites |
| 181 |
update_stack |
| 182 |
health_check |
| 183 |
;; |
| 184 |
rollback) |
| 185 |
check_prerequisites |
| 186 |
rollback_stack |
| 187 |
health_check |
| 188 |
;; |
| 189 |
cleanup) |
| 190 |
cleanup |
| 191 |
;; |
| 192 |
*) |
| 193 |
echo "Usage: $0 {deploy|update|rollback|cleanup}" |
| 194 |
echo "" |
| 195 |
echo "Commands:" |
| 196 |
echo " deploy - Initial deployment or full redeploy" |
| 197 |
echo " update - Rolling update with zero downtime" |
| 198 |
echo " rollback - Rollback to previous version" |
| 199 |
echo " cleanup - Clean up unused Docker resources" |
| 200 |
exit 1 |
| 201 |
;; |
| 202 |
esac |
| 203 |
|
| 204 |
log "Operation '$action' completed successfully" |
| 205 |
} |
| 206 |
|
| 207 |
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then |
| 208 |
main "$@" |
| 209 |
fi |