DEV Community

ahmet gedik
ahmet gedik

Posted on

Dockerizing a Video Platform: From Development to Production

Running a video discovery platform across multiple environments gets messy fast. Different PHP versions, missing extensions, inconsistent configs — the usual headaches. Here's how I containerized DailyWatch using Docker, going from a bare-metal setup to a reproducible, deployable container image.

Why Docker for a PHP + SQLite Stack?

DailyWatch aggregates trending video content from 8+ regions. The stack is intentionally simple: PHP 8.3, SQLite, and LiteSpeed. No PostgreSQL cluster, no Redis dependency in production. That simplicity is a strength when containerizing — fewer moving parts means fewer things to break.

The main goals were:

  • Identical environments across dev and production
  • Easy onboarding (one command to spin up)
  • Predictable deployments with no "works on my machine" surprises

Multi-Stage Dockerfile

A single-stage build bloats the image with build tools you never need at runtime. Multi-stage keeps it lean:

# Stage 1: Build dependencies
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

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

# Stage 2: Production image
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/

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

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

The builder stage compiles extensions and installs Composer packages. The production stage starts from the official OpenLiteSpeed image and copies only what's needed.

Volume Mounts for SQLite Persistence

SQLite files live on disk, so you need volumes to survive container restarts. This is the part most tutorials skip:

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8080:80"
    volumes:
      - sqlite_data:/var/www/html/data
      - page_cache:/var/www/html/data/pagecache
    environment:
      - APP_ENV=production
      - SQLITE_JOURNAL_MODE=WAL
    restart: unless-stopped

volumes:
  sqlite_data:
    driver: local
  page_cache:
    driver: local
Enter fullscreen mode Exit fullscreen mode

Critical detail: SQLite in WAL mode needs the -wal and -shm files to live alongside the main .db file. Mount the entire data/ directory, not individual files.

LiteSpeed Configuration Inside Docker

OpenLiteSpeed uses XML-based virtual host configs. Override the default one:

<!-- conf/vhosts/videoplatform/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>
  <cache>
    <enableCache>1</enableCache>
    <cacheStorePath>/tmp/lscache/</cacheStorePath>
  </cache>
</virtualHost>
Enter fullscreen mode Exit fullscreen mode

The cache store path points to /tmp inside the container so it doesn't pollute the persistent volume.

Development vs Production Compose Files

For local development, override the production compose with hot-reload mounts:

# docker-compose.dev.yml
services:
  web:
    build:
      context: .
      target: builder  # Use the builder stage for dev tools
    volumes:
      - .:/var/www/html  # Live code reload
      - sqlite_data:/var/www/html/data
    environment:
      - APP_ENV=development
      - DISPLAY_ERRORS=1
Enter fullscreen mode Exit fullscreen mode

Run with docker compose -f docker-compose.yml -f docker-compose.dev.yml up. Changes to PHP files reflect immediately without rebuilding.

Health Checks

Don't skip this. A container that starts isn't necessarily a container that works:

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

On DailyWatch, the /health endpoint checks PHP, SQLite connectivity, and disk space on the data volume.

Lessons Learned

  • SQLite locking: Use WAL mode and keep write operations short. Long-running imports should use a separate connection with busy timeout set.
  • OPcache in containers: Set opcache.validate_timestamps=0 in production — files don't change inside a running container, so skip the stat calls.
  • Image size: The multi-stage approach brought the image from 1.2GB down to 380MB.

Containerizing a simple stack like PHP + SQLite is one of those things that sounds trivial until you hit the edge cases. Volume ownership, SQLite journal files, and LiteSpeed's config format all needed attention. But once it works, every deploy is just docker compose up -d.


This article is part of the Building DailyWatch series.

Top comments (0)