Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ COPY --from=builder /app/tsconfig.json ./tsconfig.json
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/sources ./sources
COPY --from=builder /app/prisma ./prisma

# Expose the port the app will run on
EXPOSE 3000
Expand Down
364 changes: 364 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
#!/bin/bash
set -e

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Helpers
info() { echo -e "${BLUE}$1${NC}"; }
success() { echo -e "${GREEN}✓ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; }
error() { echo -e "${RED}✗ $1${NC}"; }
prompt() { echo -en "${YELLOW}$1${NC}"; }

# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

echo ""
echo -e "${BLUE}═══════════════════════════════════════════${NC}"
echo -e "${BLUE} Happy Server Deployment Script ${NC}"
echo -e "${BLUE}═══════════════════════════════════════════${NC}"
echo ""

# ─────────────────────────────────────────────────────────────
# Environment Detection
# ─────────────────────────────────────────────────────────────

info "Detecting environment..."

# Check Docker
if ! command -v docker &> /dev/null; then
error "Docker is not installed"
exit 1
fi
success "Docker installed"

# Check docker compose
if docker compose version &> /dev/null; then
COMPOSE_CMD="docker compose"
success "Docker Compose installed"
elif command -v docker-compose &> /dev/null; then
COMPOSE_CMD="docker-compose"
success "Docker Compose (standalone) installed"
else
error "Docker Compose is not installed"
exit 1
fi

# Check system Caddy
SYSTEM_CADDY=false
if systemctl is-active --quiet caddy 2>/dev/null; then
SYSTEM_CADDY=true
success "System Caddy detected (active)"
elif command -v caddy &> /dev/null; then
SYSTEM_CADDY=true
success "System Caddy detected (installed)"
else
warn "No system Caddy - will use Docker Caddy"
fi

# Check existing PostgreSQL data
EXISTING_DATA=false
if docker volume inspect happy-server_postgres_data &> /dev/null; then
EXISTING_DATA=true
warn "Existing PostgreSQL data found"
else
info "No existing database data"
fi

# Check if app is running
APP_RUNNING=false
if $COMPOSE_CMD ps 2>/dev/null | grep -q "app.*Up"; then
APP_RUNNING=true
info "App is currently running"
fi

echo ""

# ─────────────────────────────────────────────────────────────
# Interactive Prompts
# ─────────────────────────────────────────────────────────────

# Domain
DEFAULT_DOMAIN=""
if [ -f /etc/caddy/Caddyfile ]; then
DEFAULT_DOMAIN=$(grep -oP '^\S+(?=\s*\{)' /etc/caddy/Caddyfile 2>/dev/null | head -1 || true)
fi
if [ -z "$DEFAULT_DOMAIN" ]; then
DEFAULT_DOMAIN=$(hostname -f 2>/dev/null || echo "localhost")
fi

prompt "Enter domain name [$DEFAULT_DOMAIN]: "
read -r DOMAIN
DOMAIN=${DOMAIN:-$DEFAULT_DOMAIN}
echo ""

# Existing data handling
RESET_DATA=false
if [ "$EXISTING_DATA" = true ]; then
echo "Existing database found. What would you like to do?"
echo " 1) Keep existing data (default)"
echo " 2) Reset everything (WARNING: destroys all data)"
prompt "Choice [1]: "
read -r DATA_CHOICE
if [ "$DATA_CHOICE" = "2" ]; then
prompt "Are you sure? Type 'yes' to confirm: "
read -r CONFIRM
if [ "$CONFIRM" = "yes" ]; then
RESET_DATA=true
warn "Will reset all data"
else
info "Keeping existing data"
fi
fi
echo ""
fi

# PostgreSQL password
echo "PostgreSQL password:"
echo " 1) Use default (postgres) - for development"
echo " 2) Enter custom password"
echo " 3) Generate random password"
prompt "Choice [1]: "
read -r PW_CHOICE

case "$PW_CHOICE" in
2)
prompt "Enter password: "
read -rs POSTGRES_PASSWORD
echo ""
;;
3)
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
info "Generated password: $POSTGRES_PASSWORD"
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated password is echoed to stdout in plaintext when option 3 is selected. This could be logged or visible in terminal history, creating a security risk. Consider displaying a message indicating the password has been saved to the .env file instead of displaying it directly.

Suggested change
info "Generated password: $POSTGRES_PASSWORD"
info "Generated a secure random password. It will be stored in the .env file and not displayed for security reasons."

Copilot uses AI. Check for mistakes.
;;
*)
POSTGRES_PASSWORD="postgres"
;;
esac
Comment on lines +123 to +143
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded default password "postgres" used in development may be exposed in production deployments if users select option 1. While the script warns "for development", this configuration is available in what appears to be a production deployment script. Consider either removing the default password option or adding a stronger warning that option 1 should never be used in production environments.

Copilot uses AI. Check for mistakes.
echo ""

# ─────────────────────────────────────────────────────────────
# Create/Update .env file
# ─────────────────────────────────────────────────────────────

info "Creating .env file..."

cat > .env <<EOF
# Generated by deploy.sh on $(date)
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
DATABASE_URL=postgresql://postgres:$POSTGRES_PASSWORD@postgres:5432/handy
DOMAIN=$DOMAIN
EOF

success ".env file created"

# Ensure .env is in .gitignore
if [ -f .gitignore ]; then
if ! grep -q "^\.env$" .gitignore; then
echo ".env" >> .gitignore
success "Added .env to .gitignore"
fi
else
echo ".env" > .gitignore
success "Created .gitignore with .env"
fi

# ─────────────────────────────────────────────────────────────
# Docker Deployment
# ─────────────────────────────────────────────────────────────

echo ""
info "Starting Docker deployment..."

# Reset if requested
if [ "$RESET_DATA" = true ]; then
warn "Stopping and removing all containers and volumes..."
$COMPOSE_CMD down -v --remove-orphans 2>/dev/null || true
fi

# Build app
info "Building app image..."
$COMPOSE_CMD build app

# Start infrastructure
info "Starting infrastructure (postgres, redis, minio)..."
$COMPOSE_CMD up -d postgres redis minio

# Wait for postgres to be healthy
info "Waiting for PostgreSQL to be ready..."
for i in {1..60}; do
if $COMPOSE_CMD exec -T postgres pg_isready -U postgres &> /dev/null; then
success "PostgreSQL is ready"
break
fi
if [ $i -eq 60 ]; then
error "PostgreSQL failed to start within 60 seconds"
exit 1
fi
sleep 1
done

# Fix PostgreSQL password
info "Configuring PostgreSQL password..."
$COMPOSE_CMD exec -T postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD '$POSTGRES_PASSWORD';" > /dev/null
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PostgreSQL password is interpolated directly into the shell command without proper escaping. If the password contains special characters (like $, `, ", ), the ALTER USER command could fail or behave unexpectedly. Consider using proper quoting or escaping for the password value.

Suggested change
$COMPOSE_CMD exec -T postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD '$POSTGRES_PASSWORD';" > /dev/null
ESCAPED_POSTGRES_PASSWORD=$(printf "%s" "$POSTGRES_PASSWORD" | sed "s/'/''/g")
$COMPOSE_CMD exec -T postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD '${ESCAPED_POSTGRES_PASSWORD}';" > /dev/null

Copilot uses AI. Check for mistakes.
success "PostgreSQL password configured"

# Wait for redis
info "Waiting for Redis..."
for i in {1..30}; do
if $COMPOSE_CMD exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; then
success "Redis is ready"
break
fi
if [ $i -eq 30 ]; then
error "Redis failed to start"
exit 1
fi
sleep 1
done

# Start minio-init and app
info "Starting application..."
$COMPOSE_CMD up -d

# Wait for app to be healthy
info "Waiting for app to be ready..."
for i in {1..30}; do
if curl -sf http://127.0.0.1:3005/health 2>/dev/null | grep -q '"status":"ok"'; then
success "App is healthy"
break
fi
if [ $i -eq 30 ]; then
error "App failed to start within 30 seconds"
echo ""
warn "Checking logs..."
$COMPOSE_CMD logs app --tail 20
exit 1
fi
sleep 1
done

# ─────────────────────────────────────────────────────────────
# Caddy Configuration
# ─────────────────────────────────────────────────────────────

echo ""
info "Configuring Caddy for HTTPS..."

if [ "$SYSTEM_CADDY" = true ]; then
# System Caddy
CADDYFILE="/etc/caddy/Caddyfile"

info "Updating $CADDYFILE..."
sudo tee "$CADDYFILE" > /dev/null <<EOF
$DOMAIN {
encode gzip
reverse_proxy 127.0.0.1:3005
}

www.$DOMAIN {
redir https://$DOMAIN{uri} permanent
}
EOF

info "Reloading Caddy..."
sudo systemctl reload caddy
success "System Caddy configured"
else
# Docker Caddy - create Caddyfile
cat > Caddyfile <<EOF
$DOMAIN {
encode gzip
reverse_proxy app:3005
}

www.$DOMAIN {
redir https://$DOMAIN{uri} permanent
}
EOF

# Check if caddy service exists in docker-compose
if ! grep -q "caddy:" docker-compose.yml; then
warn "Docker Caddy not in docker-compose.yml"
info "Please add Caddy service manually or install system Caddy"
else
$COMPOSE_CMD up -d caddy
success "Docker Caddy started"
fi
fi

# ─────────────────────────────────────────────────────────────
# Health Verification
# ─────────────────────────────────────────────────────────────

echo ""
info "Verifying deployment..."
echo ""

ERRORS=0

# Health endpoint
if curl -sf http://127.0.0.1:3005/health 2>/dev/null | grep -q '"status":"ok"'; then
success "Health endpoint: OK"
else
error "Health endpoint: FAILED"
((ERRORS++))
fi

# Root endpoint
if curl -sf http://127.0.0.1:3005/ &>/dev/null; then
success "Root endpoint: OK"
else
error "Root endpoint: FAILED"
((ERRORS++))
fi

# Socket.io
if curl -sf "http://127.0.0.1:3005/v1/updates/?EIO=4&transport=polling" 2>/dev/null | grep -q '"sid"'; then
success "Socket.io: OK"
else
error "Socket.io: FAILED"
((ERRORS++))
fi

# HTTPS (give Caddy time to get certificate)
sleep 2
if curl -sf "https://$DOMAIN/health" 2>/dev/null | grep -q '"status":"ok"'; then
success "HTTPS ($DOMAIN): OK"
else
warn "HTTPS ($DOMAIN): Not ready yet (certificate may still be provisioning)"
fi

echo ""

# ─────────────────────────────────────────────────────────────
# Summary
# ─────────────────────────────────────────────────────────────

if [ $ERRORS -eq 0 ]; then
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
echo -e "${GREEN} Deployment Successful! ${NC}"
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
else
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
echo -e "${YELLOW} Deployment completed with warnings ${NC}"
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
fi

echo ""
echo "Access URLs:"
echo " API: https://$DOMAIN"
echo " Health: https://$DOMAIN/health"
echo " Direct: http://$(hostname -I | awk '{print $1}'):3005"
echo ""

if [ "$POSTGRES_PASSWORD" != "postgres" ]; then
echo "Credentials saved in .env file"
echo ""
fi
Loading