616 lines
17 KiB
Docker
616 lines
17 KiB
Docker
# ========== 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"]
|