Custom application design¶
site-custom lets a Site run an application image selected by the Site
owner or administrator. It runs inside the Site Pod and receives traffic from
site-proxy. It does not create an external Service or an independent route.
The shared container mechanics are in
site application architecture; the selection rules
are in domain model.
Site configuration¶
The custom application is the site-custom entry in
spec.configuration.applications.
| Field | Rule |
|---|---|
name |
Only the value site-custom has an effect. The key is not declared in the v2 schema; it survives because the applications array node sets x-kubernetes-preserve-unknown-fields. |
image |
A non-empty value enables the application. The Custom Resource Definition (CRD) pattern is ^[^/\s]+/(([a-z0-9]+/)*[a-z0-9-_]+)/([a-z0-9._-]+)(:([A-Za-z0-9._-]+))?(?:@sha256:[a-f0-9]{64})?$: host (no whitespace), one or more lowercase repository path segments, an image name (lowercase letters, digits, dots, hyphens, underscores), optional mixed-case tag, optional digest. The Hub form uses the identical regex (IMAGES_REGEX, core/.../config/constants.py) and matches the whole value. The form removes leading and trailing whitespace before it validates the image; whitespace inside the value is an error. Kubernetes rejects an image with whitespace when the StatefulSet creates the pod, and only an Event reports that error. |
proxy_type |
CRD pattern ^(fastcgi\|proxy)$. |
context_root |
CRD pattern ^(root_context\|site_context)$. |
extra_env |
List of {name, value}; the value is stored base64-encoded (like the model's other secrets) and decoded for use. Names follow ^[A-Za-z_][A-Za-z0-9_]*$; a value must not contain a comma, the entry separator of the Hub form transport; names the platform injects (SITE_PLATFORM_ENVIRONMENT_NAMES, core/.../model/site_exporter.py, which includes the masked NAMESPACE and KUBERNETES_* names) are reserved. |
The Hub form and its client-side check show one example on an invalid
reference: the first entry of site.custom_application_allowed_registries,
or docker.io/library/redis:7 when the list is empty. The example does not
use kubernetes.private_registry: that key locates the platform images, and
a short name that fails the check is an implicit Docker Hub reference.
The two enum-like fields use the same accepted values on every path:
from_json(custom resource (CR)/API) rejects a present-but-invalidproxy_typeorcontext_rootwith a message that names the field, the accepted values and the received value (SiteApplicationsBuilder._validate_enum_field). A null value is the platform default and stays valid.from_form(Hub UI): the custom entry is created only when the form has a non-empty image.proxy_typefalls back toproxyandcontext_roottoroot_contextwhen the submitted value is not a valid option; the form validators are the write boundary that enforces the accepted values. The environment string is parsed with the dedicated parserparse_extra_env(core/.../config/utils.py): entries split at the comma, then each entry splits at the first=, so a value keeps its spaces.
Rejected values reach the Hub API as a validation error on the request; the Site resource is not changed.
Note: despite the similar names, custom-headers.conf and
custom-mime.types are proxy files rendered from the Site headers
configuration and the platform MIME map — they are unrelated to site-custom
(Site proxy).
Image policy¶
The platform configuration controls custom images
(core/.../form/forms.py validation and core/.../form/utils.py):
site.custom_application_allowed_registrieslists registry hosts or host/path prefixes. The check is a prefix match against the image string with trailing slashes normalised. An empty list (default) allows every registry.- The registry check applies only to a new or changed image: the form compares against the previously stored custom image and skips the check when they match, so unrelated edits keep working under restrictive lists.
site.allow_public_custom_applicationdefaults tofalse. Without it, a custom image is allowed only on a private Site.- Public-site fallback: when a form edit submits a public Site that
already was public and keeps the same custom image while the setting
is false, the form reverts the Site to private (
site_type_reverted = True) and fills the default access users and groups fromsite.default_view_users/default_view_groupswhen the submission has none. The Hub API holds that revert until the caller confirms it: an update whose data does not setconfirm_site_type_reverttotruereturns statusERRORwithdata.confirm_site_type_revert: trueand the effect message, and does not change the Site (hub/.../app/api/common.py,edit_site). The Hub interface shows the message and re-sends the same data with the confirmation set. A raw API client that ignores the field gets the silent revert and finds it in the response fieldssite_type_reverted/site_type_revert_access_groups. Any other public+custom combination fails validation with a message on the custom-image field; the same message also lands on the site-type field when the submission changes a private Site to public. kubernetes.custom_application_image_pull_secrets_nameadds a second image pull Secret only when it is configured, the Site has a custom image, and it differs from the platform Secret. Every Site receives the platform pull Secret (kubernetes.private_registry_image_pull_secrets_name) when that name is not empty (core/.../model/site_exporter.py,_get_image_pull_secrets). Both settings default tonull, and the chart does not fill them fromserviceAccount.imagePullSecretName. Thus a Site pod gets registry credentials only when the operator sets them explicitly. Kubernetes gives pull Secrets to the full pod, so a custom image can also use the platform pull Secret when it is set.
The built-in platform registry does not limit custom images. The custom image registry list provides that policy.
Image availability¶
The Hub does not ask the registry whether an image exists before a save. Such
a check would make the Hub send requests to a host that the user chooses, and
it could only give a reliable answer for a clean HTTP 404. The container
runtime is the authority: when the platform cannot pull the image, the Settings
tab status line shows error with the container message, for example
ImagePullBackOff, and the System tab shows the details.
Container contract¶
SiteCustomApplication builds the container through the shared _container()
helper with include_oidc_secrets=False and image_pull_policy='Always'
(user images are mutable tags):
| Property | Value |
|---|---|
| Container name | site-custom |
| Port | 8000 (also exposed as the PORT environment variable) |
| Image pull policy | Always |
| OpenID Connect (OIDC) Secret | Not injected (the only application that passes through the shared builder and opts out; the initializer and the mirror build their own environment lists, which also omit it) |
| Site Secret and environment ConfigMap | Injected through envFrom, so MASTER_TOKEN and the site environment reach the container; extra_env entries arrive as decoded direct env entries |
| Masked variables | NAMESPACE and the KUBERNETES_* variables have empty values; they come after extra_env, so that the application cannot see the namespace or the API server address (shared container contract) |
| Site volume | Mounted at /www |
| Service port | None |
| External route | None |
| Liveness probe | TCP socket on port 8000; 30 s initial delay, 60 s period, failure threshold 10 (tolerant of slow user applications) |
| Readiness probe | TCP socket on port 8000; 15 s initial delay, 30 s period, failure threshold 3. It reports acceptance through the Pod Ready condition, so a Site whose application does not accept on port 8000 reads as starting, not as running (Hub interface behaviour). It does not remove the pod from routing: the site Service publishes not-ready addresses (14), so the static site, the Site API, and the other site containers stay reachable. The proxy reaches the application on 127.0.0.1, and the application has no Service of its own |
| Bind address | 0.0.0.0 (all interfaces). The proxy uses 127.0.0.1, but both TCP probes set no host, so the kubelet connects to the Pod IP. An application that binds only 127.0.0.1 fails both probes: readiness keeps the Site in preparing, and liveness restarts the container after about 10 minutes (60 s period × failure threshold 10) |
| Security context | Non-root, not privileged, no privilege escalation, root filesystem writable |
| Resources | From the Site tier table below |
| Files ConfigMap | The application contributes none (get_config_map_values returns {}) |
Custom application tiers (site/.../applications/custom/app_config.py):
| Tier | Memory (request / limit) | CPU (request / limit) |
|---|---|---|
normal |
512 Mi / 1 Gi | 100 m / 1000 m |
medium |
1 Gi / 2 Gi | 200 m / 2000 m |
high |
2 Gi / 3 Gi | 300 m / 3000 m |
The application shares the Site Pod and its storage boundary. The proxy is the only component that sends external traffic to it.
Proxy integration¶
The exporter's render data carries two values that select the backend:
proxy_app (the custom entry's proxy_type) and context_root
(core/.../model/site_exporter.py, proxy files ConfigMap render data); the
site location block (proxy/nginx/location.conf) includes
{{ proxy_app|default('default', true) }}-app.conf, after the auth includes
(basic.conf, then private.conf or public.conf):
| Configuration | Rendered include | Behaviour |
|---|---|---|
| No custom image | default-app.conf |
Static content: alias /www/html/; with the fancyindex listing (site proxy). |
proxy_type empty or None (the field is absent on the Site resource) |
default-app.conf |
The default(..., true) filter treats the empty value as unset: static content renders even though the custom container runs, and it receives no traffic. |
proxy_type: proxy |
proxy-app.conf |
proxy_pass http://127.0.0.1:8000/ plus the shared proxy.conf headers. |
proxy_type: fastcgi |
fastcgi-app.conf |
fastcgi_pass 127.0.0.1:8000 plus the FastCGI parameters from fastcgi.conf. |
In proxy mode the context root controls the upstream path:
context_root: site_context renders
proxy_pass http://127.0.0.1:8000<site_path> (the application sees the Site
prefix); any other value (including an unset one) renders ...:8000/ (the
application is mounted at /). FastCGI has no upstream path: the full
request path always reaches the application, and fastcgi.conf splits it
with fastcgi_split_path_info ^<site_path>(.*\.*)(.*)$.
The FastCGI template also reads a DOCUMENT_ROOT value: when the custom
application's extra_env defines DOCUMENT_ROOT, the decoded value arrives
in the render data (extra_env is added to the files ConfigMap data, unless the
name is already a platform variable) and is injected into the SCRIPT_FILENAME
and DOCUMENT_ROOT parameters; otherwise
the nginx $document_root variable applies. An extra_env entry can add a new
render variable, but it cannot replace one the platform sets (type,
site_type, site_path, hostname, trusted_ips, the OIDC variables, and so
on): the platform value wins, so a custom application cannot change the access
policy of its own site. The parameters are written for
PHP-style backends: large buffers (8 × 1600k), buffering off, 600 s connect,
send, and read timeouts.
The retention banner sub_filter from location.conf still applies: custom
application responses that contain </html> receive the retention markup
before the first occurrence.
Public sites¶
A custom application on a public site answers read-only requests only. The
content location runs public_auth.lua, which accepts GET, HEAD,
OPTIONS, PROPFIND, and SEARCH; every other method answers 403 before
the application sees the request. This applies to every path the application
serves, including its own routes. A token or a cookie does not change it,
because a public site does not run the authentication chain.
site.allow_public_custom_application controls whether a public site may
carry a custom image; it does not change the request methods. The reasons
are in 06-security.
Model and upgrade flow¶
flowchart TD
F["Hub form or Hub API"] --> V["Validate image and application fields"]
V --> CR["Site resource"]
CR --> O["Operator reconciliation"]
O --> SE["SiteExporter"]
SE --> SS["StatefulSet with site-custom :8000"]
SE --> CM["Proxy files ConfigMap"]
CM --> P["site-proxy backend selection"]
On a platform upgrade, the model rebuilds application images
(SiteBuilder.from_crd(..., upgrade=True)): every application whose name
resolves to a registered class except site-custom gets its image
rewritten to the built-in CONTAINER_IMAGE. The custom image is kept because
the platform knows nothing about its versions. A change to the custom
application fields is a restart-triggering Site update and causes the
operator to render the files and restart the Site Pod
(09).
Hub interface behaviour¶
The Settings tab of the Site view holds the custom application dialog
(hub/.../templates/hub/view-tab-settings.html,
hub/.../static/js/custom-application.js,
hub/.../static/js/list-editor.js).
- Image check in the browser. When you press Save, the dialog removes
leading and trailing whitespace from the image, checks it against
IMAGES_REGEX_JS, and stops the save when the value is not a full image reference. The error shows under the field and no request leaves the browser. The same check runs when the field loses focus. The server checks the value again, because the browser check is only a convenience. The stop usesinplace:presave: a handler that callspreventDefault()on that event cancels the save (inplace.js). - Status line. The tab renders one line of badges under the field for a
Site with a custom image:
runningwhen the Site steps have started,errorwhen thesite-customcontainer reports a problem, andpreparingin every other case.custom_application_status_frompasses the container entry toclassify_custom_application_container(hub/.../app/hub/custom_application.py), which maps the reason: a waitingContainerCreatingorPodInitializingis normal progress and keepspreparing; a running container that fails its readiness check reaches the Hub as aNotReadyentry and keepspreparingwith the labelwaiting for the applicationand an advice that says this is normal at start-up and lists the possible causes when it continues (a wrong port or interface, a slow start-up, an application error); a pull reason (ImagePullBackOff,ErrImagePull,ErrImageNeverPull,InvalidImageName), a crash (CrashLoopBackOff, a terminated container) andOOMKilledproduce theerrorstate with a short label (cannot pull image,application failed,out of memory) and a one-sentence advice; an unknown reason reportserrorwith the raw reason and no label. Theerrorbadge tooltip carries the container message, or the reason when the message is empty; the label tooltip carries the advice. The readiness probe above keeps a Site whose application does not accept on port8000inpreparinginstead of reportingrunning. The problem line carries two links.Go to error detailsopens the System tab.Restart nowposts the existing site-restart endpoint: changing the image reference restarts the Site through the operator on its own, and this link covers the case where the user fixed the image in the registry under the same tag, so no Site field changed. Thepreparingline with thewaiting for the applicationlabel also carriesRestart now, but notGo to error details. A Site update replaces a pod that is not ready on its own (09-lifecycle), so removing or changing the image recovers the Site; the link covers a fix pushed under the same tag. The line comes fromcustom_application_status()inhub/.../app/hub/custom_application.py, which reads the same Site system status the System tab uses (sites_service.get_site_system_status); the refresher reloads the tab, so the line follows the restart without a new endpoint. Site administrators only. - Tags. A reference without a tag and without a digest gets the hint that the Site runs the newest image at every restart.
- Dependent fields.
Proxy Mode,URL Pathand the environment rows stay editable while the image field is empty, and the hintThese settings apply after you set a Docker image.explains when they take effect. A raw API submission that carries those settings without an image reports them back: the form setscustom_application_settings_removedand the response message saysRemoved the custom application settings because no image is set. - Environment rows. The dialog edits one row per variable, with an
Add variablecontrol. This is the shared row-list editor ofhub/.../static/js/list-editor.js; the CORS domains, file extensions, and custom error redirects dialogs use the same mechanism for their single-value andCODE=/targetentries. The rows serialise into the comma-joinedcustom_application_extra_envstring the form and the API already accept, so the wire format does not change. A client-side mirror of the server rules flags a comma in a value, an invalid name, and a reserved name before the save. TheAdd variablecontrol creates a row only when every existing row holds a value in each of its required fields; otherwise it focuses the first required field left empty and briefly showsFill this row before adding another one.in the hint. The value of a variable is optional (NAME=is valid on the server), so only the name is required. A repeated click restarts the delay before the hint returns to the validation text. Whether a row may be submitted stays the validation rules' decision: a filled but invalid row does not block the control. - Values are not in the page. The tab HTML lists the variable names
only. The dialog reads the values from
GET /sites/<space>/<site_name>/custom-application/environment/when it opens, shows them masked behindShow values, and leaves the submitted field out of the form until that read succeeds — a save can therefore not delete variables the user never saw. The endpoint requires Site administrator access, the same person who can already change the values. Like every Hub API route, it answers CORS withAccess-Control-Allow-Origin: *and never withAccess-Control-Allow-Credentials: a browser page on another origin cannot send the session cookie, so it cannot read the values. The Hub session cookie isSameSite=Laxand host-only, and the Hub has its own host whenrouting.hub_hostis set. The dialog calls the endpoint from the Hub origin, and API clients use a bearer token. This is masking, not a secret store: the values remain base64-encoded entries on the Site resource, they reach the container as directenventries, and the exporter also writes the decoded values of every application into the{site_id}-filesConfigMap render data (SiteExporter.to_files_config_map), because the FastCGI template readsDOCUMENT_ROOTfrom there. A Site environment variable is therefore stored as configuration, not as a credential. - URL preview. The dialog shows
Your application will answer at: <url>for the selected URL path.
Use cases¶
- A per-Site web application: a dashboard, a form, a notebook server, or a Web Server Gateway Interface (WSGI) application, with the Site content as its data directory.
- An application that needs the Site authentication: it never authenticates itself, it trusts the proxy header.
- A replacement for the listing UI at the Site path, or an extra path under it.
Example usage¶
The Site owner adds the application to
spec.configuration.applications; the image alone enables it:
applications:
- name: site-custom
image: registry.example.org/team/app:1.2.3
proxy_type: proxy
context_root: root_context
extra_env:
- name: APP_MODE
value: production
The container listens on port 8000. proxy_type: fastcgi selects the
FastCGI integration instead, and context_root: site_context keeps the
application under the Site path. Only the platform administrator sets the
allowed registries
(custom application design).
Request flow¶
flowchart LR
B["Browser"] --> O["site-oauth2"]
O --> P["site-proxy"]
P -- "proxy_type: proxy" --> C["site-custom :8000"]
P -- "proxy_type: fastcgi" --> F["FastCGI socket"]
C --> V[("Site content /www/html")]
F --> V