Skip to content

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.

  • 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
  • Structured URLs: /images/2024/post-name.webp thay 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
  • 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
  • VPS/Server đã cài CloudPanel
  • Domain chính (vd: example.com)
  • Quyền root hoặc admin trên CloudPanel
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 logs

Bước 1: Tạo Subdomain trên CloudPanel

Section titled “Bước 1: Tạo Subdomain trên CloudPanel”
  1. Đăng nhập CloudPanel
  2. Vào DomainsAdd Domain
  3. Điền thông tin:
    • Domain: img.example.com
    • Root Directory: /var/www/img.example.com
    • PHP Version: Không cần (static only)

Tại DNS provider (CloudFlare/namecheap):

TypeNameValueTTL
AimgYOUR_SERVER_IP300
AAAAimgYOUR_IPV6 (optional)300

CloudPanel dùng Nginx. Chỉnh sửa config subdomain để tối ưu ả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;
}
Terminal window
# Symlink to sites-enabled
ln -sf /etc/nginx/sites-available/img.example.com.conf /etc/nginx/sites-enabled/
# Test config
nginx -t
# Reload nginx
systemctl reload nginx

Tạo script PHP/Python để resize ảnh on-the-fly và cache.

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 needed
if (!is_dir(dirname($cachePath))) {
mkdir(dirname($cachePath), 0755, true);
}
// Check cache
if (file_exists($cachePath) && filemtime($cachePath) > filemtime($sourcePath)) {
serveImage($cachePath, $format);
exit;
}
// Process image
processImage($sourcePath, $cachePath, $width, $height, $format, $config);
// Serve processed image
serveImage($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);
}

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”
Terminal window
cd /var/www/img.example.com
mkdir -p images thumbs cache logs
chmod -R 755 images thumbs cache
chown -R www-data:www-data .
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.ico

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']));
}
// Validate
if ($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 save
processUploadedImage($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);
}

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 srcset
const 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>
---
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>

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>

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>
<!-- 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">
Type: CNAME
Name: img
Target: img.example.com
Proxy: ON (Orange cloud)
URL: img.example.com/images/*
Settings:
- Cache Level: Cache Everything
- Edge Cache TTL: 1 month
- Browser Cache TTL: 1 month

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.jpg
location /images/ {
rewrite ^/images/(.*)$ /cdn-cgi/image/w=1920,q=85/images/$1 break;
proxy_pass https://img.example.com;
}
TaskStatus
✅ 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
Terminal window
# Fix permissions
chown -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/
Terminal window
# Install ImageMagick extension
apt-get install php-imagick
systemctl restart php8.2-fpm

Kiểm tra try_files directive và đảm bảo folder thumbs/ được tạo đúng.

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.