DEV Community

ahmet gedik
ahmet gedik

Posted on

Dockerizing a Video Platform: From Dev to Production

TrendVidStream aggregates trending video content from 8 regions spanning the Nordics, Middle East, and Central Europe — CH, DK, AE, BE, CZ, FI plus US and GB. Each region has its own cron schedule and fetch cadence. Keeping all of that consistent between a developer's laptop and a LiteSpeed production server is where Docker earns its keep.

The Problem with PHP+SQLite on Bare Metal

SQLite has no server to install, which seems like an advantage. The catch is that PHP extensions, system SQLite versions, and locale settings can differ silently between machines. A developer on Ubuntu 24.04 with SQLite 3.45 behaves differently from the LiteSpeed server running SQLite 3.39 from the distro package manager.

Docker freezes these variables into a reproducible image.

Multi-Stage Dockerfile

# Stage 1: Build dependencies
FROM php:8.3-cli-alpine AS deps

RUN apk add --no-cache git curl unzip
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
      --no-dev \
      --optimize-autoloader \
      --no-interaction \
      --no-progress

# Stage 2: Production runtime
FROM php:8.3-fpm-alpine AS production

RUN apk add --no-cache \
        sqlite-dev \
        libpng-dev libjpeg-turbo-dev freetype-dev \
        curl-dev icu-dev && \
    docker-php-ext-configure gd \
        --with-freetype --with-jpeg && \
    docker-php-ext-install \
        pdo_sqlite gd curl intl opcache

# OPcache tuned for a read-heavy video platform
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini

WORKDIR /var/www/html

COPY --from=deps /app/vendor ./vendor
COPY app/           ./app/
COPY public/        ./public/
COPY templates/     ./templates/
COPY cron/          ./cron/
COPY api_keys.conf  ./

# data/ is ALWAYS a volume — never in the image
RUN mkdir -p data/pagecache && \
    chown -R www-data:www-data data

USER www-data
EXPOSE 9000
CMD ["php-fpm"]
Enter fullscreen mode Exit fullscreen mode
; docker/opcache.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=8192
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.fast_shutdown=1
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml for Local Dev

version: '3.9'

services:
  app:
    build:
      context: .
      target: production
    volumes:
      # Live code reload — mount source over image copies
      - ./app:/var/www/html/app:ro
      - ./templates:/var/www/html/templates:ro
      - ./public:/var/www/html/public:ro
      # Persistent SQLite database
      - tvs_data:/var/www/html/data
    environment:
      SITE_NAME: TrendVidStream
      SITE_URL: https://trendvidstream.com
      FETCH_REGIONS: "US,GB,CH,DK,AE,BE,CZ,FI"
      DB_PATH: /var/www/html/data/videos.db
      CACHE_PATH: /var/www/html/data/pagecache
    networks:
      - tvs

  nginx:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./public:/var/www/html/public:ro
      - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app
    networks:
      - tvs

  cron:
    build:
      context: .
      target: production
    volumes:
      - tvs_data:/var/www/html/data
    environment:
      FETCH_REGIONS: "US,GB,CH,DK,AE,BE,CZ,FI"
      DB_PATH: /var/www/html/data/videos.db
    command: >
      sh -c 'while true;
        do php /var/www/html/cron/fetch_videos.php;
        sleep 25200;
      done'
    networks:
      - tvs

volumes:
  tvs_data:

networks:
  tvs:
Enter fullscreen mode Exit fullscreen mode

Nginx Dev Config (LiteSpeed Proxy)

server {
    listen 80;
    root /var/www/html/public;
    index index.php;
    charset utf-8;

    # Same rewrite logic as production .htaccess
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass   app:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
        # Simulate Cloudflare Flexible SSL header
        fastcgi_param  HTTP_X_FORWARDED_PROTO https;
    }

    location ~* \.(css|js|woff2|svg|webp|png|jpg)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
Enter fullscreen mode Exit fullscreen mode

LiteSpeed Gap: What Docker Cannot Replicate

LiteSpeed has two production-only behaviours:

1. The lscache/ directory — LiteSpeed writes HTML page cache here. On Docker/Nginx this directory simply does not appear. The PHP fallback page cache in data/pagecache/ is used instead:

<?php

const IS_LITESPEED = (PHP_SAPI === 'litespeed');

function serveFromCache(string $cacheKey): bool
{
    if (IS_LITESPEED) {
        // LiteSpeed handles this at the web server layer
        return false;
    }

    $file = CACHE_PATH . '/' . md5($cacheKey) . '.html';
    if (file_exists($file) && (time() - filemtime($file)) < 10800) {
        readfile($file);
        return true;
    }
    return false;
}
Enter fullscreen mode Exit fullscreen mode

2. <IfModule LiteSpeed> blocks — Apache and Nginx silently skip these, so .htaccess cache headers do not interfere with local dev.

SQLite WAL Mode Check

# Verify WAL mode is active after first boot
docker compose exec app \
  sqlite3 data/videos.db 'PRAGMA journal_mode;'
# Expected output: wal
Enter fullscreen mode Exit fullscreen mode
<?php
// Set on first connection — idempotent
$pdo->exec('PRAGMA journal_mode=WAL');
$pdo->exec('PRAGMA synchronous=NORMAL');
$pdo->exec('PRAGMA cache_size=-32768'); // 32MB page cache
Enter fullscreen mode Exit fullscreen mode

Developer Workflow

# Bootstrap: one command from clone to running platform
docker compose up -d --build

# Seed the database with real trending data
docker compose exec cron php /var/www/html/cron/fetch_videos.php

# Tail the fetcher logs
docker compose logs -f cron

# Open a SQLite shell
docker compose exec app sqlite3 data/videos.db

# Build the production image without dev mounts
docker build --target production -t tvs:latest .

# Check image size
docker images tvs:latest
# Should be ~95MB
Enter fullscreen mode Exit fullscreen mode

Deployment: From Docker Image to LiteSpeed

The production LiteSpeed servers do not run Docker — they are shared hosting. The Docker image serves three purposes:

  1. CI testing — GitHub Actions builds the image and runs PHPUnit inside it
  2. Local dev — Developers run the full stack locally
  3. Staging previewdocker compose up spins up a functional preview before FTP deploy

The actual deploy to TrendVidStream production uses lftp to mirror files to the LiteSpeed server, as covered in other articles in this series.

Results

The Dockerfile enforced PHP 8.3 with identical extensions across the team and CI. Three previously silent bugs (a strftime() locale difference, a missing intl extension on one machine, and a SQLite version discrepancy) were caught before they ever reached production.


This is part of the "Building TrendVidStream" series, documenting the architecture behind a global video directory covering Nordic, Middle Eastern, and Central European regions.

Top comments (0)