Chapter 04

Getting your code on

There is no deploy button, no git integration, and no build pipeline. You put files in the folder, then restart the site. This chapter is short because the mechanism is — but the restart is not optional, and that part catches everyone.

The rule

Changed PHP files do not take effect until the container restarts. You can copy a fixed file into place, reload the page, and watch the old code run indefinitely.

The reason is the opcache. PHP normally compiles every .php file it touches on every request, so it keeps the compiled result in memory and reuses it — a large speedup for an app that loads hundreds of files per request. Normally it also checks each file's modification time to notice edits, but here that check is switched off: it saves a disk read per file per request, at the cost of never noticing that a file changed.

So once a file has been compiled and cached, the copy in memory is what runs. Editing the file on disk changes something PHP has stopped looking at. Restarting replaces the processes, the cache dies with them, and the next request compiles your new code from disk.

The shape of a deploy

Whatever tooling you wrap around it, every deploy here is the same three steps.

  1. Get the files into the site folder — on a static site that is /var/www/virtual/<your-domain>/ directly; on a Laravel site the app goes into that folder's application/ subdirectory (/var/www/virtual/<your-domain>/application/).
  2. Do any work the app needs — install dependencies, run migrations, rebuild caches.
  3. Restart the site from the panel, so PHP picks up the new code.

Step 3 is required for any change to PHP code — including Blade templates. The only things exempt are static assets: CSS, JavaScript, images and fonts are served straight off disk by the web server without PHP involved, so they update the moment you copy them up.

Templates are not an exception

It is tempting to assume a Blade template is re-read each request. It is not. Blade compiles your .blade.php into a plain PHP file under storage/framework/views/, named from a hash of the view's path — so the name stays the same when you edit it. Laravel notices the source changed and rewrites that compiled file, but the opcache is still holding the previous version under that same path and never re-checks the disk. The old template renders on.

Treat the rule as simple: if it is PHP, or it becomes PHP, it needs a restart.

Step 1 — moving files

How you get files onto the box depends entirely on what access your operator has given you: SSH and rsync, SFTP, or something else. The platform itself takes no position and provides no upload mechanism.

On a static site you publish the built output to the site folder directly. On a Laravel site you publish into the folder's application/ subdirectory, and you keep two things outside it: <folder>/storage/ (the durable Laravel storage tree — logs, caches, uploaded files; the image scaffolds it for you) and <folder>/.env (your credentials, placed on the box). The deploy symlinks both back into application/, which is what makes it safe to wipe and re-publish application/ on every push:

# from your project, into application/ on the box
rsync -az --delete \
  --exclude .git --exclude storage --exclude .env \
  ./ user@box:/var/www/virtual/app.example.com/application/

# point storage/ and .env at the durable copies beside application/
ln -sfn ../storage /var/www/virtual/app.example.com/application/storage
ln -sfn ../.env    /var/www/virtual/app.example.com/application/.env

Excluding .env and storage from the sync matters: they are the two things that must not be overwritten — .env holds your credentials and lives only on the box, and storage/ holds logs, caches and uploaded files you would otherwise wipe on every deploy. Because they sit outside application/ and are symlinked in, the --delete can never reach them.

Run php artisan storage:link once inside application/ so public/storage points at the storage tree. Vendor dependencies (vendor/, node_modules/) either travel in the sync or, more usually, are installed on the box in the next step.

File ownership is handled for you

Every time the container starts it takes ownership of the site folder for the web user. You do not need to chown anything after copying files in — a restart settles it.

Step 2 — work that runs inside the container

The PHP image ships Composer, git and the PHP CLI, so dependency installs and framework commands run inside your own container, against the mounted folder. Run them from the app root, /var/www/virtual/<your-domain>/application. Because the folder is at the same path inside and out, the commands read exactly as they would locally:

composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache

Running these needs a shell in your container, which your operator grants. If you do not have that access, dependencies have to be vendored — committed or built elsewhere and synced up with everything else.

Step 3 — the restart

Use the restart action on your site in the panel. It clears the opcache by replacing the process, and it is also when newly added background programs are picked up.

A restart takes a few seconds and does not touch your folder, your domain, your certificate, or your settings.

The symptom to recognise

If you have deployed a fix and the old behaviour persists — an error on a line you already corrected, a route that does not exist any more, a config value that will not change — do not go looking for a caching bug in your app. Restart the site first. This accounts for most "my deploy did not work" reports.

The same goes for a stale config:cache: rebuild it then restart, in that order.

Verifying

Load a real page in a browser. As covered in chapter 02, the status dot in the panel goes green as soon as nginx answers, and knows nothing about whether your application actually runs — a site with a fatal error still reports up.

If something is wrong, your logs are on the container's standard output: PHP errors, nginx errors, and anything your background programs print. Retrieving them means docker logs on the box, so ask your operator if you do not have that access.

A deployment runner is designed, not built

There is a design for CI running inside each site's own container — pushing to a repository would deploy into your folder, run your build commands, and reload PHP for you, with no manual restart. None of it exists yet. Until it does, the three steps above are the whole story. Treat any mention of automated deploys you may find in the project's design documents as a plan rather than a feature.