# ========== Multi-Stage Dockerfile for Hexo Blog Service ========== # This Dockerfile creates a secure, production-ready container for running # a Hexo blog with Nginx web server and SSH Git deployment capabilities. # Features: Non-root execution, environment-based user ID mapping, comprehensive logging # ---- Stage 1: Builder/Base ---- # Sets up templates, configurations, and scripts FROM ubuntu:22.04 AS builder ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Asia/Shanghai # Install build dependencies and configure locale RUN apt-get update && \ apt-get install -y --no-install-recommends \ locales \ git \ nginx-full \ gettext-base && \ # Configure Chinese locale sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen && \ locale-gen && \ update-locale LANG=zh_CN.UTF-8 && \ # Set timezone ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && \ echo $TZ > /etc/timezone && \ # Clean up rm -rf /var/lib/apt/lists/* # Create template directories RUN mkdir -p /etc/container/templates && \ mkdir -p /app # ========== IMPROVEMENT 1: Use Heredoc for Better Readability ========== # Create enhanced SSH configuration template with heredoc RUN cat << 'EOF' > /etc/container/templates/sshd_config.template # SSH Server Configuration Template # This template supports environment variable substitution Port ${SSH_PORT:-22} ListenAddress 0.0.0.0 ListenAddress :: # Authentication settings PermitRootLogin ${PERMIT_ROOT_LOGIN:-no} PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys PasswordAuthentication no ChallengeResponseAuthentication no UsePAM yes # Security settings AllowUsers ${SSH_USER:-hexo} X11Forwarding no PrintMotd no MaxAuthTries ${MAX_AUTH_TRIES:-3} ClientAliveInterval ${CLIENT_ALIVE_INTERVAL:-300} ClientAliveCountMax ${CLIENT_ALIVE_COUNT_MAX:-3} # Logging SyslogFacility AUTH LogLevel ${SSH_LOG_LEVEL:-INFO} # Subsystems Subsystem sftp /usr/lib/openssh/sftp-server # Environment AcceptEnv LANG LC_* EOF # Create Nginx configuration template with heredoc RUN cat << 'EOF' > /etc/container/templates/nginx.conf.template # Nginx Configuration Template # Optimized for security and performance user ${NGINX_USER:-hexo}; worker_processes ${NGINX_WORKERS:-auto}; pid /var/run/nginx.pid; # Error logging error_log /var/log/nginx/error.log ${NGINX_LOG_LEVEL:-warn}; events { worker_connections ${NGINX_CONNECTIONS:-1024}; use epoll; multi_accept on; } http { # Basic settings include /etc/nginx/mime.types; default_type application/octet-stream; # Logging log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; # Performance settings sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size ${MAX_UPLOAD_SIZE:-16m}; # Security headers add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; # Compression gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_min_length 1000; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/atom+xml image/svg+xml; # Rate limiting limit_req_zone $binary_remote_addr zone=general:10m rate=${RATE_LIMIT:-10r/s}; server { listen ${HTTP_PORT:-80}; server_name ${SERVER_NAME:-localhost}; root ${WEB_ROOT:-/home/www/hexo}; index index.html index.htm; # Security settings server_tokens off; # Rate limiting limit_req zone=general burst=${RATE_BURST:-20} nodelay; # Main location location / { try_files $uri $uri/ =404; } # Static assets with caching location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|eot|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; add_header Vary Accept-Encoding; } # Security: Deny access to hidden files location ~ /\. { deny all; } # Security: Deny access to sensitive files location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)$ { deny all; } } } EOF # ========== IMPROVEMENT 2: Enhanced Start Script with Log Rotation ========== # Create comprehensive start script with heredoc for better maintainability RUN cat << 'EOF' > /app/start.sh #!/bin/bash # Color codes for logging readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly PURPLE='\033[0;35m' readonly NC='\033[0m' # Logging configuration readonly LOG_DIR="/var/log/container" readonly LOG_FILE="$LOG_DIR/services.log" readonly MAX_LOG_SIZE=${MAX_LOG_SIZE:-10485760} # 10MB default readonly MAX_LOG_FILES=${MAX_LOG_FILES:-5} # Logging functions _log() { local level_color=$1 local level_name=$2 shift 2 echo -e "${level_color}[${level_name}]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $*" } log_info() { _log "$BLUE" "INFO" "$@"; } log_success() { _log "$GREEN" "SUCCESS" "$@"; } log_warning() { _log "$YELLOW" "WARNING" "$@"; } log_error() { _log "$RED" "ERROR" "$@"; } log_debug() { _log "$PURPLE" "DEBUG" "$@"; } # ========== LOG ROTATION IMPLEMENTATION ========== rotate_logs() { if [[ -f "$LOG_FILE" && $(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0) -gt $MAX_LOG_SIZE ]]; then log_info "Rotating log file (size: $(du -h "$LOG_FILE" 2>/dev/null | cut -f1))" # Rotate existing logs for i in $(seq $((MAX_LOG_FILES-1)) -1 1); do if [[ -f "${LOG_FILE}.$i" ]]; then mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i+1))" fi done # Move current log to .1 mv "$LOG_FILE" "${LOG_FILE}.1" touch "$LOG_FILE" log_success "Log rotation completed" fi } setup_logging() { mkdir -p "$LOG_DIR" touch "$LOG_FILE" # Rotate logs if needed rotate_logs log_info "Logging to console and $LOG_FILE" log_info "Log rotation: enabled (max size: ${MAX_LOG_SIZE} bytes, max files: ${MAX_LOG_FILES})" # Redirect all output to log file while keeping console output exec > >(tee -a "$LOG_FILE") 2> >(tee -a "$LOG_FILE" >&2) } # ========== USER MANAGEMENT WITH PUID/PGID SUPPORT ========== setup_user() { local target_uid=${PUID:-1000} local target_gid=${PGID:-1000} log_info "Setting up user with UID:$target_uid, GID:$target_gid" # Create group with specified GID if ! getent group hexo >/dev/null 2>&1; then if getent group "$target_gid" >/dev/null 2>&1; then log_warning "GID $target_gid already exists, using existing group" existing_group=$(getent group "$target_gid" | cut -d: -f1) log_info "Using existing group: $existing_group" else groupadd -g "$target_gid" hexo log_success "Created group 'hexo' with GID $target_gid" fi fi # Create user with specified UID if ! getent passwd hexo >/dev/null 2>&1; then if getent passwd "$target_uid" >/dev/null 2>&1; then log_error "UID $target_uid already exists" return 1 else useradd -u "$target_uid" -g "$target_gid" -d /home/hexo -s /bin/bash hexo log_success "Created user 'hexo' with UID $target_uid" fi else # User exists, check if UID needs updating current_uid=$(id -u hexo) if [[ "$current_uid" != "$target_uid" ]]; then log_info "Updating hexo user UID from $current_uid to $target_uid" usermod -u "$target_uid" hexo fi fi # Ensure home directory exists and has correct ownership mkdir -p /home/hexo/.ssh mkdir -p /home/www/hexo chown -R hexo:hexo /home/hexo /home/www/hexo chmod 700 /home/hexo/.ssh log_success "User setup completed" } render_config() { log_info "Rendering configuration templates..." local rendered=0 # Render SSH configuration if envsubst < /etc/container/templates/sshd_config.template > /etc/ssh/sshd_config; then log_success "SSH configuration rendered" ((rendered++)) else log_error "Failed to render SSH configuration" return 1 fi # Render Nginx configuration if envsubst < /etc/container/templates/nginx.conf.template > /etc/nginx/nginx.conf; then log_success "Nginx configuration rendered" ((rendered++)) else log_error "Failed to render Nginx configuration" return 1 fi log_success "All $rendered configuration files rendered successfully" return 0 } validate_configs() { log_info "Validating configurations..." # Validate SSH configuration if /usr/sbin/sshd -t; then log_success "SSH configuration is valid" else log_error "SSH configuration validation failed" return 1 fi # Validate Nginx configuration if nginx -t; then log_success "Nginx configuration is valid" else log_error "Nginx configuration validation failed" return 1 fi return 0 } start_services() { log_info "Starting services..." # Generate SSH host keys if they don't exist if [[ ! -f "/etc/ssh/ssh_host_rsa_key" ]]; then log_info "Generating SSH host keys..." ssh-keygen -A log_success "SSH host keys generated" fi # Start SSH service log_info "Starting SSH service..." /usr/sbin/sshd -D & SSH_PID=$! # Start Nginx service log_info "Starting Nginx service..." nginx -g "daemon off;" & NGINX_PID=$! # Wait for services to start sleep 3 # Verify services are running local services_ok=true if ! kill -0 $SSH_PID 2>/dev/null; then log_error "SSH service failed to start" services_ok=false else log_success "SSH service started (PID:$SSH_PID)" fi if ! kill -0 $NGINX_PID 2>/dev/null; then log_error "Nginx service failed to start" services_ok=false else log_success "Nginx service started (PID:$NGINX_PID)" fi if $services_ok; then log_success "All services started successfully" return 0 else log_error "Some services failed to start" return 1 fi } monitor_services() { log_info "Starting service monitoring and log rotation..." while true; do sleep 30 # Rotate logs if needed rotate_logs # Monitor SSH service if ! kill -0 $SSH_PID 2>/dev/null; then log_warning "SSH service stopped, attempting restart..." /usr/sbin/sshd -D & SSH_PID=$! if kill -0 $SSH_PID 2>/dev/null; then log_success "SSH service restarted (PID:$SSH_PID)" else log_error "Failed to restart SSH service" fi fi # Monitor Nginx service if ! kill -0 $NGINX_PID 2>/dev/null; then log_warning "Nginx service stopped, attempting restart..." nginx -g "daemon off;" & NGINX_PID=$! if kill -0 $NGINX_PID 2>/dev/null; then log_success "Nginx service restarted (PID:$NGINX_PID)" else log_error "Failed to restart Nginx service" fi fi done } cleanup() { log_info "Received shutdown signal, gracefully stopping services..." # Stop Nginx if [[ -n "$NGINX_PID" ]] && kill -0 $NGINX_PID 2>/dev/null; then log_info "Stopping Nginx (PID:$NGINX_PID)" kill -TERM $NGINX_PID wait $NGINX_PID 2>/dev/null log_success "Nginx stopped gracefully" fi # Stop SSH if [[ -n "$SSH_PID" ]] && kill -0 $SSH_PID 2>/dev/null; then log_info "Stopping SSH (PID:$SSH_PID)" kill -TERM $SSH_PID wait $SSH_PID 2>/dev/null log_success "SSH stopped gracefully" fi log_info "Container shutdown complete" exit 0 } # Signal handlers trap cleanup SIGTERM SIGINT # Main execution function main() { setup_logging log_info "========== Container Starting ==========" log_info "Timestamp: $(date)" log_info "Timezone: ${TZ:-UTC}" log_info "PUID: ${PUID:-1000}, PGID: ${PGID:-1000}" log_info "Current user: $(whoami)" # Setup user with PUID/PGID support if ! setup_user; then log_error "User setup failed" exit 1 fi # Render configuration templates if ! render_config; then log_error "Configuration rendering failed" exit 1 fi # Validate configurations if ! validate_configs; then log_error "Configuration validation failed" exit 1 fi # Start services if ! start_services; then log_error "Service startup failed" exit 1 fi log_success "========== All services started successfully ==========" log_info "Container is ready to serve requests" # Start monitoring loop monitor_services } # Execute main function main "$@" EOF chmod +x /app/start.sh # Create Git repository with enhanced security RUN git init --bare /home/hexo/hexo.git # ========== IMPROVEMENT 3: Enhanced Post-Receive Hook Security ========== RUN cat << 'EOF' > /home/hexo/hexo.git/hooks/post-receive #!/bin/bash # Enhanced Git post-receive hook with security checks # This hook deploys the website content after a git push LOG_FILE="/var/log/container/git-deploy.log" WORK_TREE="/home/www/hexo" GIT_DIR="/home/hexo/hexo.git" # Logging function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } # Security: Validate that we're running as the correct user if [[ "$(whoami)" != "hexo" ]]; then log "ERROR: Post-receive hook must run as 'hexo' user, currently: $(whoami)" exit 1 fi # Security: Validate paths if [[ ! -d "$WORK_TREE" ]]; then log "ERROR: Work tree directory does not exist: $WORK_TREE" exit 1 fi if [[ ! -d "$GIT_DIR" ]]; then log "ERROR: Git directory does not exist: $GIT_DIR" exit 1 fi log "Starting deployment..." log "Work tree: $WORK_TREE" log "Git directory: $GIT_DIR" # Perform the checkout if git --work-tree="$WORK_TREE" --git-dir="$GIT_DIR" checkout -f; then log "SUCCESS: Code deployed successfully" # Ensure correct permissions if chown -R hexo:hexo "$WORK_TREE"; then log "SUCCESS: Permissions updated" else log "WARNING: Failed to update permissions" fi # Optional: Reload Nginx if configuration changed if [[ -f "$WORK_TREE/nginx.conf" ]]; then log "INFO: Custom Nginx configuration detected, consider reloading" fi log "Deployment completed successfully" else log "ERROR: Deployment failed" exit 1 fi EOF chmod +x /home/hexo/hexo.git/hooks/post-receive # ---- Stage 2: Production ---- # ========== IMPROVEMENT 4: Remove Unnecessary Packages ========== FROM ubuntu:22.04 AS production ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Asia/Shanghai ENV PUID=1000 ENV PGID=1000 ENV LANG=zh_CN.UTF-8 # Copy timezone and locale configuration COPY --from=builder /etc/localtime /etc/localtime COPY --from=builder /etc/timezone /etc/timezone # Install ONLY runtime dependencies (removed vim, nodejs, npm) RUN apt-get update && \ apt-get install -y --no-install-recommends \ openssh-server \ git \ nginx-light \ gettext-base \ curl \ ca-certificates \ locales && \ # Configure locale locale-gen zh_CN.UTF-8 && \ update-locale LANG=zh_CN.UTF-8 && \ # Create necessary directories mkdir -p /var/run/sshd && \ mkdir -p /var/log/container && \ mkdir -p /var/log/nginx && \ mkdir -p /home/hexo/.ssh && \ mkdir -p /home/www/hexo && \ mkdir -p /home/www/ssl && \ # Clean up rm -rf /var/lib/apt/lists/* && \ rm -rf /tmp/* && \ rm -rf /var/tmp/* # Copy artifacts from builder stage COPY --from=builder /etc/container/templates/ /etc/container/templates/ COPY --from=builder /app/start.sh /app/start.sh COPY --from=builder /home/hexo/hexo.git/ /home/hexo/hexo.git/ # Set executable permissions RUN chmod +x /app/start.sh && \ chmod +x /home/hexo/hexo.git/hooks/post-receive # Health check with better error handling HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f -s -o /dev/null http://localhost:${HTTP_PORT:-80}/ || exit 1 # Document exposed ports EXPOSE 22 80 443 # Define volumes for persistent data VOLUME ["/home/www/hexo", "/home/hexo/.ssh", "/home/www/ssl", "/home/hexo/hexo.git", "/var/log/container", "/var/log/nginx"] # Use the enhanced start script CMD ["/app/start.sh"]