') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - abukamila/docker-webstack: web dev stack with preset nginx, php & mariadb · GitHub
Skip to content

Repository files navigation

Docker Webstack

Production-grade Docker Compose stack untuk hosting multiple PHP projects pada satu server.

Target host: 24 CPU, 96GB RAM (AMD EPYC 7282)

Arsitektur

 ┌─────────────┐
:80 │ Nginx │
───────────────►│ (Alpine) │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ PHP 8.4 │ │ RustFS │ │Gotenberg │
│ FPM │ │ (S3) │ │ (PDF) │
│ :9000 │ │:9000/9001│ │ :3000 │
└────┬─────┘ └──────────┘ └──────────┘
│
┌─────┴─────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ MySQL │ │ Redis │
│ 8.4 │ │ (Alpine) │
│ :3306 │ │ :6379 │
└──────────┘ └──────────┘

Services

ServiceImagePort(s)Fungsi
Nginxnginx:alpine80Reverse proxy & static files
PHP 8.4Custom (FPM)9000PHP application server
MySQLmysql:8.43306Database utama
Redisredis:alpine6379Cache & session store
RustFSrustfs/rustfs9000, 9001S3-compatible object storage
Gotenberggotenberg3000HTML-to-PDF conversion

Struktur Direktori

docker-webstack/
├── docker-compose.yml
├── .env # Credentials (gitignored)
├── docker/
│ ├── nginx/
│ │ ├── nginx.conf # Config utama Nginx
│ │ └── conf.d/
│ │ ├── optimization.conf # Gzip, buffers, caching
│ │ └── rustfs.conf # Proxy ke RustFS console & S3 API
│ ├── php/8.4/
│ │ ├── Dockerfile # PHP extensions & Composer
│ │ ├── php.ini # OPcache, JIT, session via Redis
│ │ ├── www.conf # FPM pool (dynamic, max 200 workers)
│ │ └── zz-docker.conf # FPM daemon config
│ ├── mysql/
│ │ └── my.cnf # InnoDB tuning untuk 96GB RAM
│ └── redis/
│ └── redis.conf # 4GB maxmemory, AOF + RDB persistence
├── data/ # Volume data (gitignored)
│ ├── mysql/
│ ├── redis/
│ └── rustfs/
└── projects/ # PHP projects (gitignored)
└── <project>/
└── nginx.conf # Vhost auto-loaded oleh Nginx

Setup

1. Clone & Configure

git clone <repo-url>&&cd docker-webstack

Buat .env dari contoh:

cat > .env << 'EOF'MYSQL_ROOT_PASSWORD=<password>MYSQL_DATABASE=<database>MYSQL_USER=<user>MYSQL_PASSWORD=<password>RUSTFS_ACCESS_KEY=<access_key>RUSTFS_SECRET_KEY=<secret_key>EOF

2. Start Services

docker compose up -d

3. Tambah Project Baru

Letakkan project PHP dalam projects/<nama-project>/. Buat nginx.conf di root project tersebut — ia akan di-load secara automatik oleh Nginx.

Contoh vhost (projects/example.com/nginx.conf):

server{listen80;server_name example.com;root /var/www/projects/example.com/public;indexindex.php;location / {try_files$uri$uri/ /index.php?$query_string;}location~\.php$ {fastcgi_pass php84:9000;fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;include fastcgi_params;}}

Kemudian reload Nginx:

docker compose exec nginx nginx -s reload

Konfigurasi Utama

PHP 8.4 FPM

  • OPcache enabled dengan JIT tracing (256MB buffer)
  • Session disimpan dalam Redis (tcp://redis:6379)
  • FPM Pool: dynamic, 20 start / max 200 workers
  • Extensions: pdo_mysql, redis, gd, intl, zip, bcmath, pcntl, sockets, exif, opcache
  • Upload limit: 256MB

MySQL 8.4

  • InnoDB buffer pool: 16GB (8 instances)
  • Log file: 1GB
  • Max connections: 500
  • Slow query log: enabled (> 2s)
  • Charset: utf8mb4

Redis

  • Max memory: 4GB (allkeys-lru eviction)
  • Persistence: AOF (everysec) + RDB snapshots
  • IO threads: 4

Nginx

  • Worker connections: 8,192
  • Gzip: level 6, min 256 bytes
  • FastCGI timeout: 300s
  • Upload limit: 256MB
  • Auto-vhost: loads projects/*/nginx.conf

RustFS (S3-Compatible Storage)

  • Console: port 9001 (proxied via rustfs.okdii.com)
  • S3 API: port 9000 (proxied via /s3/ path)

Resource Limits

ServiceCPU LimitMemory LimitCPU ReservedMemory Reserved
Nginx42GB1256MB
PHP 8.4816GB21GB
MySQL824GB24GB
Redis46GB1512MB
RustFS24GB0.5256MB
Gotenberg22GB0.5256MB
Total2854GB76.25GB

Commands

# Start semua services
docker compose up -d
# Stop semua
docker compose down
# Rebuild PHP image (selepas edit Dockerfile)
docker compose build php84 && docker compose up -d php84
# Logs
docker compose logs -f <service># Masuk container
docker compose exec php84 bash
docker compose exec mysql mysql -u root -p
# Reload Nginx config
docker compose exec nginx nginx -s reload

About

web dev stack with preset nginx, php & mariadb

Topics

Resources

Stars

4 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages