All templates must be organized according to the following directory structure:
templates/
└── template/
└── <template-name>/ # The folder name must match the template's name field
└── index.yaml # The template file must be named index.yaml
templates/
└── template/
├── formbricks/
│ └── index.yaml # formbricks template file
├── langflow/
│ └── index.yaml # langflow template file
└── fastgpt/
└── index.yaml # fastgpt template file
- The folder name must be consistent with the
metadata.namefield in the Template CR - The template file must be named
index.yaml - Folder names should use lowercase letters and hyphens; avoid underscores or other special characters
- The
metadata.nameof the Template CR must be hardcoded in lowercase letters and cannot use variables (such as${{ defaults.app_name }})
# Correct example
apiVersion: app.sealos.io/v1
kind: Template
metadata:
name: typesense # ✅ Hardcoded lowercase name
spec:
defaults:
app_name:
type: string
value: typesense-${{ random(8) }} # ✅ Variables can be used here
# Incorrect example
metadata:
name: ${{ defaults.app_name }} # ❌ Error: Variables cannot be usedResources within a template must be created in the following order:
Create the Template metadata definition first
apiVersion: objectstorage.sealos.io/v1
kind: ObjectStorageBucketDatabase resources must be created in the following order:
- ServiceAccount
- Role
- RoleBinding
- Cluster (the actual database instance)
- Job (if database initialization is needed)
Application resources must be created in the following order:
- ConfigMap (application configuration files)
- Deployment/StatefulSet (main application)
- Service
- Ingress
- App
Template CR
---
ObjectStorageBucket
---
Redis ServiceAccount
---
Redis Role
---
Redis RoleBinding
---
Redis Cluster
---
PostgreSQL ServiceAccount
---
PostgreSQL Role
---
PostgreSQL RoleBinding
---
PostgreSQL Cluster
---
PostgreSQL Init Job
---
Application StatefulSet
---
Application Service
---
Application Ingress
---
App
The App resource is the last resource in the template. It provides the Sealos dashboard entry point for the deployed application.
Complete and definitive schema — only these fields are valid:
apiVersion: app.sealos.io/v1
kind: App
metadata:
name: ${{ defaults.app_name }}
labels:
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
spec:
data:
url: https://${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}
displayType: normal
icon: <icon-url>
name: <human-readable app title>
type: linkAllowed spec fields (exhaustive list):
| Field | Type | Required | Description |
|---|---|---|---|
data.url |
string | yes | App access URL (Ingress host) |
displayType |
string | no | Display mode (normal) |
icon |
string | no | App icon URL |
name |
string | no | Human-readable title shown in dashboard |
type |
string | no | Entry type (link) |
When generating templates in this repository, treat these enum values as fixed: displayType must be normal, and type must be link. Do not emit alternatives such as standalone or web.
Do NOT add any other fields. Fields like menuData, nameColor, template, etc. do not exist in the App CRD and will cause strict decoding error: unknown field on apply.
Important distinction:
defaults: Used to store automatically generated values (such as random strings, random ports, etc.)inputs: Used to store values that require user input (such as email, API Key, custom configurations, etc.)
Values in defaults are automatically generated when the template is parsed and do not require user interaction:
defaults:
app_host:
type: string
value: typesense-${{ random(8) }} # ✅ With application name prefix
app_name:
type: string
value: typesense-${{ random(8) }} # ✅ Application name
api_key:
type: string
value: ${{ random(32) }} # ✅ Randomly generated secret keyNotes:
app_hostmust include an application name prefix (e.g.,typesense-${{ random(8) }})app_namemust include${{ random(8) }}to ensure uniqueness- Randomly generated configurations (secret keys, passwords, etc.) should be placed in
defaults, not ininputs
Values in inputs need to be filled in by the user at deployment time:
inputs:
admin_email:
description: 'Administrator email address'
type: string
default: ''
required: true
enable_feature_x:
description: 'Enable advanced feature X'
type: boolean
default: 'false'
required: falseWhen to use inputs:
- ✅ User's email address
- ✅ Custom domain name
- ✅ API Key for external services (needs to be provided by the user)
- ✅ Feature toggles (enable/disable certain features)
- ❌ Randomly generated secret keys (should be placed in defaults)
- ❌ Automatically generated configurations (should be placed in defaults)
Templates need to add locale and i18n configuration to support multiple languages:
spec:
locale: en # Default language
i18n:
zh:
description: '中文描述'apiVersion: app.sealos.io/v1
kind: Template
metadata:
name: example
spec:
title: 'Example App'
description: 'An example application for demonstration'
locale: en
i18n:
zh:
description: '一个用于演示的示例应用程序'The i18n configuration supports translation of the following fields:
description- Application description
localespecifies the default language, typically set toen- Currently only
zh(Chinese) translation is supported i18n.zh.descriptionshould use Simplified Chinese- Technical field names and default values do not need translation
- If the Chinese title is the same as
spec.title, it is recommended to omiti18n.zh.title
When creating Sealos templates, the categories field cannot be customized and must be selected from the following predefined options:
tool- Utility applicationsai- AI/Machine Learning related applicationsgame- Game applicationsdatabase- Database applicationslow-code- Low-code platformsmonitor- Monitoring applicationsdev-ops- DevOps toolsblog- Blog/Content management systemsstorage- Storage applicationsfrontend- Frontend applicationsbackend- Backend applications
categories:
- storage # Correct: Using a predefined category
- tool # Correct: Multiple categories can be selected
# - media # Error: Not in the allowed listSealos does not support emptyDir! All scenarios requiring temporary storage must be converted to persistent storage.
Incorrect example:
volumes:
- name: config-storage
emptyDir: {} # Error! Sealos does not support emptyDir
Correct approach:
- For StatefulSet: Use
volumeClaimTemplatesto create persistent storage - For Deployment: Consider whether storage is truly needed; if so, switch to StatefulSet
- For temporary configuration: Consider using ConfigMap or Secret
- For StatefulSet PVC tracking: set
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}on both the StatefulSet metadata labels and everyvolumeClaimTemplates[].metadata.labels, while preserving component labels such asapp.
Storage cannot create PersistentVolumeClaim independently; it must use the volumeClaimTemplates field within a Deployment or StatefulSet.
volumeClaimTemplates on a Deployment is a Sealos-specific extension. It works only when deployed through the Sealos Template API (POST /api/v2alpha/templates/raw). Standard Kubernetes kubectl apply will reject a Deployment with volumeClaimTemplates:
error: Deployment in version "v1" cannot be handled as a Deployment:
strict decoding error: unknown field "spec.volumeClaimTemplates"
When using kubectl apply as a fallback, you must handle this:
- If the resource is
kind: Deploymentwithspec.volumeClaimTemplates→ remove thevolumeClaimTemplatesfield and the correspondingvolumeMountsentries before applying, OR convert to aStatefulSet. - If the resource is
kind: StatefulSetwithspec.volumeClaimTemplates→ this is standard Kubernetes and works fine with kubectl apply.
Recommendation for template authors: When persistent storage is needed, prefer StatefulSet over Deployment to ensure compatibility with both the Template API and kubectl apply.
volumeClaimTemplates:
- metadata:
annotations:
path: /var/lib/headscale # Mount path
value: '1' # Fixed value
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/deploy-on-sealos: ${{ defaults.app_name }}
name: vn-varvn-libvn-headscale # Naming rules see below
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gimetadata.name reuses the value of metadata.annotations.path, with special characters replaced by "vn-":
/is replaced withvn--is replaced withvn-- Other special characters are also replaced with
vn-
For example:
/var/lib/headscale→vn-varvn-libvn-headscale/usr/src/app/upload→vn-usrvn-srcvn-appvn-upload/cache→vn-cache
The name of the ConfigMap must be the same as the metadata.name value of the application that mounts the ConfigMap.
Important reminder: All key names in the ConfigMap's data field must strictly follow the vn- conversion rules!
All configuration files should be placed in the same ConfigMap. The key names in data.<filename> must have special characters in the mount path replaced with "vn-":
Conversion rules:
- Replace
/in the path withvn- - Replace
-in the path withvn- - Replace
.in the path withvn- - Other special characters are also replaced with
vn-
Incorrect example (never do this):
data:
inifile: | # Error! Not using vn- conversion
content here
chart.ini: | # Error! Contains a dot
content hereCorrect example:
apiVersion: v1
kind: ConfigMap
metadata:
name: ${{ defaults.app_name }}
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
data:
# Original path: /etc/nginx/conf.d/default.conf
# After conversion: vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf
vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf: |
server {
listen 80;
...
}
# Original path: /tmp/chart.ini
# After conversion: vn-tmpvn-chartvn-ini
vn-tmpvn-chartvn-ini: |
[cluster]
seedlist = examplevolumes:
- name: vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf
configMap:
name: ${{ defaults.app_name }}
items:
- key: vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf
path: ./etc/nginx/conf.d/default.conf
defaultMode: 420volumeMounts:
- name: vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: ./etc/nginx/conf.d/default.conf# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: ${{ defaults.app_name }}
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
data:
vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf: |
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
vn-appvn-configvn-ymlvn-: |
database:
host: localhost
port: 5432
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${{ defaults.app_name }}
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
spec:
revisionHistoryLimit: 1
template:
spec:
automountServiceAccountToken: false
containers:
- name: ${{ defaults.app_name }}
volumeMounts:
- name: vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: ./etc/nginx/conf.d/default.conf
- name: vn-appvn-configvn-ymlvn-
mountPath: /app/config.yml
subPath: ./app/config.yml
volumes:
- name: vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf
configMap:
name: ${{ defaults.app_name }}
items:
- key: vn-etcvn-nginxvn-confvn-dvn-defaultvn-conf
path: ./etc/nginx/conf.d/default.conf
defaultMode: 420
- name: vn-appvn-configvn-ymlvn-
configMap:
name: ${{ defaults.app_name }}
items:
- key: vn-appvn-configvn-ymlvn-
path: ./app/config.yml
defaultMode: 420- Application workloads (Deployment/StatefulSet/DaemonSet) must include
metadata.labels.app, and the value must be consistent with the resource'smetadata.name - The value of
cloud.sealos.io/app-deploy-managermust be consistent with the resource'smetadata.namevalue - The
metadata.nameof each template's main application (the frontend application providing the public-facing port) must be${{ defaults.app_name }} - Other components should be named based on
${{ defaults.app_name }}plus a component identifier, for example:${{ defaults.app_name }}-server${{ defaults.app_name }}-ml${{ defaults.app_name }}-redis
- Application Service must include
metadata.labels.appandmetadata.labels.cloud.sealos.io/app-deploy-manager, andmetadata.name, both labels, andspec.selector.appmust be exactly the same - Runtime component-level ConfigMap must include
metadata.labels.appandmetadata.labels.cloud.sealos.io/app-deploy-manager, and both must be consistent withmetadata.name; ConfigMaps used only by init containers to copy initial config into persistent storage must not include either label - Root-path Ingress rules (
pathType: Prefix,path: /) must keepmetadata.nameconsistent withmetadata.labels.cloud.sealos.io/app-deploy-managerand backendservice.name; non-root or non-Prefix Ingress rules may use a distinct Ingress name and backend service
The primary business container name must be consistent with the workload
metadata.name value. Sidecar/helper containers may use distinct descriptive
names when they are not the main business container.
# Correct example
metadata:
name: ${{ defaults.app_name }}
spec:
template:
spec:
containers:
- name: ${{ defaults.app_name }} # Must be consistent with metadata.name
- name: metrics-sidecar # Allowed: helper/sidecar container
# Correct example for sub-components
metadata:
name: ${{ defaults.app_name }}-ml
spec:
template:
spec:
containers:
- name: ${{ defaults.app_name }}-ml # Must be consistent with metadata.name# Main application (correct)
metadata:
name: ${{ defaults.app_name }}
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
# Sub-component (correct)
metadata:
name: ${{ defaults.app_name }}-ml
labels:
app: ${{ defaults.app_name }}-ml
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}-ml
# Incorrect example
metadata:
name: ${{ defaults.app_name }}-server
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }} # Error: Label value does not match nameDatabase resources (Clusters created via kubeblocks) use dbprovider labels instead of cloud.sealos.io/app-deploy-manager.
Required labels:
kb.io/databasemust identify the KubeBlocks database/version.sealos-db-provider-crmust equalmetadata.name.clusterdefinition.kubeblocks.io/namemust identify the database engine, such aspostgresql,apecloud-mysql,mongodb,redis, orkafka.- Related Pods, Services, and OpsRequests should carry
app.kubernetes.io/instance=<database name>for dbprovider detail views. Generated templates may also place this label on the Cluster for consistency, but dbprovider's Cluster list path primarily keys onclusterdefinition.kubeblocks.io/name.
# Correct labels for database resources
metadata:
name: ${{ defaults.app_name }}-redis
labels:
kb.io/database: redis-7.2.7
sealos-db-provider-cr: ${{ defaults.app_name }}-redis
app.kubernetes.io/instance: ${{ defaults.app_name }}-redis
clusterdefinition.kubeblocks.io/name: redisObject storage environment variable configuration must follow this format:
env:
- name: S3_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: object-storage-key
key: accessKey
- name: S3_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: object-storage-key
key: secretKey
- name: S3_BUCKET
valueFrom:
secretKeyRef:
name: object-storage-key-${{ SEALOS_SERVICE_ACCOUNT }}-${{ defaults.app_name }}
key: bucket
- name: S3_ENDPOINT
value: "https://$(BACKEND_STORAGE_MINIO_EXTERNAL_ENDPOINT)"
- name: BACKEND_STORAGE_MINIO_EXTERNAL_ENDPOINT
valueFrom:
secretKeyRef:
name: object-storage-key
key: external
- name: S3_PUBLIC_DOMAIN
value: "https://$(BACKEND_STORAGE_MINIO_EXTERNAL_ENDPOINT)"
- name: S3_ENABLE_PATH_STYLE
value: "1"object-storage-keyis a fixed secret name (does not include the application name)- Only the bucket's secret name includes the application name:
object-storage-key-${{ SEALOS_SERVICE_ACCOUNT }}-${{ defaults.app_name }}. Bucket-scoped variants may append a lowercase suffix, for exampleobject-storage-key-${{ SEALOS_SERVICE_ACCOUNT }}-${{ defaults.app_name }}-public. - S3_ENDPOINT and S3_PUBLIC_DOMAIN use environment variable references:
$(BACKEND_STORAGE_MINIO_EXTERNAL_ENDPOINT) - S3_ENABLE_PATH_STYLE must be set to "1"
Ingress must use the following format:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ${{ defaults.app_name }}
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager-domain: ${{ defaults.app_host }}
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: 32m
nginx.ingress.kubernetes.io/server-snippet: |
client_header_buffer_size 64k;
large_client_header_buffers 4 128k;
nginx.ingress.kubernetes.io/ssl-redirect: 'true'
nginx.ingress.kubernetes.io/backend-protocol: HTTP
nginx.ingress.kubernetes.io/client-body-buffer-size: 64k
nginx.ingress.kubernetes.io/proxy-buffer-size: 64k
nginx.ingress.kubernetes.io/proxy-send-timeout: '300'
nginx.ingress.kubernetes.io/proxy-read-timeout: '300'
nginx.ingress.kubernetes.io/configuration-snippet: |
if ($request_uri ~* \.(js|css|gif|jpe?g|png)) {
expires 30d;
add_header Cache-Control "public";
}
spec:
rules:
- host: ${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: ${{ defaults.app_name }}
port:
number: <port-number>
tls:
- hosts:
- ${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}
secretName: ${{ SEALOS_CERT_SECRET_NAME }}metadata.namemust be${{ defaults.app_name }}- Must include the
cloud.sealos.io/app-deploy-manager-domainlabel ssl-redirectdefaults to'true'- Includes a configuration-snippet for static resource caching
- Backend service name must be
${{ defaults.app_name }}
All PostgreSQL environment variables are obtained from the secret automatically created by kubeblocks. The secret name format is: ${{ defaults.app_name }}-pg-conn-credential
The secret contains the following keys:
endpoint: Full connection endpoint (host:port)host: Hostnamepassword: Passwordport: Port numberusername: Username (usually postgres)
env:
# Configure host and port separately
- name: DB_HOSTNAME
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-pg-conn-credential
key: host
- name: DB_PORT
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-pg-conn-credential
key: port
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-pg-conn-credential
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-pg-conn-credential
key: password
# Or use endpoint to directly get host:port
- name: DB_ENDPOINT
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-pg-conn-credential
key: endpointOther databases follow the same approved secret policy, with service-FQDN exceptions where KubeBlocks only exposes credentials:
- Redis:
${{ defaults.app_name }}-redis-redis-account-default(legacy${{ defaults.app_name }}-redis-account-defaultmay be accepted); host/port may use${{ defaults.app_name }}-redis-redis-redis.${{ SEALOS_NAMESPACE }}.svc.cluster.localand6379 - MySQL:
${{ defaults.app_name }}-mysql-conn-credential - MongoDB:
${{ defaults.app_name }}-mongo-mongodb-account-root(or${{ defaults.app_name }}-mongodb-mongodb-account-rootwhen the Cluster name uses${{ defaults.app_name }}-mongodb); MongoDB URLs may use${{ defaults.app_name }}-mongo-mongodb.${{ SEALOS_NAMESPACE }}.svc:27017
PostgreSQL does not create a database by default. If the application needs a custom database (rather than using the default postgres database), it must be created via a Job.
Important specification:
- The database name should use the application's default value and should not be a user input parameter
- The database name should be related to the application name, typically using the application's short name or identifier
- For example: the langflow application uses the 'langflow' database, the fastgpt application uses the 'fastgpt' database
apiVersion: batch/v1
kind: Job
metadata:
name: ${{ defaults.app_name }}-pg-init
spec:
backoffLimit: 3
template:
spec:
containers:
- name: pgsql-init
image: postgres:16-alpine
imagePullPolicy: IfNotPresent
env:
- name: PG_PASSWORD
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-pg-conn-credential
key: password
- name: PG_ENDPOINT
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-pg-conn-credential
key: endpoint
- name: PG_DATABASE
value: langflow
command:
- /bin/sh
- -c
- |
set -eu
for i in $(seq 1 60); do
if pg_isready -h "${PG_ENDPOINT%:*}" -p "${PG_ENDPOINT##*:}" -U postgres -d postgres >/dev/null 2>&1; then
break
fi
sleep 2
done
pg_isready -h "${PG_ENDPOINT%:*}" -p "${PG_ENDPOINT##*:}" -U postgres -d postgres >/dev/null 2>&1
if ! psql "postgresql://postgres:$(PG_PASSWORD)@$(PG_ENDPOINT)/postgres" -tAc "SELECT 1 FROM pg_database WHERE datname='$(PG_DATABASE)'" | grep -q 1; then
psql "postgresql://postgres:$(PG_PASSWORD)@$(PG_ENDPOINT)/postgres" -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$(PG_DATABASE)\";"
fi
restartPolicy: OnFailure
ttlSecondsAfterFinished: 300Notes:
- Job name uses the format
${{ defaults.app_name }}-pg-init - Uses the
postgres:16-alpineimage to keep it lightweight ttlSecondsAfterFinished: 300ensures the Job is automatically cleaned up 5 minutes after completion- The initialization script must wait for PostgreSQL to be ready first (e.g.,
pg_isready) - The initialization script must be idempotent (check
pg_databasefirst, create only if it does not exist) - The database name should be hardcoded in the template, using the application's default database name (e.g., 'langflow' in the example above)
Important: Services must reference each other using Fully Qualified Domain Names (FQDN); direct service names cannot be used.
FQDN format: <service-name>.${{ SEALOS_NAMESPACE }}.svc.cluster.local
# Correct example: Using FQDN
env:
- name: WORKER_URL
value: http://${{ defaults.app_name }}-worker.${{ SEALOS_NAMESPACE }}.svc.cluster.local:4003
- name: COUCH_DB_URL
value: http://${{ defaults.app_name }}-svc-couchdb.${{ SEALOS_NAMESPACE }}.svc.cluster.local:5984
- name: REDIS_URL
value: redis://:$(REDIS_PASSWORD)@${{ defaults.app_name }}-redis-redis-redis.${{ SEALOS_NAMESPACE }}.svc:6379
# Incorrect example: Using service name directly
# - name: WORKER_URL
# value: http://worker-service:4003 # Error: May fail to resolveNote: Although the .svc.cluster.local suffix can be omitted in some cases (as in the REDIS_URL example above), it is recommended to always include the full domain name to ensure cross-namespace compatibility and clarity.
Important: If an environment variable references another environment variable, the referenced variable must be defined before the variable that references it.
env:
# Correct example: REDIS_PASSWORD comes first, REDIS_URL comes after
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: ${{ defaults.app_name }}-redis-redis-account-default
key: password
- name: REDIS_URL
value: redis://:$(REDIS_PASSWORD)@${{ defaults.app_name }}-redis-redis-redis.${{ SEALOS_NAMESPACE }}.svc:6379
# Incorrect example: If REDIS_URL is defined before REDIS_PASSWORD
# - name: REDIS_URL
# value: redis://:$(REDIS_PASSWORD)@... # Error: REDIS_PASSWORD is not defined yet
# - name: REDIS_PASSWORD
# valueFrom: ...This is because Kubernetes parses environment variables in the order they appear in the YAML. If a referenced variable has not been defined yet, the reference will fail.
All application Deployments or StatefulSets must include the following configurations:
- automountServiceAccountToken: Must be set to
falseto avoid unnecessary permission exposure. Set it totrueonly when the application explicitly needs the Kubernetes API/service account token, evidenced by Kubernetes integration settings,serviceAccountName, orsealos.io/service-account-token-reasonin workload annotations. - revisionHistoryLimit: Must be set to
1to reduce resources consumed by historical revisions - imagePullSecrets: Omit for public images. For private-registry images, reference only the app-scoped pull Secret
${{ defaults.app_name }} - metadata.annotations: Must include the following annotations:
originImageName: Original image namedeploy.cloud.sealos.io/minReplicas: Minimum replica count, typically set to'1'deploy.cloud.sealos.io/maxReplicas: Maximum replica count, typically set to'1'
Recommended registry pull Secret model:
- Public-image managed workloads omit
imagePullSecrets - For private GHCR images,
sealos-deploycreates or refreshes${{ defaults.app_name }}from localghCLI credentials and the workload may reference it throughimagePullSecrets - If a private-registry template is deployed outside
sealos-deploy, the operator must create the Secret manually before applying the workload
apiVersion: apps/v1
kind: Deployment # or StatefulSet
metadata:
name: ${{ defaults.app_name }}
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
annotations:
originImageName: example/app:1.0.0 # Required: Original image name
deploy.cloud.sealos.io/minReplicas: '1' # Required: Minimum replica count
deploy.cloud.sealos.io/maxReplicas: '1' # Required: Maximum replica count
spec:
revisionHistoryLimit: 1 # Must be set to 1
template:
spec:
automountServiceAccountToken: false # Default; only set true with evidenced Kubernetes API token need
containers:
- name: ${{ defaults.app_name }}
# Other container configuration...apiVersion: apps/v1
kind: Deployment
metadata:
name: ${{ defaults.app_name }}
annotations:
originImageName: example/app:1.0.0
deploy.cloud.sealos.io/minReplicas: '1'
deploy.cloud.sealos.io/maxReplicas: '1'
labels:
app: ${{ defaults.app_name }}
cloud.sealos.io/app-deploy-manager: ${{ defaults.app_name }}
spec:
revisionHistoryLimit: 1 # Revision history limit set to 1
replicas: 1
selector:
matchLabels:
app: ${{ defaults.app_name }}
template:
metadata:
labels:
app: ${{ defaults.app_name }}
spec:
automountServiceAccountToken: false # Disable automatic service account token mounting
containers:
- name: ${{ defaults.app_name }}
image: example/app:1.0.0
imagePullPolicy: IfNotPresentImportant: The resources field of all containers must include both requests and limits.
All containers in application Deployments or StatefulSets must use the fixed Sealos resource ladder. Do not invent intermediate values during template generation or resource tuning.
Allowed limits.cpu values use canonical Kubernetes quantities:
100m(0.1 core)200m(0.2 core)500m(0.5 core)12348
Allowed limits.memory values:
128Mi256Mi512Mi1024Mi2048Mi4096Mi8192Mi16384Mi
requests must be derived from limits by dropping the last numeric digit:
| limits | requests |
|---|---|
cpu: 100m |
cpu: 10m |
cpu: 200m |
cpu: 20m |
cpu: 500m |
cpu: 50m |
cpu: 1 |
cpu: 100m |
cpu: 2 |
cpu: 200m |
cpu: 3 |
cpu: 300m |
cpu: 4 |
cpu: 400m |
cpu: 8 |
cpu: 800m |
memory: 128Mi |
memory: 12Mi |
memory: 256Mi |
memory: 25Mi |
memory: 512Mi |
memory: 51Mi |
memory: 1024Mi |
memory: 102Mi |
memory: 2048Mi |
memory: 204Mi |
memory: 4096Mi |
memory: 409Mi |
memory: 8192Mi |
memory: 819Mi |
memory: 16384Mi |
memory: 1638Mi |
Default lightweight application quota:
resources:
requests:
cpu: 20m
memory: 25Mi
limits:
cpu: 200m
memory: 256MiStandard backend or broker quota after validation:
resources:
requests:
cpu: 50m
memory: 51Mi
limits:
cpu: 500m
memory: 512MiHeavy workload quota:
resources:
requests:
cpu: 200m
memory: 204Mi
limits:
cpu: 2
memory: 2048MiInvalid examples:
# Incorrect: non-ladder values
resources:
requests:
cpu: 30m
memory: 160Mi
limits:
cpu: 300m
memory: 384Mi
# Incorrect: requests copied from old ratio guidance instead of deriving from limits
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1
memory: 1024Mi
# Incorrect: G/Gi forms can make Sealos Template API quota preview parse memory as 0; use Mi ladder values
resources:
requests:
cpu: 200m
memory: 200Mi
limits:
cpu: 2
memory: 2GTuning guidance:
- Move only between allowed
limitsladder values. - Recompute
requestsfrom the selectedlimits; do not preserve old requests. - If a StatefulSet fails readiness at a lower resource tier, recreate or cleanly roll the Pod before testing the next tier so a stale non-ready Pod is not mistaken for the next tier's result.
- Choose the lowest tier that becomes Ready and passes application-level probes.
The image pull policy for all containers must be set to IfNotPresent:
spec:
template:
spec:
containers:
- name: ${{ defaults.app_name }}
image: example/app:1.0.0
imagePullPolicy: IfNotPresent # Must use IfNotPresentThis helps to:
- Reduce unnecessary image pulls and improve deployment speed
- Reduce pressure on the image registry
- Save network bandwidth
(More specifications and best practices to be added)