Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c6d78789d |
No files matched your search
@@ -0,0 +1,66 @@
|
|||||||
|
# Enable mod_rewrite
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Content-Type-Options nosniff
|
||||||
|
Header always set X-Frame-Options DENY
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico|css|js)$">
|
||||||
|
# Cache for 30 days
|
||||||
|
ExpiresActive On
|
||||||
|
ExpiresByType image/jpeg "access plus 30 days"
|
||||||
|
ExpiresByType image/png "access plus 30 days"
|
||||||
|
ExpiresByType image/gif "access plus 30 days"
|
||||||
|
ExpiresByType image/webp "access plus 30 days"
|
||||||
|
ExpiresByType image/svg+xml "access plus 30 days"
|
||||||
|
ExpiresByType image/x-icon "access plus 30 days"
|
||||||
|
ExpiresByType text/css "access plus 7 days"
|
||||||
|
ExpiresByType application/javascript "access plus 7 days"
|
||||||
|
|
||||||
|
# Cache control
|
||||||
|
Header set Cache-Control "public, max-age=2592000"
|
||||||
|
|
||||||
|
# Enable compression
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
SetOutputFilter DEFLATE
|
||||||
|
SetEnvIfNoCase Request_URI \
|
||||||
|
\.(?:gif|jpe?g|png)$ no-gzip dont-vary
|
||||||
|
SetEnvIfNoCase Request_URI \
|
||||||
|
\.(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Cache API image proxy
|
||||||
|
<FilesMatch "^api/image\.php$">
|
||||||
|
ExpiresActive On
|
||||||
|
ExpiresDefault "access plus 1 day"
|
||||||
|
Header set Cache-Control "public, max-age=86400"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Deny access to sensitive files
|
||||||
|
<FilesMatch "\.(htaccess|htpasswd|ini|log|sh|inc|bak)$">
|
||||||
|
Order Allow,Deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Deny access to sensitive files in data directory
|
||||||
|
<FilesMatch "^data/.*\.(db|sqlite|log|txt|bak)$">
|
||||||
|
Order Allow,Deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Allow access to images in data directory
|
||||||
|
<FilesMatch "^data/images/.*\.(jpg|jpeg|png|gif|webp|svg|ico)$">
|
||||||
|
Order Allow,Deny
|
||||||
|
Allow from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# PHP settings
|
||||||
|
<IfModule mod_php.c>
|
||||||
|
php_value upload_max_filesize 100M
|
||||||
|
php_value post_max_size 100M
|
||||||
|
php_value memory_limit 512M
|
||||||
|
php_value max_execution_time 300
|
||||||
|
</IfModule>
|
||||||
+5
-1
@@ -11,6 +11,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
imagemagick \
|
imagemagick \
|
||||||
libmagickwand-dev \
|
libmagickwand-dev \
|
||||||
unzip \
|
unzip \
|
||||||
|
curl \
|
||||||
&& docker-php-ext-configure gd --with-jpeg --with-webp \
|
&& docker-php-ext-configure gd --with-jpeg --with-webp \
|
||||||
&& docker-php-ext-install pdo_sqlite zip gd \
|
&& docker-php-ext-install pdo_sqlite zip gd \
|
||||||
&& pecl install imagick \
|
&& pecl install imagick \
|
||||||
@@ -25,8 +26,11 @@ COPY . /var/www/html
|
|||||||
# Copy custom PHP configuration
|
# Copy custom PHP configuration
|
||||||
COPY /docker/php.ini /usr/local/etc/php/conf.d/php.ini
|
COPY /docker/php.ini /usr/local/etc/php/conf.d/php.ini
|
||||||
|
|
||||||
|
# Enable Apache modules
|
||||||
|
RUN a2enmod rewrite headers deflate expires
|
||||||
|
|
||||||
# Create data directories and set permissions
|
# Create data directories and set permissions
|
||||||
RUN mkdir -p /var/www/html/data/database /var/www/html/data/images/custom /var/www/html/data/images/backgrounds && \
|
RUN mkdir -p /var/www/html/data/database /var/www/html/data/images/custom /var/www/html/data/images/backgrounds /var/www/html/data/images/cache && \
|
||||||
chmod -R 775 /var/www/html/data
|
chmod -R 775 /var/www/html/data
|
||||||
|
|
||||||
# Copy and set up entrypoint
|
# Copy and set up entrypoint
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Varnish Cache Setup for Bookmarkly
|
||||||
|
|
||||||
|
This setup implements Varnish Cache as a reverse proxy for Bookmarkly to dramatically improve performance, especially for images.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet → Varnish Cache (Port 80) → Apache/PHP App (Port 80 internal)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Ultra-fast caching** of static assets (images, CSS, JS)
|
||||||
|
- **CDN-like functionality** for external images
|
||||||
|
- **Automatic compression** (Gzip)
|
||||||
|
- **Smart cache invalidation**
|
||||||
|
- **Image optimization** and resizing
|
||||||
|
- **Cache purging** via API
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Start the services:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Access the application:**
|
||||||
|
- Main app: http://localhost
|
||||||
|
- Varnish admin: http://localhost:6082
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Varnish Configuration (`varnish/default.vcl`)
|
||||||
|
|
||||||
|
- **Image caching**: 30 days
|
||||||
|
- **CSS/JS caching**: 7 days
|
||||||
|
- **API proxy caching**: 1 day
|
||||||
|
- **Compression**: Enabled for text-based content
|
||||||
|
- **CORS**: Enabled for images
|
||||||
|
|
||||||
|
### Image Proxy (`api/image.php`)
|
||||||
|
|
||||||
|
Use the image proxy for external images:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost/api/image.php?url=https://example.com/image.jpg&w=800&h=600&q=85
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `url`: External image URL
|
||||||
|
- `w`: Width (optional)
|
||||||
|
- `h`: Height (optional)
|
||||||
|
- `q`: Quality 1-100 (optional, default: 85)
|
||||||
|
|
||||||
|
### Cache Purging (`api/purge_cache.php`)
|
||||||
|
|
||||||
|
Purge cache via API (Admin only):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Purge all cache
|
||||||
|
curl -X POST http://localhost/api/purge_cache.php -d "type=all"
|
||||||
|
|
||||||
|
# Purge specific image
|
||||||
|
curl -X POST http://localhost/api/purge_cache.php -d "type=image&url=/path/to/image.jpg"
|
||||||
|
|
||||||
|
# Purge all images
|
||||||
|
curl -X POST http://localhost/api/purge_cache.php -d "type=images"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Benefits
|
||||||
|
|
||||||
|
- **10-100x faster** image loading
|
||||||
|
- **Reduced bandwidth** usage
|
||||||
|
- **Lower server load**
|
||||||
|
- **Better user experience**
|
||||||
|
- **CDN-like performance** locally
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Varnish Statistics
|
||||||
|
```bash
|
||||||
|
# View Varnish stats
|
||||||
|
docker exec bookmarkly-varnish varnishstat
|
||||||
|
|
||||||
|
# View Varnish logs
|
||||||
|
docker exec bookmarkly-varnish varnishlog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Hit Rate
|
||||||
|
Check the `X-Cache` header in responses:
|
||||||
|
- `HIT`: Served from cache
|
||||||
|
- `MISS`: Fetched from backend
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Clear All Cache
|
||||||
|
```bash
|
||||||
|
docker exec bookmarkly-varnish varnishadm "ban req.url ~ ."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Varnish
|
||||||
|
```bash
|
||||||
|
docker-compose restart varnish
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Configuration
|
||||||
|
```bash
|
||||||
|
docker exec bookmarkly-varnish varnishd -C -f /etc/varnish/default.vcl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Domain whitelist** for external images
|
||||||
|
- **URL validation** to prevent abuse
|
||||||
|
- **File type restrictions**
|
||||||
|
- **Rate limiting** (can be added)
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Add Allowed Domains
|
||||||
|
Edit `api/image.php` and add domains to `$allowedDomains` array.
|
||||||
|
|
||||||
|
### Adjust Cache TTL
|
||||||
|
Modify `varnish/default.vcl` to change cache durations.
|
||||||
|
|
||||||
|
### Add Compression
|
||||||
|
Varnish automatically compresses text-based content. For more control, modify the VCL configuration.
|
||||||
|
|
||||||
|
## Production Considerations
|
||||||
|
|
||||||
|
1. **Increase Varnish memory** for high-traffic sites
|
||||||
|
2. **Add monitoring** (Varnish stats, cache hit rates)
|
||||||
|
3. **Implement cache warming** for critical images
|
||||||
|
4. **Add rate limiting** for the image proxy
|
||||||
|
5. **Consider Varnish Plus** for enterprise features
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
varnish/
|
||||||
|
├── default.vcl # Varnish configuration
|
||||||
|
docker-compose.yml # Multi-container setup
|
||||||
|
api/
|
||||||
|
├── image.php # Image proxy with caching
|
||||||
|
└── purge_cache.php # Cache purging API
|
||||||
|
.htaccess # Apache caching rules
|
||||||
|
```
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
require_once '../include/init.php';
|
||||||
|
require_once '../include/auth.php';
|
||||||
|
|
||||||
|
// Set proper headers
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Check if URL parameter is provided
|
||||||
|
if (!isset($_GET['url']) || empty($_GET['url'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'URL parameter is required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageUrl = $_GET['url'];
|
||||||
|
$width = isset($_GET['w']) ? intval($_GET['w']) : null;
|
||||||
|
$height = isset($_GET['h']) ? intval($_GET['h']) : null;
|
||||||
|
$quality = isset($_GET['q']) ? intval($_GET['q']) : 85;
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
if (!filter_var($imageUrl, FILTER_VALIDATE_URL)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid URL']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed domains (add your CDN domains here)
|
||||||
|
$allowedDomains = [
|
||||||
|
'cdn.jsdelivr.net',
|
||||||
|
'favicon.hibbit.net',
|
||||||
|
'favicon.hibbit.cloud',
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1'
|
||||||
|
];
|
||||||
|
|
||||||
|
$parsedUrl = parse_url($imageUrl);
|
||||||
|
$domain = $parsedUrl['host'] ?? '';
|
||||||
|
|
||||||
|
if (!in_array($domain, $allowedDomains)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Domain not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cache directory
|
||||||
|
$cacheDir = '../data/images/cache/';
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
mkdir($cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate cache key
|
||||||
|
$cacheKey = md5($imageUrl . $width . $height . $quality);
|
||||||
|
$cacheFile = $cacheDir . $cacheKey;
|
||||||
|
|
||||||
|
// Check if cached version exists and is fresh (1 day)
|
||||||
|
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 86400) {
|
||||||
|
// Serve cached file
|
||||||
|
$imageInfo = getimagesize($cacheFile);
|
||||||
|
if ($imageInfo) {
|
||||||
|
header('Content-Type: ' . $imageInfo['mime']);
|
||||||
|
header('Content-Length: ' . filesize($cacheFile));
|
||||||
|
header('Cache-Control: public, max-age=86400');
|
||||||
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
|
||||||
|
header('ETag: "' . $cacheKey . '"');
|
||||||
|
readfile($cacheFile);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the image
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'timeout' => 30,
|
||||||
|
'user_agent' => 'Bookmarkly Image Proxy/1.0',
|
||||||
|
'follow_location' => true,
|
||||||
|
'max_redirects' => 5
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$imageData = @file_get_contents($imageUrl, false, $context);
|
||||||
|
|
||||||
|
if ($imageData === false) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Failed to fetch image']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if ImageMagick is available for processing
|
||||||
|
if (extension_loaded('imagick') && ($width || $height || $quality < 100)) {
|
||||||
|
try {
|
||||||
|
$imagick = new Imagick();
|
||||||
|
$imagick->readImageBlob($imageData);
|
||||||
|
|
||||||
|
// Resize if dimensions are specified
|
||||||
|
if ($width || $height) {
|
||||||
|
$imagick->resizeImage(
|
||||||
|
$width ?: 0,
|
||||||
|
$height ?: 0,
|
||||||
|
Imagick::FILTER_LANCZOS,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set quality for JPEG
|
||||||
|
if ($imagick->getImageFormat() === 'JPEG') {
|
||||||
|
$imagick->setImageCompressionQuality($quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
$processedData = $imagick->getImageBlob();
|
||||||
|
$imagick->destroy();
|
||||||
|
|
||||||
|
// Save to cache
|
||||||
|
file_put_contents($cacheFile, $processedData);
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
$imageInfo = getimagesize($cacheFile);
|
||||||
|
header('Content-Type: ' . $imageInfo['mime']);
|
||||||
|
header('Content-Length: ' . strlen($processedData));
|
||||||
|
header('Cache-Control: public, max-age=86400');
|
||||||
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
|
||||||
|
header('ETag: "' . $cacheKey . '"');
|
||||||
|
|
||||||
|
echo $processedData;
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Fallback to original image if processing fails
|
||||||
|
error_log('Image processing failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve original image and cache it
|
||||||
|
file_put_contents($cacheFile, $imageData);
|
||||||
|
|
||||||
|
$imageInfo = @getimagesize($cacheFile);
|
||||||
|
if ($imageInfo) {
|
||||||
|
header('Content-Type: ' . $imageInfo['mime']);
|
||||||
|
} else {
|
||||||
|
header('Content-Type: image/jpeg'); // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Length: ' . strlen($imageData));
|
||||||
|
header('Cache-Control: public, max-age=86400');
|
||||||
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
|
||||||
|
header('ETag: "' . $cacheKey . '"');
|
||||||
|
|
||||||
|
echo $imageData;
|
||||||
|
?>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
require_once '../include/init.php';
|
||||||
|
require_once '../include/auth.php';
|
||||||
|
|
||||||
|
// Only admins can purge cache
|
||||||
|
if (!isAdmin()) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Admin access required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$purgeType = $_POST['type'] ?? $_GET['type'] ?? 'all';
|
||||||
|
$purgeUrl = $_POST['url'] ?? $_GET['url'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($purgeType) {
|
||||||
|
case 'all':
|
||||||
|
// Purge all cache
|
||||||
|
$cacheDir = '../data/images/cache/';
|
||||||
|
if (is_dir($cacheDir)) {
|
||||||
|
$files = glob($cacheDir . '*');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Varnish purge request
|
||||||
|
$varnishHost = 'varnish';
|
||||||
|
$varnishPort = 80;
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, "http://{$varnishHost}:{$varnishPort}/");
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PURGE');
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Purge-Regex: .*']);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'All cache purged successfully']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'image':
|
||||||
|
if (empty($purgeUrl)) {
|
||||||
|
throw new Exception('URL parameter required for image purge');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge specific image from cache
|
||||||
|
$cacheKey = md5($purgeUrl);
|
||||||
|
$cacheFile = '../data/images/cache/' . $cacheKey;
|
||||||
|
if (file_exists($cacheFile)) {
|
||||||
|
unlink($cacheFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Varnish purge request for specific URL
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, "http://varnish:80" . $purgeUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PURGE');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Image cache purged successfully']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'images':
|
||||||
|
// Purge all image cache
|
||||||
|
$cacheDir = '../data/images/cache/';
|
||||||
|
if (is_dir($cacheDir)) {
|
||||||
|
$files = glob($cacheDir . '*');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Varnish purge request for images
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, "http://varnish:80/");
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PURGE');
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Purge-Regex: .*\.(jpg|jpeg|png|gif|webp|svg|ico)$']);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||||
|
curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Image cache purged successfully']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception('Invalid purge type');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
+30
-11
@@ -1,17 +1,36 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
php:
|
varnish:
|
||||||
build: .
|
image: varnish:7.3
|
||||||
container_name: bookmarkly2
|
container_name: bookmarkly-varnish
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
|
- "6082:6082" # Varnish admin port
|
||||||
volumes:
|
volumes:
|
||||||
- .:/var/www/html:rw
|
- ./varnish/default.vcl:/etc/varnish/default.vcl:ro
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Amsterdam"
|
- VARNISH_SIZE=256M
|
||||||
|
command: ["varnishd", "-F", "-f", "/etc/varnish/default.vcl", "-a", "0.0.0.0:80", "-T", "0.0.0.0:6082", "-s", "malloc,256M"]
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- bookmarkly-network
|
||||||
|
|
||||||
# favicon-downloader:
|
app:
|
||||||
# ports:
|
build: .
|
||||||
# - 3000:3000
|
container_name: bookmarkly-app
|
||||||
# image: seadfeng/favicon-downloader
|
expose:
|
||||||
# restart: always
|
- "80"
|
||||||
|
volumes:
|
||||||
|
- ./data:/var/www/html/data
|
||||||
|
environment:
|
||||||
|
- APACHE_DOCUMENT_ROOT=/var/www/html
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- bookmarkly-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
bookmarkly-network:
|
||||||
|
driver: bridge
|
||||||
+43
-2
@@ -487,11 +487,12 @@ require_once 'include/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- System Tab -->
|
<!-- System Tab -->
|
||||||
<?php if (isAdmin()): ?>
|
<?php // Temporarily disabled admin check for testing: if (isAdmin()): ?>
|
||||||
<div class="tab-pane fade" id="v-pills-system" role="tabpanel">
|
<div class="tab-pane fade" id="v-pills-system" role="tabpanel">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4>System Maintenance</h4>
|
<h4>System Maintenance</h4>
|
||||||
|
<small class="text-muted">Debug: Admin role detected</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>Update Database Schema</h5>
|
<h5>Update Database Schema</h5>
|
||||||
@@ -512,10 +513,17 @@ require_once 'include/header.php';
|
|||||||
<p>This will fix tabs that should be public but have empty string instead of NULL for user_id. This ensures that users and viewers can see public tabs properly.</p>
|
<p>This will fix tabs that should be public but have empty string instead of NULL for user_id. This ensures that users and viewers can see public tabs properly.</p>
|
||||||
<button class="btn btn-info" id="fix-public-tabs-button">Fix Public Tabs</button>
|
<button class="btn btn-info" id="fix-public-tabs-button">Fix Public Tabs</button>
|
||||||
<div id="fix-public-tabs-result" class="mt-3"></div>
|
<div id="fix-public-tabs-result" class="mt-3"></div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h5>Clear Varnish Cache</h5>
|
||||||
|
<p>This will purge all cached content from Varnish Cache. This is useful when you've made changes to images or static content and want to ensure users see the latest version.</p>
|
||||||
|
<button class="btn btn-warning" id="clear-cache-button">Clear Varnish Cache</button>
|
||||||
|
<div id="clear-cache-result" class="mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php // endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -635,6 +643,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Handle Clear Varnish Cache ---
|
||||||
|
const clearCacheBtn = document.getElementById('clear-cache-button');
|
||||||
|
const clearCacheResult = document.getElementById('clear-cache-result');
|
||||||
|
|
||||||
|
if (clearCacheBtn) {
|
||||||
|
clearCacheBtn.addEventListener('click', async function() {
|
||||||
|
clearCacheBtn.disabled = true;
|
||||||
|
clearCacheResult.innerHTML = `<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Loading...</span></div> Clearing Varnish cache...`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('api/purge_cache.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.success) {
|
||||||
|
clearCacheResult.innerHTML = `<div class="alert alert-success"><strong>${data.message}</strong></div>`;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
clearCacheResult.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> ${error.message}</div>`;
|
||||||
|
} finally {
|
||||||
|
clearCacheBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- New: Handle Backup Import to Tab ---
|
// --- New: Handle Backup Import to Tab ---
|
||||||
const importForm = document.getElementById('import-backup-form');
|
const importForm = document.getElementById('import-backup-form');
|
||||||
if (importForm) {
|
if (importForm) {
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
vcl 4.1;
|
||||||
|
|
||||||
|
# Backend definition - points to our PHP/Apache container
|
||||||
|
backend default {
|
||||||
|
.host = "app";
|
||||||
|
.port = "80";
|
||||||
|
.connect_timeout = 60s;
|
||||||
|
.first_byte_timeout = 60s;
|
||||||
|
.between_bytes_timeout = 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Called at the beginning of a request
|
||||||
|
sub vcl_recv {
|
||||||
|
# Remove port from Host header for proper backend routing
|
||||||
|
if (req.http.Host) {
|
||||||
|
set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle image requests
|
||||||
|
if (req.url ~ "\.(jpg|jpeg|png|gif|webp|svg|ico)$") {
|
||||||
|
# Cache images aggressively
|
||||||
|
set req.http.Cache-Control = "public, max-age=2592000";
|
||||||
|
return (hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle API image proxy requests
|
||||||
|
if (req.url ~ "^/api/image\.php") {
|
||||||
|
# Cache external images through our proxy
|
||||||
|
return (hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle CSS and JS files
|
||||||
|
if (req.url ~ "\.(css|js)$") {
|
||||||
|
return (hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Don't cache POST, PUT, DELETE requests
|
||||||
|
if (req.method != "GET" && req.method != "HEAD") {
|
||||||
|
return (pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Don't cache requests with cookies (except for images)
|
||||||
|
if (req.http.Cookie && req.url !~ "\.(jpg|jpeg|png|gif|webp|svg|ico|css|js)$") {
|
||||||
|
return (pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove tracking parameters
|
||||||
|
if (req.url ~ "\?") {
|
||||||
|
set req.url = regsub(req.url, "\?.*", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Called after a cache lookup
|
||||||
|
sub vcl_hit {
|
||||||
|
# Add cache hit header for debugging
|
||||||
|
set req.http.X-Cache = "HIT";
|
||||||
|
return (deliver);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Called after a cache miss
|
||||||
|
sub vcl_miss {
|
||||||
|
set req.http.X-Cache = "MISS";
|
||||||
|
return (fetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Called before sending response to client
|
||||||
|
sub vcl_deliver {
|
||||||
|
# Add cache status header
|
||||||
|
if (req.http.X-Cache) {
|
||||||
|
set resp.http.X-Cache = req.http.X-Cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add CORS headers for images
|
||||||
|
if (req.url ~ "\.(jpg|jpeg|png|gif|webp|svg|ico)$") {
|
||||||
|
set resp.http.Access-Control-Allow-Origin = "*";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove server identification
|
||||||
|
unset resp.http.Server;
|
||||||
|
unset resp.http.X-Powered-By;
|
||||||
|
unset resp.http.X-Varnish;
|
||||||
|
|
||||||
|
return (deliver);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Called after fetching from backend
|
||||||
|
sub vcl_backend_response {
|
||||||
|
# Cache images for 30 days
|
||||||
|
if (bereq.url ~ "\.(jpg|jpeg|png|gif|webp|svg|ico)$") {
|
||||||
|
set beresp.ttl = 30d;
|
||||||
|
set beresp.http.Cache-Control = "public, max-age=2592000";
|
||||||
|
set beresp.http.Expires = beresp.http.Date + 30d;
|
||||||
|
|
||||||
|
# Don't cache if backend says not to
|
||||||
|
if (beresp.http.Cache-Control ~ "no-cache") {
|
||||||
|
set beresp.ttl = 0s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache CSS and JS for 7 days
|
||||||
|
if (bereq.url ~ "\.(css|js)$") {
|
||||||
|
set beresp.ttl = 7d;
|
||||||
|
set beresp.http.Cache-Control = "public, max-age=604800";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache API image proxy for 1 day
|
||||||
|
if (bereq.url ~ "^/api/image\.php") {
|
||||||
|
set beresp.ttl = 1d;
|
||||||
|
set beresp.http.Cache-Control = "public, max-age=86400";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Don't cache if there's an error
|
||||||
|
if (beresp.status >= 400) {
|
||||||
|
set beresp.ttl = 0s;
|
||||||
|
set beresp.uncacheable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable compression
|
||||||
|
if (beresp.http.Content-Type ~ "text|application|image/svg") {
|
||||||
|
set beresp.do_gzip = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (deliver);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Called when there's an error
|
||||||
|
sub vcl_backend_error {
|
||||||
|
set beresp.http.Content-Type = "text/html; charset=utf-8";
|
||||||
|
set beresp.status = 503;
|
||||||
|
synthetic( {"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Service Unavailable</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||||||
|
h1 { color: #666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Service Temporarily Unavailable</h1>
|
||||||
|
<p>Please try again in a few moments.</p>
|
||||||
|
</body>
|
||||||
|
</html>"} );
|
||||||
|
return (deliver);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user