Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions apps/docs/content/guides/backup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Zerops auto-backs up databases and storage daily (00:00-01:00 UTC) with X25519 e
## Supported Services
MariaDB, PostgreSQL, Qdrant, Elasticsearch, NATS, Meilisearch, Shared Storage.

**ClickHouse**: not on the standard auto-backup path — back it up with the native `BACKUP ALL ...` SQL command (super user), stored as `.tar.gz`.

**Not supported**: Runtimes, Object Storage (use S3 lifecycle policies), Valkey/KeyDB (in-memory).

## Schedule Options
Expand Down Expand Up @@ -40,15 +42,15 @@ End-to-end with X25519 per-project keys. Decrypted only on download.
7 days after service or project deletion before backups are permanently removed.

## Backup Formats by Service
| Service | Format |
|---------|--------|
| PostgreSQL | pg_dump |
| MariaDB | mysqldump |
| Elasticsearch | elasticdump (.gz) |
| Meilisearch | .dump |
| Qdrant | .snapshot |
| NATS | .tar.gz |
| Shared Storage | filesystem archive |
| Service | Tool → Format |
|---------|---------------|
| PostgreSQL | `pg_dump` → `.zip` (per-schema custom-format `-Fc` dumps) |
| MariaDB | `mariabackup` → `.xb.gz` (xbstream + gzip) — **not** `mysqldump` (that is the manual-export tool, a different operation) |
| Elasticsearch | elasticdump → `.gz` |
| Meilisearch | `.dump` |
| Qdrant | `.snapshot` |
| NATS | `.tar.gz` |
| Shared Storage | tar → `.tar.gz` |

## Gotchas
1. **Object Storage has no Zerops backup**: Use S3 lifecycle policies or external backup
Expand Down
9 changes: 6 additions & 3 deletions apps/docs/content/guides/cdn.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,16 @@ DNS TTL: 30 seconds. Geo-steering routes to nearest node. EU Prague is fallback
Wildcard must be at end. Use `$` suffix for exact file match.

### Purge via zsc

Signature: `zsc cdn purge <domain> [path]` — the **domain is required first**; the path/pattern is the optional second arg (defaults to `*`). Works for **Static-Mode CDN only** — Object-Storage CDN content is purged via the REST API, not `zsc`. Run from a container whose CDN domain is active.
```bash
zsc cdn purge /* # Purge all cached content
zsc cdn purge /images/* # Purge directory
zsc cdn purge /style.css$ # Purge exact file
zsc cdn purge example.com # Purge all cached content for the domain
zsc cdn purge example.com "/images/*" # Purge a directory
zsc cdn purge example.com "/style.css$" # Purge exact file
```

## Gotchas
1. **30-day fixed TTL**: Cannot be changed — `Cache-Control: max-age=3600` has no effect on CDN
2. **No wildcard domains on static CDN**: `*.domain.com` is not supported
3. **Purge wildcards at end only**: `/images/*.jpg` is invalid — use `/images/*`
4. **CDN URLs are project-scoped env vars**: `${storageCdnUrl}`, `${staticCdnUrl}`, `${apiCdnUrl}` are referenced directly with no hostname prefix (unlike service vars like `${storage_apiUrl}`)
9 changes: 5 additions & 4 deletions apps/docs/content/guides/choose-cache.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ description: "**Use Valkey.** KeyDB development has stalled and is effectively d

| Need | Choice | Why |
|------|--------|-----|
| **Any caching need** | **Valkey** (default) | Active development, full HA, Redis-compatible |
| **Any caching need** | **Valkey** (default) | Active development, optional HA, Redis-compatible |
| Legacy KeyDB apps | KeyDB | Only if migrating existing KeyDB deployment |

## Valkey (Default Choice)

- Redis-compatible drop-in replacement
- HA: 3 nodes (1 master + 2 replicas) with automatic failover
- Ports: 6379 (non-TLS), 6380 (TLS), 7000 (read replica non-TLS), 7001 (read replica TLS)
- Connection: `redis://${user}:${password}@${hostname}:6379`
- Version: use `valkey@7.2` — the platform rejects `valkey@8` at import (`serviceStackTypeNotFound`)
- Mode: default **NON_HA** (single node), immutable after creation. HA (3 nodes — 1 master + 2 replicas, automatic failover) is opt-in via `mode: HA`
- Ports: 6379 (non-TLS), 6380 (TLS); **HA-only**: 7000 (read replica non-TLS), 7001 (read replica TLS)
- Connection: `redis://${hostname}:6379` — Valkey runs **unauthenticated** on Zerops; there are NO `user`/`password` env vars. Do **not** template `${cache_user}`/`${cache_password}` — they don't exist and produce a broken DSN
- HA detail: Ports 6379/6380 on replicas forward traffic to current master (Zerops-specific, not native Valkey)

## KeyDB (Deprecated)
Expand Down
16 changes: 8 additions & 8 deletions apps/docs/content/guides/choose-database.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,35 @@ title: "Choosing a Database on Zerops"
description: "**Use PostgreSQL** for everything unless you have a specific reason not to. It's the best-supported database on Zerops with full HA, read replicas, and pgBouncer."
---

**Use PostgreSQL** for everything unless you have a specific reason not to. It's the best-supported database on Zerops with full HA, read replicas, and pgBouncer.
**Use PostgreSQL** for everything unless you have a specific reason not to the best-supported database on Zerops, with optional HA, read replicas, and pgBouncer. Default mode is **NON_HA** (single node); HA is opt-in and immutable after creation.

## Decision Matrix

| Need | Choice | Why |
|------|--------|-----|
| **General-purpose** | **PostgreSQL** (default) | Full HA, read replicas, pgBouncer, best Zerops support |
| **General-purpose** | **PostgreSQL** (default) | Optional HA, read replicas, pgBouncer, best Zerops support |
| MySQL compatibility | MariaDB | MaxScale routing, async replication |
| Analytics / OLAP | ClickHouse | Columnar storage, ReplicatedMergeTree, 4 protocol ports |

## PostgreSQL (Default Choice)

- HA: 3 nodes (1 primary + 2 replicas)
- Ports: 5432 (primary), 5433 (read replicas), 6432 (external TLS via pgBouncer)
- Connection: `postgresql://${user}:${password}@${hostname}:5432/${db}`
- Read scaling: Use port 5433 for read-heavy workloads
- Mode: default **NON_HA** (single node), immutable. HA (3 nodes: 1 primary + 2 replicas) is opt-in via `mode: HA`
- Ports: 5432 (primary / RW); **HA-only**: 5433 (read replicas); 6432 (external TLS via pgBouncer)
- Connection: the generated `${connectionString}` is `postgresql://${user}:${password}@${hostname}:5432` (no database path). Append `/${dbName}` yourself if your driver needs one — the db-name var is `dbName` (default `db`)
- Read scaling: port 5433 routes to read replicas — **HA mode only** (a NON_HA single node has no 5433)

## MariaDB

- HA: MaxScale routing with async replication
- Port: 3306
- Connection: `mysql://${user}:${password}@${hostname}:3306/${db}`
- Connection: the generated `${connectionString}` is `mysql://${user}:${password}@${hostname}:3306` (no database path); append `/${dbName}` if your driver needs one
- Use when: Application requires MySQL wire protocol

## ClickHouse

- HA: 3 data nodes, replication factor 3
- Ports: 9000 (native), 8123 (HTTP), 9004 (MySQL), 9005 (PostgreSQL)
- Requires `ReplicatedMergeTree` engine in HA mode
- HA: replicated databases use a `Replicated(...)` engine `ON CLUSTER`; tables use a `Replicated*MergeTree` engine (without `ON CLUSTER`)
- Use when: Analytics, time-series, OLAP workloads

## Gotchas
Expand Down
1 change: 0 additions & 1 deletion apps/docs/content/guides/choose-queue.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ NATS exposes **two distinct messaging shapes**. Pick ONE per recipe and write ya
- HA: 3 brokers, 6 partitions, replication factor 3
- Storage: Up to 40GB RAM + 250GB persistent
- Topic retention: **Indefinite** (no time or size limits)
- Schema Registry: Port 8081 (if enabled)

## Gotchas
1. **NATS config changes need restart**: No hot-reload — changing env vars requires service restart
Expand Down
15 changes: 7 additions & 8 deletions apps/docs/content/guides/choose-runtime-base.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@ title: "Choosing a Runtime Base on Zerops"
description: "**Use Alpine** as the default base for all services. Use Ubuntu only when you need system packages not available in Alpine. Use Docker only for pre-built images."
---

**Use Alpine** as the default base for all services. Use Ubuntu only when you need system packages not available in Alpine. Use Docker only for pre-built images.
**Use Alpine** as the default base for all services. Switch to Ubuntu only for **glibc** needs (musl incompatibility): CGO-enabled Go, glibc-built Python/C-extension wheels, or the **Deno** runtime (no Alpine build). Needing a package is NOT itself a reason — both bases install packages (`sudo apk add` / `sudo apt-get install`). Use Docker only for pre-built images.

## Decision Matrix

| Need | Choice | Why |
|------|--------|-----|
| **Any standard app** | **Alpine** (default) | ~5MB, fast, secure, sufficient for 95% of apps |
| System packages (apt) | Ubuntu | Full Debian ecosystem, ~100MB |
| glibc / CGO / C-extensions / Deno | Ubuntu | musl-incompatible binaries; Deno has no Alpine build (~100MB) |
| Pre-built Docker images | Docker | VM-based, bring your own image |
| CGO / native libs | Ubuntu | Better glibc compatibility than Alpine's musl |

## Alpine (Default)

- Size: ~5MB base
- Package manager: `apk add`
- Best for: All runtimes (Node.js, Python, Go, Rust, Java, PHP, etc.)
- Package manager: `sudo apk add --no-cache <pkg>` (sudo required — containers run as the `zerops` user)
- Best for: nearly all runtimes (Node.js, Python, Go, Rust, Java, PHP, Gleam, etc.). Exception: **Deno** has no Alpine build — it requires `os: ubuntu`
- Zerops uses Alpine as default base for all managed runtimes

## Ubuntu

- Size: ~100MB base
- Package manager: `apt-get install`
- Version: 24.04 LTS
- Use when: You need packages not available in Alpine, or need glibc (not musl)
- Package manager: `sudo apt-get update && sudo apt-get install -y <pkg>` (sudo required)
- Version: 24.04 LTS (22.04 also available)
- Use when: you need glibc (musl incompatibility) — CGO-linked Go, glibc-built C-extensions, or the Deno runtime (no Alpine build). Needing a package is NOT a reason — both bases install packages
- Example: Go apps with CGO, Python packages with C extensions that don't compile on musl

## Docker
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/content/guides/choose-search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ description: "**Use Meilisearch** for simple full-text search. Use **Elasticsear
- Cluster support with multiple nodes
- Port: 9200 (HTTP only)
- Auth: `elastic` user with auto-generated password
- Plugins via `PLUGINS` env var (comma-separated)
- JVM heap: `HEAP_PERCENT` env var (default 50%)
- Plugins via `PLUGINS` (set in `envSecrets`, comma-separated)
- JVM heap: `HEAP_PERCENT` (in `envSecrets`, default 50%)
- Min RAM: 0.25 GB

## Typesense (Fast Autocomplete)
Expand All @@ -49,4 +49,4 @@ description: "**Use Meilisearch** for simple full-text search. Use **Elasticsear
1. **Meilisearch has no HA**: Single-node only — for HA full-text search, use Elasticsearch or Typesense
2. **Qdrant is internal-only**: Cannot be exposed publicly — access via your runtime service
3. **Typesense API key is immutable**: Cannot change `apiKey` after service creation
4. **Elasticsearch plugins require restart**: Changing `PLUGINS` env var needs service restart
4. **Elasticsearch plugins require restart**: Changing `PLUGINS` (in `envSecrets`) needs service restart
10 changes: 6 additions & 4 deletions apps/docs/content/guides/ci-cd.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: zeropsio/actions@main
- uses: zeropsio/actions@v1.0.2
with:
access-token: ${{ secrets.ZEROPS_TOKEN }}
service-id: <service-id>
service-id: ${{ secrets.ZEROPS_SERVICE_ID }}
```

- `access-token`: From Settings → Access Token Management
- `service-id`: From service URL or three-dot menu → Copy Service ID
- The compact `zeropsio/actions` wrapper exposes only `access-token`/`service-id` — it **cannot pass `--setup`**. For a multi-setup `zerops.yaml`, install zcli and run `zcli push --service-id "${{ secrets.ZEROPS_SERVICE_ID }}" --setup <name>` instead

## GitLab Integration (Webhook)

Expand All @@ -40,15 +41,16 @@ jobs:
4. Choose trigger: **New tag** (optional regex) or **Push to branch**

## Skip Pipeline
Include `ci skip` or `skip ci` in commit message (case-insensitive).
Include `[ci skip]` or `[skip ci]` (with the square brackets) in the commit message (case-insensitive).

## Disconnect
Service detail → Build, Deploy, Run → Stop automatic build trigger.

## Gotchas
1. **Full repo access required**: Webhook integration needs full access to create/manage webhooks
2. **`ci skip` in commit message**: Prevents pipeline trigger — useful for docs-only changes
2. **`[ci skip]` in commit message**: Prevents pipeline trigger — useful for docs-only changes (the square brackets are required; bare `ci skip` does not match)
3. **Service ID not obvious**: Find it in service URL or three-dot menu → Copy Service ID
4. **External/CI deploys leave ZCP unaware**: a webhook or CI `zcli push` does not record the deploy in ZCP local state — the service stays at `deployState=never-deployed`. Bridge it with `zerops_workflow action="record-deploy" targetService="<hostname>"` when you return to the ZCP develop flow

## GitLab CI

Expand Down
8 changes: 4 additions & 4 deletions apps/docs/content/guides/cloudflare.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "Cloudflare Integration with Zerops"
description: "Always use **Full (strict)** SSL mode in Cloudflare — \"Flexible\" causes redirect loops. Shared IPv4 with Cloudflare proxy is not recommended."
---

Always use **Full (strict)** SSL mode in Cloudflare "Flexible" causes redirect loops. Shared IPv4 with Cloudflare proxy is not recommended.
Use **Full (strict)** SSL mode in Cloudflare for production (plain "Full" is acceptable for testing) — **never "Flexible"**, which causes redirect loops. Shared IPv4 with Cloudflare proxy is not recommended.

## DNS Configuration

Expand Down Expand Up @@ -34,7 +34,7 @@ ACME: CNAME _acme-challenge.<domain> <domain>.zerops.zone
```

## SSL/TLS Settings (Cloudflare Dashboard)
- **Encryption mode: Full (strict)** — mandatory
- **Encryption mode: Full (strict)** for production (Full is acceptable for testing) — **never "Flexible"**
- **Never use "Flexible"** — causes infinite redirect loops
- Enable "Always Use HTTPS"
- WAF exception: Skip rule for `/.well-known/acme-challenge/` (ACME validation)
Expand All @@ -55,7 +55,7 @@ Any runtime service (nodejs, go, python, etc.) can be put behind Cloudflare. Ste
3. **Configure Cloudflare DNS** to point to your Zerops project IP
4. **Set SSL mode to "Full (strict)"** in Cloudflare dashboard

**Important**: The `zerops_subdomain enable` tool only works on deployed (ACTIVE) services. For new services, use `enableSubdomainAccess: true` in import YAML.
**Important**: On first deploy, `zerops_deploy` **auto-enables** the L7 subdomain for eligible modes (dev/stage/simple/standard/local-stage) and waits HTTP-ready — you usually never call `zerops_subdomain enable`. Use it only for explicit recovery, or set `enableSubdomainAccess: true` in import YAML to pre-declare intent. The enable call rejects with `serviceStackIsNotHttp` only when the service has no HTTP-shaped port (a worker, or a port missing `httpSupport: true`) — that is about port shape, not deploy state.

Internal service-to-service communication must always use `http://` — never `https://`. SSL terminates at the Zerops L7 balancer.

Expand All @@ -64,4 +64,4 @@ Internal service-to-service communication must always use `http://` — never `h
2. **Shared IPv4 + proxy is broken**: Reverse AAAA lookup doesn't work with Cloudflare proxy on shared IPv4
3. **ACME challenge needs WAF exception**: Without it, Cloudflare blocks Let's Encrypt validation
4. **Wildcard SSL on Cloudflare Free**: Free plan doesn't proxy wildcard subdomains — use DNS-only or upgrade
5. **Subdomain on undeployed service**: `zerops_subdomain enable` returns "Service stack is not http or https" on READY_TO_DEPLOY services — deploy code first or use `enableSubdomainAccess` in import YAML
5. **"Service stack is not http or https"**: `zerops_subdomain enable` returns this when the service has no HTTP-shaped port (a worker, or a port without `httpSupport: true`) — it's about port shape, not deploy state. A READY_TO_DEPLOY service WITH `httpSupport: true` can be enabled; a deployed worker still cannot.
10 changes: 5 additions & 5 deletions apps/docs/content/guides/deployment-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ Use `temporaryShutdown: true` only when you cannot run two versions simultaneous

1. Application starts via `start` command
2. Readiness check runs (httpGet or exec)
3. If **fails** -- wait `retryPeriod` seconds (default 5s), retry
3. If **fails** -- wait `retryPeriod` seconds (set it explicitly; platform default applies if unset, not schema-pinned), retry
4. If **succeeds** -- container marked active, receives traffic
5. If still failing after `failureTimeout` (default 300s / 5 min) -- container deleted, new one created
5. If still failing after `failureTimeout` (configurable — commonly ~5 min; set it explicitly, it is not a fixed schema default) -- container deleted, new one created

**httpGet**: succeeds on HTTP `2xx`, follows `3xx` redirects, 5-second per-request timeout
**exec.command**: succeeds on exit code 0, 5-second per-command timeout
Expand All @@ -123,7 +123,7 @@ Typical pipeline events in chronological order:

1. **`stack.build` process RUNNING** -- build container created, pipeline started
2. **`stack.build` process FINISHED** -- build complete, artifact uploaded
3. **`appVersion` build event ACTIVE** -- deploy started, containers launching
3. **`appVersion` build event ACTIVE** -- the new version is deployed and running (this is the terminal success state, NOT "launching")
4. **Service status returns to RUNNING** -- all containers active, deploy complete

**Terminal states:**
Expand Down Expand Up @@ -154,7 +154,7 @@ Zerops keeps **10 most recent versions**. Older auto-deleted. Any archived versi

1. **Build and run are SEPARATE containers** -- build output does not automatically appear in runtime. You must specify `deployFiles`
2. **initCommands run on EVERY container start** -- including restarts and horizontal scaling, not just deploys
3. **initCommands failures do NOT cancel deploy** -- app starts regardless of init exit code
3. **initCommands failures DO fail the deploy** -- a non-zero `initCommands` exit aborts the deploy (`RUN.INIT COMMANDS FINISHED WITH ERROR`); the new appVersion is not activated and the app does not start. Keep init commands idempotent and exit 0
4. **prepareCommands in build vs run** -- `build.prepareCommands` customizes build env, `run.prepareCommands` creates custom runtime image. Different containers, different purposes
5. **deployFiles land in `/var/www`** -- tilde syntax (`dist/~`) extracts contents directly to `/var/www/` (strips directory). Without tilde, `dist` → `/var/www/dist/` (preserved). **CRITICAL**: `run.start` path must match — `dist/~` + `start: bun dist/index.js` BREAKS because the file is at `/var/www/index.js`, not `/var/www/dist/index.js`

Expand All @@ -163,7 +163,7 @@ Zerops keeps **10 most recent versions**. Older auto-deleted. Any archived versi
When using SSHFS (`zerops_mount`) for dev workflows, deploy replaces the container. This has important consequences:

1. **After deploy, run container only has `deployFiles` content.** All other files (including zerops.yml if not in deployFiles) are gone. Use `deployFiles: [.]` for dev services to ensure zerops.yml and source files survive the deploy cycle.
2. **SSHFS mount auto-reconnects after deploy.** No explicit remount is neededthe SSHFS reconnect mechanism handles the container replacement transparently. The mount only becomes truly stale during stop (container not running); after start it auto-reconnects again.
2. **SSHFS mount auto-reconnects only while the service is running.** Usually no remount is needed, but if the mount goes stale after a deploy (stat/ls returns empty, writes hang), recover it explicitly with `zerops_mount action="mount"`. A stopped service has no live mount until it starts again.
3. **zerops.yml must be in deployFiles** for dev self-deploy lifecycle. Without it, subsequent deploys from the container fail because zerops.yml is missing.

**Two kinds of "mount" (disambiguation):**
Expand Down
Loading
Loading