Chapter 04
Getting your code on
On a catalog site there is 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.
A custom stack deploys the other way round — its source is always git, and the panel pulls, builds and starts it when you press Deploy. Nothing in this chapter applies to one.
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.
- Get the files into the site folder — on a static or single-page-app site that is
/var/www/virtual/<your-domain>/directly; on a Laravel site the app goes into that folder'sapplication/subdirectory (/var/www/virtual/<your-domain>/application/). - Do any work the app needs — install dependencies, run migrations, rebuild caches.
- 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.
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
The platform gives you one way of its own: SFTP, opened with an SSH key you add to your account. Anything beyond that — a shell and rsync, a copy from somewhere else — depends on what access your operator has given you separately.
SFTP with your own key
Open Your account in the panel and pick the SSH keys tab. Paste the public half of an SSH key and every site you hold is reachable from any SFTP client. Every account gets this, administrator and site owner alike, and what a key reaches is decided by the account it sits on, never by the key itself.
If you have no key yet, make one and paste the file whose name ends in .pub:
$ ssh-keygen -t ed25519
$ cat ~/.ssh/id_ed25519.pub
The other file, the one without that extension, is the private key. It stays on your machine and goes nowhere. Anything beginning -----BEGIN is the wrong half — the panel refuses it, but the moment to notice is before you copy it.
The panel shows the host, the port and your username on the same page. The username is your email address, and the @ in it has to be quoted, or the client reads everything before it as the name of a machine:
$ sftp -P 2223 'you@example.com'@your-server
Without the quotes the error names a host that does not exist rather than the quoting, which is why it is worth getting right the first time. Cyberduck, WinSCP and FileZilla take the host, the port and the username in three separate fields, so the question never comes up there.
A key is the only way in. There is no password to offer and cannot be one; a client that tries is refused, and that is the design rather than something your operator has not finished setting up.
What you see when you connect
One directory per site you hold, and inside each one that site's folder. Not the server, and not a home directory with anything else in it. If you hold no sites the session is empty — that is not a fault, and the SSH keys page says so before you try.
The list is worked out fresh on every connection. A site handed to you appears the next time you connect, a site taken away is gone the next time you connect, and there is nothing to refresh, re-add or re-authorise. The panel's own site never appears, for anybody, administrators included.
Two things come up immediately:
- What you upload is owned correctly. A file put there over SFTP is exactly as usable by the site as one a deploy wrote. There is nothing to
chownafterwards. - A folder mounted from one of your other sites is not there. That arrangement lives inside the running container, and an SFTP session is looking at the folder on the server. Chapter 06 describes those mounts.
A session sees the directories listed for it and nothing else. That confinement is applied by the SFTP service; it is not a filesystem permission boundary underneath, which is worth knowing when you decide who gets an account.
Port 2223 is not behind the IP allowlist. That list is a control over web traffic and cannot cover a raw port, so locking a site does not close its files: a locked site is still reachable over SFTP. That is usually what you want, since the person holding a key is generally the person fixing whatever the site was locked for. Locking is chapter 08.
Two things follow. Keep the private half of the key safe, and take the key off your account the moment the machine holding it is lost or replaced. Removing the key is what revokes file access — locking the site is not.
A removal takes effect on the next connection rather than at once: a session already open is not interrupted, and handing a site to a different owner behaves the same way. The same public key can also sit on more than one account, each authenticating as itself, which is what you want if you hold both an administrator account and a personal one.
An administrator can see and remove the keys on any account, and cannot add one — a key is the account holder's own credential. Keys are shown in full on purpose: a public key is public, and being able to compare one against what somebody tells you they added is the whole point of showing it. If the SFTP service is not running on this box, the page says so rather than listing keys that would not work.
Where each kind of site's files go
On a static or single-page-app site you publish the built output to the site folder directly — the two behave identically here, including the caching of everything under assets/. 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.
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.
Restart is the right button here. Its neighbour, Recreate, replaces the container with a fresh one from the image — harmless, but slower and not what deployed code needs. Chapter 02.1 draws the line between them.
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.
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.