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"]
; 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
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:
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";
}
}
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;
}
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
<?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
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
Deployment: From Docker Image to LiteSpeed
The production LiteSpeed servers do not run Docker — they are shared hosting. The Docker image serves three purposes:
- CI testing — GitHub Actions builds the image and runs PHPUnit inside it
- Local dev — Developers run the full stack locally
-
Staging preview —
docker compose upspins 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)