Skip to content

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-invalid proxy_type or context_root with 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_type falls back to proxy and context_root to root_context when 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 parser parse_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_registries lists 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_application defaults to false. 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 from site.default_view_users/default_view_groups when the submission has none. The Hub API holds that revert until the caller confirms it: an update whose data does not set confirm_site_type_revert to true returns status ERROR with data.confirm_site_type_revert: true and 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 fields site_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_name adds 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 to null, and the chart does not fill them from serviceAccount.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 uses inplace:presave: a handler that calls preventDefault() 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: running when the Site steps have started, error when the site-custom container reports a problem, and preparing in every other case. custom_application_status_from passes the container entry to classify_custom_application_container (hub/.../app/hub/custom_application.py), which maps the reason: a waiting ContainerCreating or PodInitializing is normal progress and keeps preparing; a running container that fails its readiness check reaches the Hub as a NotReady entry and keeps preparing with the label waiting for the application and 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) and OOMKilled produce the error state with a short label (cannot pull image, application failed, out of memory) and a one-sentence advice; an unknown reason reports error with the raw reason and no label. The error badge 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 port 8000 in preparing instead of reporting running. The problem line carries two links. Go to error details opens the System tab. Restart now posts 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. The preparing line with the waiting for the application label also carries Restart now, but not Go 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 from custom_application_status() in hub/.../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 Path and the environment rows stay editable while the image field is empty, and the hint These 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 sets custom_application_settings_removed and the response message says Removed the custom application settings because no image is set.
  • Environment rows. The dialog edits one row per variable, with an Add variable control. This is the shared row-list editor of hub/.../static/js/list-editor.js; the CORS domains, file extensions, and custom error redirects dialogs use the same mechanism for their single-value and CODE=/target entries. The rows serialise into the comma-joined custom_application_extra_env string 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. The Add variable control 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 shows Fill 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 behind Show 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 with Access-Control-Allow-Origin: * and never with Access-Control-Allow-Credentials: a browser page on another origin cannot send the session cookie, so it cannot read the values. The Hub session cookie is SameSite=Lax and host-only, and the Hub has its own host when routing.hub_host is 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 direct env entries, and the exporter also writes the decoded values of every application into the {site_id}-files ConfigMap render data (SiteExporter.to_files_config_map), because the FastCGI template reads DOCUMENT_ROOT from 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