Sitemap
Rifqi

Rifqi

Skilled in Software Engineers, Research, and Electronics. Focused in Computer Science.

Distribute PHP without source code

5 min readJul 8, 2025

Difficulty Level: Advanced (But not that hard though!) You need to understand Docker and Linux.

Press enter or click to view image in full size
Photo by Ben Griffiths on Unsplash

The idea came in when I need to deploy my PHP code to my client’s server on-premise, but I don’t want to ship the code due to intellectual property.

The Rules:

  1. No IonCube (or similar). If you don’t know what it is, it will encrypt your PHP code but you’ll still be able to run it. Furthermore, it’s expensive. 😅
  2. It has to be performant, and natively supported by PHP.

Then I realized, PHP has something called “opcache” where it will compile your source code into opcodes (machine language) to be run on Zend VM, just like Java :) Cool! It’s not just protect your code, but also making it fasterrrer! not a language though, but you got the point :)

Let’s dive in. To make this works, we need a way to ship our code as an image (think like a read-only storage media that is isolated with the rest of the system), because opcache works globally regardless of the PHP project. The best candidate for this thing is Docker. (Docker is an alternative to VM but it’s lighter and easier to distribute and deployed. I’ll write another article about it later).

For our example today, we will use Laravel. Why? Because it has a lot of components, and uses many libraries. This way, you will see and learn a lot of edge-cases you might be facing on.

This is the default Laravel directory structure

Most of the time, our proprietary code will live inside the/app directory, which we need to protect, and the other directories like /vendor only contains open source code which we don’t need to protect.

The Steps:

Get Rifqi’s stories in your inbox

Join Medium for free to get updates from this writer.

First, on your project’s directory, create warm-opcache.php. This file will call opcache_compile_file() to manually tells PHP to compile it.

<?php
$directory = new RecursiveDirectoryIterator('/var/www/app'); # We will stick with /var/www
$iterator = new RecursiveIteratorIterator($directory);

foreach ($iterator as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
echo "Compiling: {$file}\n";
opcache_compile_file($file);
}
}

Second, create empty-preserve-time.sh (don’t forget to chmod +x it). This file will empty your PHP code contents, but still preserving the timestamp of the file. Because opcache will “refresh” if your file’s updated-at time changes.

#!/bin/bash

for file in $(find ./app -type f -name "*.php"); do
timestamp=$(stat -c %Y "$file") # Get mtime in seconds since epoch
: > "$file" # Truncate file
touch -d "@$timestamp" "$file" # Restore original timestamp
done

Third, add this zz-opcache.ini file to /usr/local/etc/php/conf.d (or wherever your conf.d folder lives. (Don’t forget to enable PHP ext-opcache on your system)

opcache.enable=1
opcache.enable_cli=1
opcache.validate_timestamps=1
opcache.revalidate_freq=10
opcache.file_cache=/var/www/.opcache
opcache.file_cache_only=1

⚠️ Be sure to git commit your code first, or back-it up somewhere else! These steps below are destructive!

This is where the magic happens, if you run php warm-opcache.php then run empty-preserve-time.sh, it will truncate/empty the file contents and preserving the directory structure under the /app, but you can still run your Laravel project. Go ahead give it a try!

This method works best with any kind of PHP project, either using PSR-4 standards, or just simple require() it will works. But Laravel uses PSR-4.

Nice, we have a working concept. Next step is packaging, we need to distribute it to the client’s on-premise server. (Like you can compile Go as .exe file)

TL;DR this is the Dockerfile. (Beware that this Dockerfile is not optimized for small layers, but verbosed to make learning easier)

FROM php:8.3-fpm-alpine # change according to your needs

# Add more pecl install or docker-php-ext-install
# to install extensions needed by your project

# Enable opcache
RUN docker-php-ext-install opcache

WORKDIR /var/www
RUN mkdir -p /var/www/.opcache

# Copy source code
COPY app ./app
COPY artisan ./artisan
COPY bootstrap ./bootstrap
COPY database ./database
COPY config ./config
COPY public ./public
COPY resources ./resources
COPY routes ./routes
COPY storage ./storage
COPY composer.* .

# Install the ini
COPY zz-opcache.ini /usr/local/etc/php/conf.d

# composer install in Laravel needs .env
# we will copy dummy .env
COPY .env.example .env

# Install PHP dependencies (Don't move this line above)
RUN composer install --no-dev --optimize-autoloader

# Compile and delete the source code in /app
RUN php warm-opcache.php
RUN ./empty-preserve-time.sh

# CONGRATULATIONS! YOUR CODE HAS BEEN STRIPPED OUT!
# if you dont believe, you can `ls` your /app directory and `cat` it

# If wanted, you can create an ENTRYPOINT script that also does
# ./artisan queue:work, or ./artisan schedule:work
CMD ["./artisan serve"]

Now, you can docker build and docker push it to your registry server, and pull it from your client’s on-premise server without ever having to ship your code naked! And when you have an update, a simple docker pull will save a lot of time!

Question might be asked, can we delete the /app directory instead of just leaving it empty? NO. Because “opcache” will check if the file exists.

Bonuses!

That Dockerfile above is not secure. Why? Because Docker uses layers on each stage, meaning when you COPY app ./app it literally copies your unprotected code, and create layer of it. People with expertise in Docker can easily unwrap the layers, and get your original code.

The solution, is to use multi-stage build. Here is the revised Dockerfile. Notice we add as build in line number 1.

FROM php:8.3-fpm-alpine as build # change according to your needs

# Add more pecl install or docker-php-ext-install
# to install extensions needed by your project

# Enable opcache
RUN docker-php-ext-install opcache

WORKDIR /var/www
RUN mkdir -p /var/www/.opcache

# Copy source code
COPY app ./app
COPY artisan ./artisan
COPY bootstrap ./bootstrap
COPY database ./database
COPY config ./config
COPY public ./public
COPY resources ./resources
COPY routes ./routes
COPY storage ./storage
COPY composer.* .

# Install the ini
COPY zz-opcache.ini /usr/local/etc/php/conf.d

# composer install in Laravel needs .env
# we will copy dummy .env
COPY .env.example .env

# Install PHP dependencies (Don't move this line above)
RUN composer install --no-dev --optimize-autoloader

# Compile and delete the source code in /app
RUN php warm-opcache.php
RUN ./empty-preserve-time.sh

# CONGRATULATIONS! YOUR CODE HAS BEEN STRIPPED OUT!
# if you dont believe, you can `ls` your /app directory and `cat` it

# ======== HERE IS THE MULTISTAGE LAYER BUILD ===========
FROM php:8.3-fpm-alpine # change according to your needs

WORKDIR /var/www

# (Repeat the exact same steps above)
# Add more pecl install or docker-php-ext-install
# to install extensions needed by your project

# Enable opcache
RUN docker-php-ext-install opcache

# Install the ini
COPY zz-opcache.ini /usr/local/etc/php/conf.d

# Copy the emptied file, and opcached code from `build` to here
COPY --from=build /var/www .

# If wanted, you can create an ENTRYPOINT script that also does
# ./artisan queue:work, or ./artisan schedule:work
CMD ["./artisan serve"]

Hi I’m Rifqi, a Senior Software Engineer from Indonesia 🇮🇩. I’m an expert in DevOps, PHP, Frontend-Backend Stack, Mobile Apps Development, Software Architecture, Cloud, Automation, and much more. If you need me or wanted to hire me, don’t hesitate to call me. Let’s be friends!

--

--

Rifqi
Rifqi

Written by Rifqi

Skilled in Software Engineers, Research, and Electronics. Focused in Computer Science.