Image Server trên CloudPanel với Subdomain - Tối ưu & Chuẩn SEO
Image Server trên CloudPanel với Subdomain
Section titled “Image Server trên CloudPanel với Subdomain”Tách server ảnh ra subdomain riêng (vd: img.example.com) giúp tối ưu hiệu suất, dễ quản lý và cải thiện SEO. Bài viết này hướng dẫn cài đặt trên CloudPanel với các tối ưu tốt nhất.
Lợi ích của Image Server riêng
Section titled “Lợi ích của Image Server riêng”1. Hiệu suất tốt hơn
Section titled “1. Hiệu suất tốt hơn”- Parallel downloading: Browser tải đồng thợi từ nhiều domain
- Không gửi cookie: Subdomain tĩnh (static) không cần cookie
- Cache riêng: Cấu hình cache độc lập cho ảnh
- CDN dễ dàng: Tích hợp CloudFlare/KeyCDN chỉ cho ảnh
2. SEO benefits
Section titled “2. SEO benefits”- Structured URLs:
/images/2024/post-name.webpthay vì/wp-content/uploads/... - Dễ index: Google Image Search hiểu rõ cấu trúc
- Alt text tốt hơn: Kiểm soát hoàn toàn thuộc tính ảnh
- Lazy load hiệu quả: Không block render main domain
3. Quản lý dễ dàng
Section titled “3. Quản lý dễ dàng”- Backup riêng: Chỉ backup ảnh quan trọng
- Scaling: Mở rộng storage độc lập
- Version control: Thêm version vào filename
Chuẩn bị
Section titled “Chuẩn bị”Yêu cầu
Section titled “Yêu cầu”- VPS/Server đã cài CloudPanel
- Domain chính (vd:
example.com) - Quyền root hoặc admin trên CloudPanel
Plan cấu trúc
Section titled “Plan cấu trúc”Main site: example.com → Server A (CloudPanel main)Image server: img.example.com → Server A (CloudPanel subdomain) ↓ /var/www/img.example.com/ ├── images/ # Ảnh gốc ├── thumbs/ # Thumbnail tự động ├── cache/ # Cache Nginx └── logs/ # Access/error logsBước 1: Tạo Subdomain trên CloudPanel
Section titled “Bước 1: Tạo Subdomain trên CloudPanel”1.1 Thêm Domain con
Section titled “1.1 Thêm Domain con”- Đăng nhập CloudPanel
- Vào Domains → Add Domain
- Điền thông tin:
- Domain:
img.example.com - Root Directory:
/var/www/img.example.com - PHP Version: Không cần (static only)
- Domain:
1.2 Cấu hình DNS
Section titled “1.2 Cấu hình DNS”Tại DNS provider (CloudFlare/namecheap):
| Type | Name | Value | TTL |
|---|---|---|---|
| A | img | YOUR_SERVER_IP | 300 |
| AAAA | img | YOUR_IPV6 (optional) | 300 |
Bước 2: Cấu hình Nginx tối ưu
Section titled “Bước 2: Cấu hình Nginx tối ưu”CloudPanel dùng Nginx. Chỉnh sửa config subdomain để tối ưu ảnh.
2.1 File config chính
Section titled “2.1 File config chính”File: /etc/nginx/sites-available/img.example.com.conf
server { listen 80; listen [::]:80; server_name img.example.com;
# Redirect HTTP to HTTPS return 301 https://$server_name$request_uri;}
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name img.example.com;
root /var/www/img.example.com; index index.html;
# SSL certificates (CloudPanel auto-generates) ssl_certificate /etc/nginx/ssl/img.example.com.crt; ssl_certificate_key /etc/nginx/ssl/img.example.com.key;
# Security headers add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# CORS - Cho phép domain chính truy cập add_header Access-Control-Allow-Origin "https://example.com" always; add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
# Optimize for images location ~* \.(jpg|jpeg|png|gif|ico|webp|avif|svg)$ { # Cache static assets expires 1y; add_header Cache-Control "public, immutable" always; add_header Vary "Accept-Encoding" always;
# Remove cookies for static content add_header Set-Cookie "";
# Enable gzip for text-based images (SVG) gzip_static on;
# Image specific headers add_header X-Image-Optimized "true" always; }
# Serve original images location /images/ { alias /var/www/img.example.com/images/; try_files $uri $uri/ =404;
# Allow directory listing for debug (remove in production) # autoindex on; }
# Serve optimized thumbnails location /thumbs/ { alias /var/www/img.example.com/thumbs/; try_files $uri $uri/ =404; expires 1y; add_header Cache-Control "public, immutable" always; }
# Dynamic image resizing endpoint location ~ ^/resize/(\d+)x(\d+)/(.*)$ { set $width $1; set $height $2; set $filename $3;
# Check if already processed try_files /thumbs/${width}x${height}/$filename @image_processor;
expires 1y; add_header Cache-Control "public, immutable" always; }
# Image processing fallback (using Nginx + ImageMagick) location @image_processor { # Pass to backend script for processing # See PHP/Python script below try_files $uri =404; }
# Deny access to hidden files location ~ /\. { deny all; }
# Logging access_log /var/www/img.example.com/logs/access.log; error_log /var/www/img.example.com/logs/error.log;}2.2 Enable config
Section titled “2.2 Enable config”# Symlink to sites-enabledln -sf /etc/nginx/sites-available/img.example.com.conf /etc/nginx/sites-enabled/
# Test confignginx -t
# Reload nginxsystemctl reload nginxBước 3: Script xử lý ảnh động
Section titled “Bước 3: Script xử lý ảnh động”Tạo script PHP/Python để resize ảnh on-the-fly và cache.
3.1 PHP Image Processor
Section titled “3.1 PHP Image Processor”File: /var/www/img.example.com/resize.php
<?php/** * Dynamic Image Resizer with Caching * URL: /resize.php?w=800&h=600&src=images/photo.jpg */
// Configuration$config = [ 'source_dir' => __DIR__ . '/images/', 'cache_dir' => __DIR__ . '/thumbs/', 'allowed_sizes' => [ 'thumbnail' => [150, 150], 'small' => [300, 0], // 0 = auto height 'medium' => [600, 0], 'large' => [1200, 0], 'hero' => [1920, 0], ], 'quality' => [ 'jpg' => 85, 'webp' => 80, ], 'default_format' => 'webp',];
// Security: Validate source path$src = $_GET['src'] ?? '';$width = intval($_GET['w'] ?? 0);$height = intval($_GET['h'] ?? 0);$format = $_GET['format'] ?? $config['default_format'];
// Prevent directory traversal$src = str_replace(['..', '//'], '', $src);$sourcePath = realpath($config['source_dir'] . $src);
if (!$sourcePath || strpos($sourcePath, realpath($config['source_dir'])) !== 0) { http_response_code(404); exit('Invalid source');}
if (!file_exists($sourcePath)) { http_response_code(404); exit('Image not found');}
// Generate cache path$sizeKey = "{$width}x{$height}";$filename = pathinfo($src, PATHINFO_FILENAME);$cachePath = $config['cache_dir'] . $sizeKey . '/' . $filename . '.' . $format;
// Create cache directory if neededif (!is_dir(dirname($cachePath))) { mkdir(dirname($cachePath), 0755, true);}
// Check cacheif (file_exists($cachePath) && filemtime($cachePath) > filemtime($sourcePath)) { serveImage($cachePath, $format); exit;}
// Process imageprocessImage($sourcePath, $cachePath, $width, $height, $format, $config);
// Serve processed imageserveImage($cachePath, $format);
/** * Process and save image */function processImage($source, $destination, $width, $height, $format, $config) { $info = getimagesize($source); $mime = $info['mime'] ?? 'image/jpeg';
// Create source image switch ($mime) { case 'image/jpeg': $srcImg = imagecreatefromjpeg($source); break; case 'image/png': $srcImg = imagecreatefrompng($source); imagealphablending($srcImg, false); imagesavealpha($srcImg, true); break; case 'image/webp': $srcImg = imagecreatefromwebp($source); break; case 'image/gif': $srcImg = imagecreatefromgif($source); break; default: http_response_code(400); exit('Unsupported format'); }
$srcWidth = imagesx($srcImg); $srcHeight = imagesy($srcImg);
// Calculate dimensions maintaining aspect ratio if ($height == 0) { $height = intval($width * $srcHeight / $srcWidth); } elseif ($width == 0) { $width = intval($height * $srcWidth / $srcHeight); }
// Create destination image $dstImg = imagecreatetruecolor($width, $height);
// Preserve transparency for PNG/WebP if ($format === 'png' || $format === 'webp') { imagealphablending($dstImg, false); imagesavealpha($dstImg, true); $transparent = imagecolorallocatealpha($dstImg, 0, 0, 0, 127); imagefill($dstImg, 0, 0, $transparent); }
// Resize imagecopyresampled( $dstImg, $srcImg, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight );
// Save based on format switch ($format) { case 'webp': imagewebp($dstImg, $destination, $config['quality']['webp']); break; case 'jpeg': case 'jpg': imagejpeg($dstImg, $destination, $config['quality']['jpg']); break; case 'png': imagepng($dstImg, $destination, 6); break; }
// Cleanup imagedestroy($srcImg); imagedestroy($dstImg);}
/** * Serve image with proper headers */function serveImage($path, $format) { $mimeTypes = [ 'webp' => 'image/webp', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'png' => 'image/png', ];
$mime = $mimeTypes[$format] ?? 'image/jpeg';
header('Content-Type: ' . $mime); header('Content-Length: ' . filesize($path)); header('Cache-Control: public, max-age=31536000, immutable'); header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); header('Vary: Accept-Encoding');
readfile($path);}3.2 Update Nginx location
Section titled “3.2 Update Nginx location”Thêm vào Nginx config:
location ~ ^/resize/(\d+)x(\d+)/(.*)$ { try_files /thumbs/$1x$2/$3 @php_resize;}
location @php_resize { include /etc/nginx/fastcgi_params; fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root/resize.php; fastcgi_param QUERY_STRING "w=$1&h=$2&src=/images/$3&format=webp";}Bước 4: Cấu trúc thư mục & Upload
Section titled “Bước 4: Cấu trúc thư mục & Upload”4.1 Tạo folder structure
Section titled “4.1 Tạo folder structure”cd /var/www/img.example.commkdir -p images thumbs cache logschmod -R 755 images thumbs cachechown -R www-data:www-data .4.2 Naming convention chuẩn SEO
Section titled “4.2 Naming convention chuẩn SEO”images/├── 2024/│ ├── blog/│ │ ├── huong-dan-tao-image-server.webp│ │ └── toi-uu-hinh-anh-astro.webp│ └── products/│ ├── iphone-15-pro-max-xanh.webp│ └── macbook-air-m3-13-inch.webp├── authors/│ ├── duy-nguyen-avatar.webp│ └── team-photo-2024.webp└── icons/ ├── logo-192x192.png └── favicon.ico4.3 Script upload tự động tối ưu
Section titled “4.3 Script upload tự động tối ưu”File: /var/www/img.example.com/upload.php
<?php/** * Upload handler with automatic optimization */
$config = [ 'upload_dir' => __DIR__ . '/images/', 'max_size' => 10 * 1024 * 1024, // 10MB 'allowed_types' => ['image/jpeg', 'image/png', 'image/webp', 'image/gif'], 'convert_to' => 'webp', 'max_width' => 2048, 'quality' => 85,];
// Simple API key auth$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';if ($apiKey !== 'YOUR_SECRET_API_KEY') { http_response_code(401); exit(json_encode(['error' => 'Unauthorized']));}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit(json_encode(['error' => 'Method not allowed']));}
$file = $_FILES['image'] ?? null;if (!$file || $file['error'] !== UPLOAD_ERR_OK) { http_response_code(400); exit(json_encode(['error' => 'Upload failed']));}
// Validateif ($file['size'] > $config['max_size']) { http_response_code(400); exit(json_encode(['error' => 'File too large']));}
if (!in_array($file['type'], $config['allowed_types'])) { http_response_code(400); exit(json_encode(['error' => 'Invalid file type']));}
// Generate SEO-friendly filename$originalName = pathinfo($file['name'], PATHINFO_FILENAME);$seoName = generateSeoFilename($originalName);$subPath = date('Y') . '/' . ($_POST['category'] ?? 'general') . '/';$targetDir = $config['upload_dir'] . $subPath;
if (!is_dir($targetDir)) { mkdir($targetDir, 0755, true);}
$finalPath = $targetDir . $seoName . '.' . $config['convert_to'];
// Process and saveprocessUploadedImage($file['tmp_name'], $finalPath, $config);
// Generate URL$url = 'https://img.example.com/' . $subPath . $seoName . '.' . $config['convert_to'];
header('Content-Type: application/json');echo json_encode([ 'success' => true, 'url' => $url, 'path' => $subPath . $seoName . '.' . $config['convert_to'], 'sizes' => [ 'thumbnail' => '/resize/150x150/' . $subPath . $seoName . '.' . $config['convert_to'], 'small' => '/resize/300x0/' . $subPath . $seoName . '.' . $config['convert_to'], 'medium' => '/resize/600x0/' . $subPath . $seoName . '.' . $config['convert_to'], 'large' => '/resize/1200x0/' . $subPath . $seoName . '.' . $config['convert_to'], ]]);
function generateSeoFilename($name) { // Remove Vietnamese accents $accents = [ 'à','á','ạ','ả','ã','â','ầ','ấ','ậ','ẩ','ẫ','ă','ằ','ắ','ặ','ẳ','ẵ', 'è','é','ẹ','ẻ','ẽ','ê','ề','ế','ệ','ể','ễ', 'ì','í','ị','ỉ','ĩ', 'ò','ó','ọ','ỏ','õ','ô','ồ','ố','ộ','ổ','ỗ','ơ','ờ','ớ','ợ','ở','ỡ', 'ù','ú','ụ','ủ','ũ','ư','ừ','ứ','ự','ử','ữ', 'ỳ','ý','ỵ','ỷ','ỹ', 'đ', ]; $noAccents = [ 'a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a', 'e','e','e','e','e','e','e','e','e','e','e', 'i','i','i','i','i', 'o','o','o','o','o','o','o','o','o','o','o','o','o','o','o','o','o', 'u','u','u','u','u','u','u','u','u','u','u', 'y','y','y','y','y', 'd', ];
$name = str_replace($accents, $noAccents, mb_strtolower($name)); $name = preg_replace('/[^a-z0-9\s-]/', '', $name); $name = preg_replace('/\s+/', '-', trim($name)); $name = preg_replace('/-+/', '-', $name);
// Add timestamp for uniqueness return $name . '-' . time();}
function processUploadedImage($source, $destination, $config) { $info = getimagesize($source); $mime = $info['mime'];
switch ($mime) { case 'image/jpeg': $img = imagecreatefromjpeg($source); break; case 'image/png': $img = imagecreatefrompng($source); imagealphablending($img, false); imagesavealpha($img, true); break; case 'image/webp': $img = imagecreatefromwebp($source); break; case 'image/gif': $img = imagecreatefromgif($source); break; }
$width = imagesx($img); $height = imagesy($img);
// Resize if too large if ($width > $config['max_width']) { $newHeight = intval($height * $config['max_width'] / $width); $newImg = imagecreatetruecolor($config['max_width'], $newHeight);
if ($config['convert_to'] === 'webp') { imagealphablending($newImg, false); imagesavealpha($newImg, true); }
imagecopyresampled( $newImg, $img, 0, 0, 0, 0, $config['max_width'], $newHeight, $width, $height );
imagedestroy($img); $img = $newImg; }
// Save as WebP imagewebp($img, $destination, $config['quality']); imagedestroy($img);}Bước 5: Sử dụng trong Astro
Section titled “Bước 5: Sử dụng trong Astro”5.1 Image component tùy chỉnh
Section titled “5.1 Image component tùy chỉnh”File: src/components/OptimizedImage.astro
---export interface Props { src: string; // Relative path on image server alt: string; width?: number; height?: number; sizes?: string; loading?: 'eager' | 'lazy'; class?: string;}
const { src, alt, width = 800, height, sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 800px', loading = 'lazy', class: className,} = Astro.props;
const IMAGE_SERVER = 'https://img.example.com';
// Generate responsive srcsetconst widths = [320, 640, 960, 1280, 1920];const srcSet = widths .filter(w => w <= (width * 2)) .map(w => `${IMAGE_SERVER}/resize/${w}x0${src} ${w}w`) .join(', ');
const fallbackSrc = `${IMAGE_SERVER}/resize/${width}x0${src}`;---
<img src={fallbackSrc} srcset={srcSet} sizes={sizes} alt={alt} width={width} height={height} loading={loading} decoding="async" class={className}/>
<style> img { max-width: 100%; height: auto; }</style>5.2 Sử dụng trong bài viết
Section titled “5.2 Sử dụng trong bài viết”---import OptimizedImage from '../components/OptimizedImage.astro';---
<OptimizedImage src="/images/2024/blog/huong-dan-tao-image-server.webp" alt="Hướng dẫn tạo image server trên CloudPanel" width={1200} height={630} loading="eager"/>
<!-- Gallery với lazy load --><div class="gallery"> {[1, 2, 3, 4].map(i => ( <OptimizedImage src={`/images/2024/gallery/photo-${i}.webp`} alt={`Photo ${i}`} width={400} height={300} loading="lazy" /> ))}</div>Bước 6: Tối ưu SEO cho Images
Section titled “Bước 6: Tối ưu SEO cho Images”6.1 Image Sitemap
Section titled “6.1 Image Sitemap”File: /var/www/img.example.com/sitemap-images.xml
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"> <url> <loc>https://example.com/blog/bai-viet-1</loc> <image:image> <image:loc>https://img.example.com/images/2024/blog/bai-viet-1.webp</image:loc> <image:title>Tiêu đề ảnh chuẩn SEO</image:title> <image:caption>Mô tả chi tiết về nội dung ảnh</image:caption> <image:license>https://example.com/license</image:license> </image:image> </url></urlset>6.2 Structured Data (Schema.org)
Section titled “6.2 Structured Data (Schema.org)”Thêm vào HTML head:
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "ImageObject", "contentUrl": "https://img.example.com/images/2024/blog/bai-viet.webp", "description": "Mô tả chi tiết ảnh", "name": "Tiêu đề ảnh", "width": "1200", "height": "630", "author": { "@type": "Person", "name": "Tên tác giả" }}</script>6.3 Open Graph & Twitter Cards
Section titled “6.3 Open Graph & Twitter Cards”<!-- Primary Image --><meta property="og:image" content="https://img.example.com/resize/1200x630/images/2024/blog/bai-viet.webp"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:image:alt" content="Mô tả ảnh cho social media">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://img.example.com/resize/1200x600/images/2024/blog/bai-viet.webp">Bước 7: CloudFlare CDN Integration
Section titled “Bước 7: CloudFlare CDN Integration”7.1 Cấu hình DNS + Proxy
Section titled “7.1 Cấu hình DNS + Proxy”Type: CNAMEName: imgTarget: img.example.comProxy: ON (Orange cloud)7.2 Page Rules cho caching
Section titled “7.2 Page Rules cho caching”URL: img.example.com/images/*Settings: - Cache Level: Cache Everything - Edge Cache TTL: 1 month - Browser Cache TTL: 1 month7.3 Image Resizing (CloudFlare Pro+)
Section titled “7.3 Image Resizing (CloudFlare Pro+)”Nếu có CloudFlare Pro, bạn có thể dùng CloudFlare Images thay vì PHP processing:
# URL format: /cdn-cgi/image/w=800,h=600,q=80/images/photo.jpglocation /images/ { rewrite ^/images/(.*)$ /cdn-cgi/image/w=1920,q=85/images/$1 break; proxy_pass https://img.example.com;}Checklist hoàn thiện
Section titled “Checklist hoàn thiện”| Task | Status |
|---|---|
✅ Tạo subdomain img.example.com | |
| ✅ Cấu hình Nginx với cache headers | |
| ✅ Setup PHP image processor | |
| ✅ Tạo upload script với WebP conversion | |
| ✅ Cấu hình CORS cho domain chính | |
| ✅ SEO-friendly naming convention | |
| ✅ Generate responsive srcset | |
| ✅ Image sitemap | |
| ✅ CloudFlare CDN integration | |
| ✅ Lazy loading implementation |
Lỗi thường gặp
Section titled “Lỗi thường gặp””Access denied” khi upload
Section titled “”Access denied” khi upload”# Fix permissionschown -R www-data:www-data /var/www/img.example.com/chmod -R 755 /var/www/img.example.com/images/chmod -R 755 /var/www/img.example.com/thumbs/”Class ‘Imagick’ not found”
Section titled “”Class ‘Imagick’ not found””# Install ImageMagick extensionapt-get install php-imagicksystemctl restart php8.2-fpmNginx 404 for resized images
Section titled “Nginx 404 for resized images”Kiểm tra try_files directive và đảm bảo folder thumbs/ được tạo đúng.
Kết luận
Section titled “Kết luận”Với setup này, bạn có:
- ✅ Image server riêng biệt, scalable
- ✅ Tự động resize & convert WebP
- ✅ Cache hiệu quả (Nginx + CloudFlare)
- ✅ Chuẩn SEO đầy đủ (sitemap, schema, OG tags)
- ✅ Dễ dàng tích hợp với Astro/React/Vue
Dung lượng ảnh giảm 50-80%, thờ gian tải trang cải thiện đáng kể, và thứ hạng Google Image Search tốt hơn.