DEV Community

ahmet gedik
ahmet gedik

Posted on

Dockerizing a Video Platform: From Development to Production

When your video platform runs across multiple European regions, environment consistency stops being a nice-to-have and becomes essential. Here's how I Dockerized ViralVidVault, a viral video vault that curates trending content across European countries, from a manual setup to a fully containerized workflow.

The Stack

ViralVidVault runs on PHP 8.3, SQLite, and LiteSpeed. No heavy database server, no message queue — the simplicity is deliberate. Docker's job is to wrap this into something portable and predictable.

The containerization goals:

  • One command to spin up the full environment locally
  • Identical behavior between dev laptop and production hosting
  • SQLite data survives container rebuilds

Multi-Stage Dockerfile

Keep build tools out of the production image:

# Stage 1: Build
FROM php:8.3-cli AS builder

RUN apt-get update && apt-get install -y \
    libsqlite3-dev libcurl4-openssl-dev libzip-dev \
    && docker-php-ext-install pdo_sqlite curl zip opcache

WORKDIR /app
COPY composer.json composer.lock ./
RUN curl -sS https://getcomposer.org/installer | php \
    && php composer.phar install --no-dev --optimize-autoloader

# Stage 2: Runtime
FROM litespeedtech/openlitespeed:1.7.19-lsphp83

COPY --from=builder /usr/local/lib/php/extensions/ \
     /usr/local/lsws/lsphp83/lib/php/extensions/
COPY --from=builder /app/vendor /var/www/html/vendor
COPY . /var/www/html/

RUN mkdir -p /var/www/html/data \
    && chown -R nobody:nogroup /var/www/html/data

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost/health || exit 1

EXPOSE 80
CMD ["/usr/local/lsws/bin/lswsctrl", "start"]
Enter fullscreen mode Exit fullscreen mode

The builder stage handles Composer dependencies and PHP extension compilation. The runtime stage copies only the artifacts it needs. This cuts the image from over 1GB to roughly 400MB.

Docker Compose with SQLite Volumes

SQLite needs careful volume management. Mount the entire data directory, not individual .db files — SQLite creates -wal and -shm companion files that must live in the same directory:

# docker-compose.yml
version: '3.8'

services:
  viralvidvault:
    build: .
    ports:
      - "8080:80"
    volumes:
      - app_data:/var/www/html/data
    environment:
      - APP_ENV=production
      - SQLITE_JOURNAL_MODE=WAL
      - FETCH_REGIONS=US,GB,PL,NL,SE,NO,AT
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M

volumes:
  app_data:
    driver: local
Enter fullscreen mode Exit fullscreen mode

The FETCH_REGIONS environment variable drives which European regions the cron fetcher targets. Different from other sites in the family — ViralVidVault focuses on European content.

LiteSpeed Virtual Host Config

OpenLiteSpeed uses XML config files. Override the default virtual host:

<!-- conf/vhosts/app/vhconf.xml -->
<virtualHost>
  <docRoot>/var/www/html/public</docRoot>
  <index useServer="0">
    <indexFiles>index.php</indexFiles>
  </index>
  <rewrite>
    <enable>1</enable>
    <rules>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php?url=$1 [QSA,L]
    </rules>
  </rewrite>
  <phpIniOverride>
php_value opcache.validate_timestamps 0
php_value opcache.memory_consumption 128
  </phpIniOverride>
</virtualHost>
Enter fullscreen mode Exit fullscreen mode

Setting opcache.validate_timestamps to 0 in production is safe inside containers — files don't change at runtime, so skipping stat calls gives a measurable performance boost.

Development Override

For local development, add live code mounting and debugging tools:

# docker-compose.dev.yml
services:
  viralvidvault:
    volumes:
      - .:/var/www/html
      - app_data:/var/www/html/data
    environment:
      - APP_ENV=development
      - DISPLAY_ERRORS=1
    ports:
      - "8080:80"
      - "9003:9003"  # Xdebug
Enter fullscreen mode Exit fullscreen mode

Run both with docker compose -f docker-compose.yml -f docker-compose.dev.yml up. The bind mount overlays the built image with your local source, so edits appear instantly without rebuilding.

Handling SQLite Locking in Containers

SQLite's biggest gotcha in Docker: file locking. WAL mode is mandatory for any concurrent read/write scenario:

// Ensure WAL mode on every connection
$db = new PDO('sqlite:/var/www/html/data/videos.db');
$db->exec('PRAGMA journal_mode=WAL');
$db->exec('PRAGMA busy_timeout=5000'); // Wait up to 5s for locks
$db->exec('PRAGMA synchronous=NORMAL'); // Good balance of safety and speed
Enter fullscreen mode Exit fullscreen mode

Without busy_timeout, concurrent requests during a cron fetch will get SQLITE_BUSY errors. Five seconds is generous but prevents any realistic timeout scenario.

What I Learned

  • Named volumes, not bind mounts for production data. Bind mounts to host directories have permission headaches across different Docker hosts.
  • OPcache preloading inside containers is free performance. Precompile your autoloader and router.
  • Don't run cron inside the app container. Use a separate container or host-level cron that hits the app's task endpoint.

The container setup took a day to get right. It saves that time back on every deploy, every new developer onboarding, and every "it works on my machine" conversation that no longer happens.


This article is part of the Building ViralVidVault series.

Top comments (0)