Video Streaming + CMS + Automation: Cara Ngebangun Platform Media dengan 3 Docker Container
Klien butuh CMS buat editor, video streaming buat pembaca, automation biar bisa survive dengan tim kecil, dan frontend yang cepat. Ini breakdown cara menyusun 3 Docker container yang saling terhubung — termasuk konfigurasi production yang beneran jalan.
Sering gue nemuin kasus kayak gini: media atau content platform butuh beberapa service yang harus jalan bareng — frontend, CMS, database, dan kadang automation. Tapi mereka nggak butuh microservices yang over-engineered. Mereka butuh 3-4 container yang masing-masing punya job jelas.
Gue bakal breakdown cara menyusun docker-compose.yml yang production-ready, cara setup video streaming dengan HLS.js, dan bagaimana automation bisa ngurangin kerja manual tim editorial.
Struktur Folder
project/ ├── docker-compose.yml ├── .env ├── frontend/ │ ├── Dockerfile │ ├── app/ │ │ └── (public)/ │ │ ├── page.tsx │ │ ├── channel/ │ │ │ └── [slug]/ │ │ │ └── LiveTVClient.tsx │ │ └── news/ │ └── package.json ├── cms/ │ ├── Dockerfile │ ├── src/ │ │ ├── api/ │ │ └── admin/ │ └── package.json ├── scripts/ │ └── init-db.sql └── nginx/ └── (handled di host)
docker-compose.yml
Ini konfigurasi yang sebenarnya gue pakai (value sensitif di .env):
services:
postgres:
image: postgres:16-alpine
container_name: app_db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432" # localhost ONLY
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 5cms: build: context: ./cms dockerfile: Dockerfile container_name: app_cms restart: unless-stopped depends_on: postgres: condition: service_healthy ports: - "127.0.0.1:1337:1337" # localhost ONLY environment: NODE_ENV: production DATABASE_HOST: postgres DATABASE_PORT: 5432 DATABASE_NAME: ${DB_NAME} DATABASE_USERNAME: ${DB_USER} DATABASE_PASSWORD: ${DB_PASSWORD} volumes: - cms_uploads:/app/public/uploads healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:1337/_health"] interval: 30s timeout: 10s start_period: 480s # CMS startup lama retries: 5
frontend: build: context: ./frontend dockerfile: Dockerfile container_name: app_frontend restart: unless-stopped depends_on: - cms ports: - "127.0.0.1:3001:3000" # localhost ONLY environment: NEXT_PUBLIC_CMS_URL: ${CMS_URL} NEXT_PUBLIC_CMS_TOKEN: ${CMS_TOKEN} healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/"] interval: 30s timeout: 10s retries: 3
volumes: postgres_data: cms_uploads: ```
Detail penting yang sering terlewat:
Semua ports di-bind ke 127.0.0.1. Tidak ada service yang langsung expose ke internet. Nginx di host yang handle public access dan SSL termination. Ini security fundamental.
depends_on dengan condition: service_healthy. Frontend cuma start kalau CMS sudah sehat. CMS cuma start kalau PostgreSQL sudah sehat. Urutan dependency jelas.
start_period: 480s untuk CMS. Headless CMS butuh waktu lama buat build admin panel pertama kali. Tanpa start_period yang cukup, health check fail dan container restart terus.
Nginx Reverse Proxy
Nginx jalan di host (bukan container). Dia yang handle SSL dan routing:
server {
listen 443 ssl http2;
server_name example.com;ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Frontend location / { proxy_pass http://127.0.0.1:3001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
# CMS — subdomain terpisah server { listen 443 ssl http2; server_name cms.example.com;
location / { proxy_pass http://127.0.0.1:1337; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
client_max_body_size 50M; # Upload besar untuk CMS } ```
Kenapa Nginx di host? SSL certificate (Let's Encrypt via Certbot) lebih gampang manage langsung dari host. Kalau mau nambah service, tinggal tambah location block.
Video Streaming dengan HLS.js
Untuk konten video (live dan VOD), HLS punya compatibility paling luas — termasuk iOS Safari yang nggak support format lain.
// components/VideoPlayer.tsx
import Hls from 'hls.js'
import { useEffect, useRef, useState } from 'react'export function VideoPlayer({ src, poster }: { src: string; poster?: string }) { const videoRef = useRef<HTMLVideoElement>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null)
useEffect(() => { const video = videoRef.current if (!video || !src) return
if (Hls.isSupported()) { // Chrome, Firefox, Edge — pakai MSE const hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60, }) hls.loadSource(src) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () => { setLoading(false) video.play().catch(() => {}) }) hls.on(Hls.Events.ERROR, (_, data) => { if (data.fatal) { setLoading(false) setError('Stream gagal dimuat') } }) return () => hls.destroy() } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Safari/iOS — native HLS video.src = src video.addEventListener('loadedmetadata', () => setLoading(false)) } else { setError('Browser tidak mendukung HLS') } }, [src])
return ( <div className='relative'> {loading && ( <div className='absolute inset-0 flex items-center justify-center bg-black/50'> Loading... </div> )} {error && ( <div className='absolute inset-0 flex items-center justify-center bg-black/80 text-white'> {error} </div> )} <video ref={videoRef} poster={poster} controls playsInline // penting buat iOS className='w-full rounded-lg' /> </div> ) } ```
Dua branch penting: Hls.isSupported() buat browser yang pakai MSE, dan canPlayType() buat Safari/iOS yang native support.
Automation dengan n8n
n8n jalan sebagai proses terpisah. Workflow-nya:
- Cron trigger setiap 30 menit
- Scrape berita dari sumber yang sudah ditentukan
- Extract judul, konten, dan gambar
- Push ke CMS sebagai draft via API
- Kirim notifikasi ke Telegram editor buat review
Keuntungan n8n dibanding custom script: tim editorial bisa modify workflow sendiri tanpa coding.
Tips Production
Jangan over-engineer. Tiga container itu cukup. Lo nggak butuh API gateway atau Kubernetes untuk platform skala menengah.
Health checks wajib. Tanpa healthcheck, container bisa jalan tapi aplikasinya belum siap menerima request.
Backup volume. postgres_data dan cms_uploads itu data yang nggak bisa di-recreate. Setup cron job buat backup.
Nginx di host lebih simpel. SSL certificate management jauh lebih gampang, dan lo nggak perlu restart container kalau mau update config.