zeabur-template
Zeabur Template Knowledge Base
Always use
npx zeabur@latestto invoke Zeabur CLI. Never usezeaburdirectly or any other installation method. Ifnpxis not available, install Node.js first.
This skill provides comprehensive knowledge for creating, debugging, and publishing Zeabur templates. It combines reference documentation with battle-tested patterns from real template development.
External Documentation
For the latest schema and detailed docs, fetch from https://raw.githubusercontent.com/zeabur/zeabur-template-doc/main/:
| User Need | Document to Fetch | Path |
|---|---|---|
| Create a template from scratch | Step-by-step guide | docs/GUIDE.md |
| Convert docker-compose.yml | Migration guide | docs/DOCKER_COMPOSE_MIGRATION.md |
| Look up YAML fields or built-in variables | Technical reference | docs/REFERENCE.md |
| Naming, design patterns, best practices | Best practices | docs/BEST_PRACTICES.md |
| Debug template errors | Troubleshooting | docs/TROUBLESHOOTING.md |
| Pre-deployment checklist | Checklist | docs/CHECKLIST.md |
| Quick all-in-one overview | Comprehensive prompt | prompt.md |
The template YAML schema is also available at https://schema.zeabur.app/template.json and the prebuilt service schema at https://schema.zeabur.app/prebuilt.json.
Core Principles
0. All services MUST use PREBUILT_V2 with Docker images
Every service in a template MUST be template: PREBUILT_V2 with a Docker image as the source. Never use template: GIT, ARBITRARY_GIT, or GITHUB source — these are NOT supported in templates.
If the project does not have a published Docker image (on Docker Hub, GHCR, etc.), tell the user they need to build and publish a Docker image first before a template can be created. Do not attempt to work around this.
If the user asks you to build the image, follow this workflow:
- Clone the project repo and find its
Dockerfile(usually at repo root) - Study the Dockerfile to understand build stages — use the production stage (often named
runnerorproduction) - Study
docker-compose.ymlordocker-compose.fullapp.ymlfor the correct startup command, env vars, and volumes — this is the battle-tested production config - Build for amd64 (Zeabur servers are amd64, local Macs are arm64):
docker buildx build --platform linux/amd64 --target runner -t org/image:tag --push . - After pushing, verify the Docker Hub repo is public:
New repos under org accounts often default to private. Ifcurl -s "https://hub.docker.com/v2/repositories/ORG/IMAGE/" | grep is_privateis_private: true, the user must make it public on Docker Hub.
1. Never start from scratch
When asked to create a template, always look for existing configuration first:
- First check if a Docker image exists — search Docker Hub, GHCR (
ghcr.io/org/repo), or the project's CI/CD for published images. If none exists, stop and inform the user. - Search for the project's
docker-compose.yml,docker-compose.yaml, orcompose.yml - Look for Helm charts (
Chart.yaml,values.yaml) - Check the project's GitHub repo for any deployment YAML files
- Use these as the foundation to build the Zeabur template, not as a loose reference — they contain battle-tested environment variables, port mappings, volume mounts, and service dependencies
2. Iterate via runtime logs — never expect one-shot success
Even experienced humans cannot create a working template in one shot. The workflow is an iterative loop:
- Write/update the template YAML
- Deploy the template
- It will likely fail — check runtime logs to find the cause
- Fix the issue in the template
- Delete the project and redeploy from scratch
- Repeat until the template achieves one-click deployment success
This is the normal process, not a sign of failure. Do not try to get everything perfect before deploying — deploy early, read logs, and iterate.
3. Reuse from existing templates — never write common services from scratch
When your template needs a common service (PostgreSQL, Redis, MySQL, MongoDB, etc.), do not write the service definition yourself. Instead:
- Search for existing templates that already use that service:
npx zeabur@latest template search postgres - Find a template that includes the service you need
- Get the raw template YAML to see the exact service definition:
npx zeabur@latest template get -c TEMPLATE_CODE --raw - Copy the service definition directly from that template into yours
How to judge template trustworthiness:
- Many templates are created by regular users and may not work correctly
- Prefer templates with more deployments — higher deployment count = more battle-tested
- Prefer official templates over user-submitted ones — official templates are vetted by Zeabur team
CLI Commands for the Iteration Loop
Deploy a template:
npx zeabur@latest template deploy -f YOUR_TEMPLATE.yaml
For non-interactive mode (automation):
npx zeabur@latest template deploy -i=false \
-f YOUR_TEMPLATE.yaml \
--project-id PROJECT_ID \
--var PUBLIC_DOMAIN=myapp
List services (to get SERVICE_ID):
npx zeabur@latest service list --project-id PROJECT_ID
Check runtime logs:
npx zeabur@latest deployment log --service-id SERVICE_ID
Execute a command inside a running service (like docker exec):
npx zeabur@latest service exec --id SERVICE_ID -- SHELL_COMMAND
This is extremely useful for debugging — check file paths, env vars, test connectivity, inspect the filesystem, etc. Examples:
npx zeabur@latest service exec --id SERVICE_ID -- ls /app
npx zeabur@latest service exec --id SERVICE_ID -- env | grep DATABASE
npx zeabur@latest service exec --id SERVICE_ID -- nc -z localhost 5432
Restart a service (useful to clear ImagePullBackOff or force re-pull):
npx zeabur@latest service restart --id SERVICE_ID -i=false -y
Delete the project and start over:
npx zeabur@latest project delete --id PROJECT_ID
DANGEROUS OPERATION — Before deleting a project, you MUST ask the user for explicit confirmation, clearly stating the Project ID, Name, and createdAt timestamp. Never delete without confirmation.
Publish a new template:
npx zeabur@latest template create -f YOUR_TEMPLATE.yaml
This returns a template URL like https://zeabur.com/templates/XXXXXX with a template code.
Update an existing template:
npx zeabur@latest template update -c TEMPLATE_CODE -f YOUR_TEMPLATE.yaml
Quick Reference: Template Skeleton
# yaml-language-server: $schema=https://schema.zeabur.app/template.json
apiVersion: zeabur.com/v1
kind: Template
metadata:
name: ServiceName
spec:
description: |
English description (1-3 sentences)
icon: https://raw.githubusercontent.com/zeabur/service-icons/main/marketplace/service.svg
coverImage: https://example.com/cover.webp
tags:
- Category
variables: []
readme: |
# Service Name
English documentation...
services:
- name: service-name
icon: https://raw.githubusercontent.com/zeabur/service-icons/main/marketplace/service.svg
template: PREBUILT_V2
spec:
source:
image: image:tag
command: # MUST be inside source, alongside image
- /bin/sh
- -c
- /opt/app/startup.sh
ports:
- id: web
port: 8080
type: HTTP
healthCheck: # ensures port is ready before dependents start
type: TCP
port: web # references the port ID above
volumes:
- id: data
dir: /path/to/data
configs:
- path: /opt/app/startup.sh
permission: 493 # 0755
envsubst: false
template: |
#!/bin/sh
exec node server.js
env:
VAR_NAME:
default: value
expose: true
localization:
zh-TW:
description: ...
variables: []
readme: |
# ...
zh-CN:
description: ...
variables: []
readme: |
# ...
ja-JP:
description: ...
variables: []
readme: |
# ...
es-ES:
description: ...
variables: []
readme: |
# ...
id-ID:
description: ...
variables: []
readme: |
# ...
Quick Reference: Built-in Variables
| Variable | Purpose |
|---|---|
${PASSWORD} |
Auto-generated secure password |
${ZEABUR_WEB_URL} |
Full public URL (e.g. https://app.zeabur.app) |
${ZEABUR_WEB_DOMAIN} |
Domain only (e.g. app.zeabur.app) |
${CONTAINER_HOSTNAME} |
Internal hostname for inter-service communication |
${[PORTID]_PORT} |
Port value by port ID (e.g. ${DATABASE_PORT}) |
${PORT_FORWARDED_HOSTNAME} |
External hostname (for instructions) |
${[PORTID]_PORT_FORWARDED_PORT} |
External forwarded port (for instructions) |
The ZEABUR_<PORT_ID>_URL pattern: for a port named web, it becomes ${ZEABUR_WEB_URL}; for console, it becomes ${ZEABUR_CONSOLE_URL}.
Quick Reference: command Placement
IMPORTANT: command MUST be inside source, alongside image. NOT at spec level.
# WRONG -- command at spec level (will be IGNORED, container uses default CMD)
spec:
source:
image: python:3.12-slim
command:
- /bin/sh
- -c
- /opt/app/start.sh
# CORRECT -- command inside source
spec:
source:
image: python:3.12-slim
command:
- /bin/sh
- -c
- /opt/app/start.sh
Note: The external docs may show
commandatspeclevel. This is incorrect. Always placecommandinsidesourceas confirmed by the JSON schema atschema.zeabur.app/prebuilt.json.
Quick Reference: YAML Gotchas
# RISKY -- @ at start of value is a YAML reserved indicator (may cause parse errors)
description: @BotFather token
# SAFE -- quote the value or avoid @ at start
description: "Token from @BotFather for Telegram bot"
description: Telegram bot token from BotFather
Quick Reference: Docker Image ENTRYPOINT
Some base images have ENTRYPOINT set, which conflicts with command.
| Image | ENTRYPOINT | Problem |
|---|---|---|
ghcr.io/astral-sh/uv:python3.12-* |
uv |
command becomes args to uv, container shows uv help and exits |
node:* |
none | Safe to use |
python:* |
none | Safe to use |
If using an image with ENTRYPOINT, switch to a plain base image (e.g. python:3.12-slim-bookworm) or one without ENTRYPOINT.
Quick Reference: Headless Services (no HTTP)
If a service does NOT listen on any HTTP port (502 Bad Gateway), see zeabur-port-mismatch skill for the fix.
Quick Reference: Critical Rules
# WRONG -- using :latest tag (may serve cached/stale image)
image: rajnandan1/kener:latest
# CORRECT -- pin to specific version
image: rajnandan1/kener:4.0.16
# WRONG -- hardcoded password
POSTGRES_PASSWORD:
default: mypassword123
# CORRECT -- use ${PASSWORD}
POSTGRES_PASSWORD:
default: ${PASSWORD}
expose: true
# WRONG -- PUBLIC_DOMAIN gives incomplete URL
APP_URL:
default: https://${PUBLIC_DOMAIN}
# CORRECT -- ZEABUR_WEB_URL gives full URL
APP_URL:
default: ${ZEABUR_WEB_URL}
readonly: true
# WRONG -- other services can't reference without expose
POSTGRES_HOST:
default: ${CONTAINER_HOSTNAME}
# CORRECT -- expose + readonly for connection info
POSTGRES_HOST:
default: ${CONTAINER_HOSTNAME}
expose: true
readonly: true
# WRONG -- referencing variables without declaring dependency
- name: app
spec:
env:
DB: ${POSTGRES_HOST}
# CORRECT -- declare dependency first
- name: app
dependencies:
- postgresql
spec:
env:
DB: ${POSTGRES_HOST}
Domain Binding
Use domainKey on the service that needs a public domain. It maps to a variable defined in spec.variables with type: DOMAIN.
Single domain:
domainKey: PUBLIC_DOMAIN
Multiple domains (different ports):
domainKey:
- port: web
variable: ENDPOINT_DOMAIN
- port: console
variable: ADMIN_ENDPOINT_DOMAIN
Common Database Configs
See references/database-configs.md for copy-paste PostgreSQL, MySQL/MariaDB, Redis configs and standard volume paths.
Template Complexity Levels
See references/complexity-levels.md for the 5-level complexity guide (single service → large-scale multi-service platform).
Writing name, description, readme, icon, and coverImage
Where to collect information (in priority order):
- The project's GitHub repo README
- The project's official website
- Other public sources (blog posts, documentation sites, etc.)
Key rules:
- Do NOT copy-paste the original README. Write the introduction for this project's Zeabur template, not for the project itself. The readme should:
- Briefly introduce what the project is
- Focus on how to use this template (deployment steps, configuration, domain binding)
- Include important caveats and troubleshooting tips specific to Zeabur deployment
- Always include licensing and attribution. If the original project has an MIT, Apache, or other license:
- Mention the license in the readme
- Link to the original repo
- Link to the official website if available
- This is legally required -- never skip it
- icon and coverImage: Find the project's logo from their GitHub repo or official website. Use a direct URL to an image (SVG, PNG, or WebP). Always verify the URL returns HTTP 200:
Common pitfall: wrong branch name incurl -s -o /dev/null -w "%{http_code}" "URL"raw.githubusercontent.comURLs (developvsmainvsmaster).
Localization Requirements
6 languages required: en-US (in spec), zh-TW, zh-CN, ja-JP, es-ES, id-ID.
Translate: description, variables[].name, variables[].description, readme
Do NOT translate: key, type, ${VARIABLE_NAMES}, URLs
Hard-Won Lessons
See references/hard-won-lessons.md for battle-tested patterns: wait-for-db, first-run init markers, OOM recovery, ImagePullBackOff, and more.
See Also
zeabur-template-deploy— deploy a template via CLIzeabur-template-backup— backup a template to git