Chapter 03
Choosing an image
The image is the runtime a catalog site gets. Three built-in ones cover most of what people run, they layer on each other, and choosing between them comes down to two questions: does your code need PHP, and if it does not, do your URLs exist as files on disk?
In the panel this is labelled Site type: the column on the sites list, the field on the New site form, and a row on a site's General tab. It is the same thing this guide calls the image. The three below are built in and on every box. The picker on your own box is the list that counts, and it may well be longer: an administrator can upload images of their own, each carrying a version, and they sit in the same picker as the built-in ones. Chapter 03.1 is about those.
This chapter, and every chapter after it, is about catalog sites. A custom stack has no site type — it brings its own images, from its own compose file.
The built-in images
| Static Website | Single-Page App | Laravel App |
|---|---|---|
| nginx serving files straight from your folder. | The same, except that a URL matching no file is answered with your index.html instead of a 404. |
Everything in the static image, plus PHP 8.3 via php-fpm, and supervisor for background work. |
| Docroot is the folder root. | Docroot is the folder root. | Docroot is public/ inside the folder's application/ subdirectory. |
| No settings to configure. | No settings to configure. | Two settings: PHP memory, and how many requests it serves at once. |
| No background programs. | No background programs. | Queue workers, scheduler, and boot tasks. |
No .env and no database — nothing runs that could use either, so the site has no Environment or Databases tab. |
The same: nothing server-side runs, so neither of those tabs appears. | An .env editor and a MySQL database you provision yourself. |
Good for: landing pages, documentation, static exports — anything where /about really is /about/index.html on disk. |
Good for: a built front-end whose routes are drawn by the browser — React, Vue, Svelte, Angular. | Good for: Laravel, and PHP applications generally — the layout convention is Laravel's, but nothing is Laravel-specific. |
Despite the name, the Laravel image is a general PHP 8.3 runtime. If your PHP application serves from a public/ subdirectory and tolerates a front-controller rewrite, it will run.
Static Website or Single-Page App
The two file-serving images are the same image with one difference, and for a hand-written site or a static export the difference never shows. It decides everything the moment your pages are drawn by a router in the browser instead of existing as files.
The Static Website image answers a request by looking for the matching file, and returns 404 when there is none. A single-page app's routes — /schedule, /settings/billing — are invented by its router and have never been files. So the site appears to work, because clicking around never asks the server anything, and then it breaks on the three things real visitors do:
- refreshing the page on any route other than the home page
- opening a bookmark, or a link somebody shared
- pressing back into a deep link
The Single-Page App image answers those URLs with the app shell — your index.html — so the router takes over and the page loads.
Ask one question of your build output: is there a real file or folder for every page? If there is, take Static Website. If it is one index.html plus a bundle — what Vite, Create React App, Vue CLI, SvelteKit's static adapter and Angular all produce — take Single-Page App.
Neither image has anything to configure, and picking wrong is cheap to undo: moving between the two is a reprovision, covered at the end of this chapter, and your folder is not touched.
The fallback deliberately stops at assets/. A request for a bundle that is not there fails honestly instead of being answered with your index.html, which the browser would then try to run as JavaScript.
You see this when a visitor's open tab references a bundle your latest deploy replaced. Reloading fixes it, because the fresh index.html names the new files.
Files under assets/ are cached in the browser for a year, which is safe because their names change whenever their contents do. index.html is revalidated every time. That pairing is what makes a deploy take effect at once rather than leaving visitors on a stale page, and adding cache headers of your own can only break it.
If your build writes its output somewhere other than assets/, everything still works — returning visitors just re-download more than they need to.
.webmanifest files are served with the correct content type, on every image. Previously the browser rejected the file and the install prompt simply never appeared, with nothing in any log to explain why. If you have a manifest and it seemed to do nothing, it will work now without any change on your side.
What the PHP image includes
PHP 8.3 with these extensions compiled in and enabled:
fpm cli mysql mbstring xml bcmath
curl zip gd redis opcache intl
Composer is installed and on the path, as are git and unzip. That matters because there is no build step outside the container — if you need to install dependencies, you do it inside the container against the mounted folder.
Requests are served by nginx with a standard front-controller rewrite: anything that is not a real file falls through to index.php. Static assets — images, fonts, CSS and JS — get a one-year immutable cache header, so fingerprint your asset filenames if you expect to change them.
Requests for paths beginning with a dot are refused, so a stray .env or .git in the docroot is not exposed. The one exception is /.well-known/, which is allowed through for certificate validation and similar uses.
The settings you can change
Every image declares which settings it accepts, and the panel offers exactly those. Of the built-in three, the Laravel image declares two and the two file-serving images declare none; a custom image declares whatever its author chose, so the fields you meet on the New site form come from the image you picked.
- PHP memory
PHP_MEMORY_LIMIT— default256M. How much memory one request may use before PHP kills it. Accepts any value PHP understands, such as512M. Applied when the container starts.- Requests at once
PHP_FPM_MAX_CHILDREN— default8. How many requests the site serves simultaneously. Beyond that they queue.
Multiply them and you have what the site can reach: at 256M and 8 at once, roughly 2 GB. Raising PHP memory alone is the intuitive move and it raises that ceiling eightfold, which is rarely what anyone intends.
Lowering Requests at once is a legitimate way to keep a small site small. It cannot break the site — under load the extra requests wait rather than fail, so the trade you are making is slower, not broken.
Both of those are limits inside the container, enforced by PHP. The container itself can be capped too — a memory limit the server enforces, set from Resource usage in the panel and covered in chapter 09, along with the figures to base it on.
You choose settings when creating a site and can change them afterwards. Changing a setting recreates the container. Your domain, folder, route, certificate and files all survive that — the site is briefly unavailable while the new container starts, and nothing else moves.
Only settings declared by the image become environment variables in the container. Arbitrary keys are rejected rather than passed through, so you cannot inject DB_PASSWORD or an API key through the panel.
Your application's own configuration belongs in a file in your site folder — for Laravel, the usual .env. It sits outside the docroot and is not web-accessible. See chapter 06 for the connection details to put in it.
Changing the site type later
A site can move from one image to another. On the site's General tab, the card for it offers everything your box's Site Images page holds minus the type you are already on — and if there is nowhere to go, no card at all. Where a custom image exists in more than one version, each version is its own entry in that list, so moving a site to a newer build of the image it is already on is this same change. Press Review change and the next page opens by stating the move, from which type to which.
Settings that both images declare carry over if the value is still valid; settings only the new image declares take their default; settings only the old image had are dropped. The panel shows you exactly which of those three happens to each setting before you confirm.
What it does not do is move your files. Static Website and Single-Page App share a docroot, so switching between those two is nothing but a reprovision — the folder and its contents are left exactly as they are, and the site comes back answering unmatched URLs differently. Switching to PHP is the one that asks something of you: the Laravel docroot is elsewhere, so your content needs to end up under application/public/, and that rearrangement is yours to do.
Switching the other way, from PHP to static, takes the Environment page with it — there is no longer an .env to edit. A database you already provisioned is not taken away: the Databases page stays so you can still reset or drop it, even though a static site would never be offered one. Nothing is ever hidden that you would then have no way to remove.
The list is per box and open-ended. A new image is a Dockerfile plus a manifest, uploaded through the panel by an administrator, so a box can carry a Python service, a Node API or an image built for one particular application, and your picker may list runtimes this chapter does not describe.
If you are a site owner, this is something to ask an administrator for — chapter 03.1 is what they will follow, and reading it tells you what to give them. The alternative is to bring the whole runtime yourself as a custom stack, which is exactly the case stacks exist for.