# ZevCloud Documentation — Full Text > Every page of the ZevCloud documentation (Guide, CLI Reference, API Reference) concatenated into one file, as context for AI agents and language models. Page boundaries are marked by `===` separators carrying the page title and canonical URL; cite from there, not from this concatenated file. For a curated map without the full body text, see https://docs.zevcloud.net/llms.txt. For a single page, append `.md` to its URL. --- === # Introduction Source: https://docs.zevcloud.net/guide/ --- ZevCloud is a cloud deployment platform that makes it easy to deploy web applications, WordPress sites, databases, and static sites. ## What You Can Deploy | Service Type | Description | Build Required | |-------------|-------------|----------------| | **Web Application** | Node.js, Python, Go, Ruby, PHP, etc. from a Git repository | Yes (auto-detected) | | **WordPress** | Fully managed WordPress with MariaDB, SFTP, and auto-login | No | | **Database** | PostgreSQL, MySQL, MariaDB, MongoDB, Redis | No | | **Static Site** | HTML/CSS/JS uploaded via ZIP or SFTP | No | ## Key Concepts ### Projects A project is a container for related services. For example, a "My SaaS" project might contain a web app, a database, and a worker service. ### Environments Each project has environments (Production, Staging, Development). Services are deployed within environments, so you can test changes before going live. ### Services A service is a single deployable unit — a web app, database, WordPress site, or static site. Each service has its own: - Deployment URL (`your-app.zevcloud.app`) - Environment variables - Logs and terminal access - Billing plan ### Teams Teams own projects. You can invite members to collaborate, and each team has its own billing. ## How It Works 1. **Sign up** via [ZevCloud Dashboard](https://zevcloud.net) using your ZevID account 2. **Create a project** — give it a name 3. **Add a service** — choose the type (app, WordPress, database, or upload) 4. **Configure** — connect your Git repo, set environment variables, choose a plan 5. **Deploy** — click Deploy or push to your branch for auto-deploy Your app is live at `your-app.zevcloud.app` with automatic SSL. ## Next Steps - [Quick Start Guide](/guide/quickstart) — deploy your first app in 5 minutes - [CLI Installation](/cli/) — deploy from your terminal - [API Reference](/api/) — programmatic access === # Quick Start Source: https://docs.zevcloud.net/guide/quickstart --- Deploy your first application in under 5 minutes. ## Prerequisites - A [ZevID account](https://accounts.zevop.com) (free) - A GitHub account (for app deployments) - A phone number on your ZevID account (only required for free-tier deployments — paid plans skip this) ## Step 1: Sign In Go to [zevcloud.net](https://zevcloud.net) and sign in with your ZevID account. If you don't have one, create one — it takes 30 seconds. ## Step 2: Create a Project Click **"New Project"** from the dashboard. Give it a name like "My First App". A project is created with a **Production** environment by default. You can add Staging and Development environments later. ## Step 3: Add a Service Click **"Add Service"** inside your project. Choose what you want to deploy: ### Option A: Web Application (from GitHub) 1. Select **"Web Application"** 2. Choose **"Import from GitHub"** (connect GitHub if prompted) 3. Select your repository 4. Choose the branch to deploy 5. Set a deployment slug (e.g., `my-app`) 6. Select a plan (Free tier available) 7. Click **Create** ZevCloud auto-detects your framework (Next.js, Express, Flask, etc.) and builds it using Nixpacks. ### Option B: WordPress 1. Select **"WordPress"** 2. Enter a site name and admin email 3. Set a deployment slug 4. Select a plan 5. Click **Create** WordPress is fully installed with MariaDB. Admin credentials are emailed to you. ### Option C: Static Site 1. Select **"Upload / Drag & Drop"** 2. Enter a site name 3. Optionally upload a ZIP file (or upload later via SFTP) 4. Click **Create** A placeholder page is deployed immediately. Upload your files via the Files tab or SFTP. > **Note: First free deploy** > > The first time you deploy on the free plan, the dashboard asks for a quick phone verification. We send a code to the number on your ZevID account and you type it back. After that you won't be prompted again for 24 hours, and KYC-verified accounts skip the step entirely. Paid plans bypass it. > > If your ZevID account doesn't have a phone yet, the modal links you to the [ZevID account portal](https://accounts.zevop.com/home/security/phones) to add one — come back to the dashboard and the deploy continues. ## Step 4: Visit Your App Your app is live at: ``` https://your-slug.zevcloud.app ``` SSL is automatic via Let's Encrypt (may take 1-2 minutes on first deploy). ## Step 5: Set Up Auto-Deploy (Optional) For Git-based apps, auto-deploy is enabled by default. Every push to your configured branch triggers a new deployment. You can configure this in **Service Settings → Build & Deploy**. ## Next Steps - [Add a custom domain](/guide/domains) - [Set environment variables](/guide/env-vars) - [Install the CLI](/cli/) - [View API reference](/api/) === # Projects & Environments Source: https://docs.zevcloud.net/guide/projects --- ## Projects A project groups related services together. For example, a "My SaaS" project might contain: - A Next.js web app - A Node.js API backend - A PostgreSQL database - A Redis cache ### Creating a Project Click **"New Project"** from the dashboard or projects page. Give it a meaningful name. ### Project Settings - **Name** — editable anytime - **Description** — optional, shown in project lists - **Delete** — removes the project and all its environments/services ## Environments Each project has one or more environments: | Environment | Purpose | |-------------|---------| | **Production** | Live, user-facing services (created by default) | | **Staging** | Pre-production testing | | **Development** | Active development | | **Preview** | Auto-created per pull request (paid plans) | ### Creating an Environment From your project page, click the environment dropdown → **"Add Environment"**. ### Environment Isolation Each environment has its own: - Set of services (independent deployments) - Environment variables - Domains Services in different environments are completely isolated — a staging database doesn't affect production. === # How Deploys Work > What happens between a git push and your app being live, what gets cached, and why a build takes the time it does. Source: https://docs.zevcloud.net/guide/how-deploys-work --- Most deploy problems are easier to fix once you know which step they happened in. This page walks through what ZevCloud does between your push and your app answering requests. ## The pipeline ``` push ──▶ match ──▶ clone ──▶ detect ──▶ build ──▶ start ──▶ route ``` **1. Match.** A push to your connected repository tells us which services to deploy. Only services on that repository *and* that branch are considered. If several services share one repository, we also compare the changed files against each service's Root Directory, so a commit that only touches `frontend/` doesn't rebuild your API. Changes to shared files outside every service folder rebuild all of them. **2. Clone.** We fetch that exact commit. If an image for this service already exists for the same commit, we skip straight to the start step — this is why redeploying an unchanged commit is near-instant. **3. Detect.** We read your **Root Directory** and work out how to build it: - A `Dockerfile` there means you've already described your build, so we use it as-is. See [Using a Dockerfile](/guide/dockerfile). - Otherwise we look for a manifest — `package.json`, `requirements.txt`, `pyproject.toml`, `go.mod`, `Gemfile`, `composer.json` — and hand the folder to [Nixpacks](https://nixpacks.com), which works out the language, the system packages it needs, and how to install and start it. Its [provider docs](https://nixpacks.com/docs/providers) list exactly which files each language is detected by, which is the quickest way to check why your project was detected as one thing rather than another. You can see the resolved plan at the top of any build log, including the detected language and the exact install and start commands. **4. Build.** The plan runs in three phases: | Phase | What happens | Example | |---|---|---| | **Setup** | System packages your stack needs | Python, Node, a compiler when a dependency requires one | | **Install** | Your dependencies | `npm ci`, `pip install -r requirements.txt` | | **Build** | Your build command, if you have one | `npm run build` | **5. Start.** The new container runs your start command and has to answer on the configured **Port** before it receives traffic. **6. Route.** Traffic moves to the new container and the previous one stops. Your domain and certificate carry over untouched. > **Warning: Detection reads the Root Directory, not the repository** > > If the Root Directory points at a folder that doesn't exist, the clone still succeeds and detection finds nothing — the failure shows up as *"failed to detect the application type"*, which reads like a build-tool problem rather than a wrong path. Pick the folder from the dropdown instead of typing it. ## What gets cached Each phase is a layer, and layers are reused until something they depend on changes. Your source code is copied in **last**, so a code-only change reuses everything above it. | Layer | Rebuilt when | |---|---| | System packages | Your detected stack changes — a new dependency needs a compiler, you change runtime version | | Dependencies | Your manifest or lockfile changes | | Your code | Every commit (this is the cheap part) | The practical shape of this: **the first build of a project is the slow one.** A first build that takes several minutes is normal, and later builds of the same project are usually well under a minute. A build that's slow *every time* means something is invalidating a layer on each run, and that's worth looking at. ## Why a build is slow Work through these in order. ### Is it only the first build? Compare durations in the **Deployments** tab. First build slow, subsequent builds fast is the system working correctly — there's nothing to fix. ### Does a dependency compile from source? This is the most common cause of a build that's slow every time. Some packages ship only source code, so your build has to install a compiler and build them on every machine that hasn't done it before. Swapping to a prebuilt equivalent is usually a one-line change: | Slow | Fast | Language | |---|---|---| | `psycopg2` | `psycopg2-binary` | Python | | `mysqlclient` | `pymysql` | Python | | `bcrypt` | `bcryptjs` | Node | | `node-sass` | `sass` | Node | | `sharp` pinned to an old version | current `sharp` | Node | `psycopg2` is worth calling out: it pulls in a C compiler and PostgreSQL development headers before it can build, which adds several minutes on its own. `psycopg2-binary` is a drop-in replacement that installs in seconds, and both SQLAlchemy and Django accept it without any code change. When you create a service, ZevCloud flags these in your manifest before you deploy. ### Is your lockfile changing on every commit? If your lockfile is regenerated or reformatted on each commit, the dependency layer can never be reused. Commit a stable lockfile and leave it alone between dependency changes. ### Are you installing things you don't need at runtime? Browser automation tools (Puppeteer, Playwright, Cypress) download 100–200 MB of browsers on install. If they're in `devDependencies` and your build doesn't run tests, they're pure cost. ZevCloud sets variables to skip those downloads by default on Node projects. ### Is the build doing work your app doesn't need? Machine-learning frameworks (`torch`, `tensorflow`) are hundreds of megabytes. That's a real cost you may need to pay, but it's worth checking it's actually imported. > **Tip: If you need full control** > > A [Dockerfile](/guide/dockerfile) lets you decide exactly what's installed and in what order. It's the right answer when your build has requirements the automatic detection can't infer. ## Reading a failed build Open **Deployments → the failed deploy**. Match the last thing in the log to a phase above: | Last thing you see | Phase | Usually means | |---|---|---| | `failed to detect the application type` | Detect | Root Directory is wrong, or the folder has no recognised manifest | | A package manager error, `ERESOLVE`, a compiler error | Install | A dependency problem — read the first error, not the last | | Your own build command failing | Build | Reproduce locally with the same command | | Build succeeded, deploy failed on health checks | Start | Your app isn't answering on the configured Port | | Deploy succeeded, every URL returns Bad Gateway | Route | The Port setting doesn't match what your app listens on | The last two are the same root cause and are covered in detail under [Troubleshooting](/guide/troubleshooting#bad-gateway-or-the-service-shows-running-but-no-endpoint-responds). ## Things that surprise people **Environment variables apply at build, not at restart.** A variable change reaches your app on the next deploy. Restarting a running container won't pick it up. The dashboard reminds you while you have unapplied changes. **A restart is not a redeploy.** Restart reuses the existing image. Settings that affect how a container is built or routed — Port, build commands, variables — need a redeploy. **Build timeouts scale with your plan.** A build that's cut off mid-way is a timeout, not a crash. Limits are listed under [Plans](/guide/plans). **The Root Directory is also the auto-deploy filter.** Setting it to a subfolder means pushes that don't touch that folder won't deploy. That's usually what you want, and occasionally surprising when you expect a deploy and don't get one. === # Web Applications Source: https://docs.zevcloud.net/guide/web-apps --- Deploy web applications from a Git repository. ZevCloud supports any language or framework that can run in a container. ## Supported Frameworks ZevCloud uses [Nixpacks](https://nixpacks.com) to auto-detect and build your application: | Framework | Auto-Detected | Default Port | |-----------|---------------|-------------| | Next.js | ✅ | 3000 | | React / Vue (Vite) | ✅ | Served as a static site (no port) | | Express / Node.js | ✅ | 3000 | | NestJS | ✅ | 3000 | | FastAPI | ✅ | 8000 | | Django / Flask | ✅ | 8000 | | Go | ✅ | 8080 | | Ruby on Rails | ✅ | 3000 | | PHP / Laravel | ✅ | 8000 | | Rust | ✅ | 8080 | | Static HTML | ✅ | 80 | ## Deploying from GitHub ### Public Repositories 1. Add Service → Web Application → **Public Repository** 2. Paste the repository URL (e.g., `https://github.com/user/repo`) 3. Select the branch 4. Configure and deploy ### Private Repositories 1. Add Service → Web Application → **Import from GitHub** 2. If not connected, click **"Connect GitHub"** to install the ZevCloud GitHub App 3. Select your repository from the list 4. Select the branch 5. Configure and deploy > **Tip: GitHub Organizations** > > You can connect multiple GitHub accounts and organizations. Each installation gives ZevCloud access to the repos you choose. ## Build Configuration ZevCloud auto-detects most settings, but you can customize: | Setting | Description | Example | |---------|-------------|---------| | **Build Command** | Command to build your app | `npm run build` | | **Install Command** | Command to install dependencies | `npm ci` | | **Start Command** | Command to start your app | `npm start` | | **Output Directory** | For static builds | `dist`, `.next`, `build` | | **Root Directory** | If your app is in a subdirectory | `packages/web` | | **Port** | Port your app listens on | `3000` | These can be set during creation or updated in **Service Settings**. Root Directory is a dropdown of the folders in your repository, so you pick the path rather than typing it — a mistyped one fails the build several minutes in, with an error about the build tool rather than the path. If your repository has its own `Dockerfile`, ZevCloud uses it instead of building your app automatically. See [Using a Dockerfile](/guide/dockerfile). ## Static Sites & SPAs Client-rendered apps (Vite, React, Vue, plain HTML) don't need a server process. Their build produces a folder of static files — the right way to serve them is as a **static site**. When you create a service from a Vite project, ZevCloud detects this and pre-selects static serving. You can also change it any time in **Service Settings → Build & Deploy**: | Setting | What it does | |---------|--------------| | **Static site** | Builds your app, then serves the output folder as files. No server process, no port to configure. | | **SPA (Single Page Application)** | Serves `index.html` for unmatched routes, so client-side routing (React Router, Vue Router) works on page refresh and direct links. | | **Publish Directory** | The folder your build produces. Vite uses `dist`; Create React App uses `build`. | > **Tip: Which mode do I need?** > > If your app runs entirely in the browser and talks to APIs, use **Static site + SPA**. If your app has its own server (Next.js SSR, Express, an API), leave static off and set the Port instead. Common symptoms and their fix: - **Deploy succeeds but the site is blank or 404s** — the Publish Directory doesn't match your build output. Check what folder your build command creates. - **Pages 404 on refresh or direct links** — enable the **SPA** toggle so unmatched routes fall back to `index.html`. - **Deploy fails with a port or health check error on a Vite/React app** — enable **Static site**. A client-rendered app has no server listening on a port, so port-based deploys can't succeed. Changes take effect on the next deploy — the dashboard offers a redeploy when you save. ## Monorepos Create one service per app. How you point each service at its app depends on whether the repository is a **workspace**. ### Independent folders If each app has its own `package.json` (or `requirements.txt`, `go.mod`) and they don't depend on each other, set each service's **Root Directory** to its folder: ``` repo/ frontend/ → service 1, Root Directory: frontend backend/ → service 2, Root Directory: backend ``` Nothing else to configure. This is the common case. ### Workspaces (pnpm, Yarn, npm, Turborepo, Nx) If your repository has a `turbo.json`, a `pnpm-workspace.yaml`, an `nx.json`, or a `workspaces` field in the root `package.json`, the apps share dependencies through the repository root. **Pointing the Root Directory at the app folder will fail the install**, because dependencies declared as `workspace:*` can only be resolved from the root. Leave the Root Directory at `/` and filter the build to one app instead: ```bash # pnpm + Turborepo pnpm install --frozen-lockfile pnpm turbo run build --filter=web... pnpm --filter web start # npm workspaces npm ci npm run build --workspace=web npm start --workspace=web # Yarn workspaces yarn install yarn workspace web build yarn workspace web start ``` The trailing `...` in a Turborepo filter builds that app's own workspace dependencies first. Without it, shared packages the app imports won't be built. When you select a repository, ZevCloud detects the workspace and offers these commands filled in with your package name — one click applies them. > **Warning: Workspaces redeploy on every push** > > Because the Root Directory is `/`, every push to the branch redeploys the service. That's unavoidable: a change anywhere in a workspace can affect any app in it. ### Auto-deploy is folder-aware For services with a Root Directory set, pushes only redeploy the services whose folder contains changed files. A commit that only touches `frontend/` won't rebuild your backend. Changes to shared files outside every service folder — a root `package.json`, a shared `packages/` directory — redeploy all services from that repository, since they can affect any build. ## Environment Variables Set environment variables in the **Variables** tab of your service: ``` DATABASE_URL=postgres://user:pass@host:5432/db NODE_ENV=production API_KEY=sk_live_... ``` Variables marked as **secret** are encrypted at rest and hidden in the UI. ## Auto-Deploy on Push When you push to the configured branch, ZevCloud automatically: 1. Receives a GitHub webhook 2. Clones the latest code 3. Builds using Nixpacks 4. Deploys with zero-downtime rollout 5. Sends an email notification (success or failure) Disable auto-deploy in **Service Settings → Build & Deploy**. ## Build Logs View real-time build logs in the **Deployments** tab. Each deployment shows: - Git commit message and SHA - Build duration - Success or failure status - Detailed build output [How Deploys Work](/guide/how-deploys-work) explains each build phase, what's cached between deploys, and how to read a failed build back to the step it failed in. ## Resource Limits Each plan provides different resource limits: | Plan | Memory | CPU | Build Timeout | |------|--------|-----|---------------| | Free | 512 MB | 1 core | 10 min | | Starter | 2 GB | 1 core | 20 min | | Pro | 4 GB | 2 cores | 30 min | | Business | 8 GB | 4 cores | 60 min | ## Persistent volumes (beta) A deploy builds a fresh container from your code, so anything your app wrote to its own filesystem is gone after the next deploy or restart. That is the right default for code, and the wrong one for data: SQLite files, user uploads, generated reports, a local cache you want to keep. A **persistent volume** is a folder that survives deploys and restarts. You pick the path inside the container, and whatever your app writes there stays. **To attach one:** open the service, go to the **Volumes** tab, enter the folder your app writes to (for example `/app/data` or `/app/uploads`) and click **Attach volume**. The service restarts once so the folder is mounted. From then on every deploy and restart finds the same files there. A few things to know: - **Paid plans only.** Each plan includes a total allowance in GB across all volumes on one service. The Volumes tab shows how much is used; we email you when a service gets close to its allowance, and the dashboard turns amber at 80%. Nothing is paused when you reach it, but writes fail once the disk is actually full, so give yourself room. - **Up to three volumes per service.** One for data and one for uploads covers most apps. Mount paths cannot overlap. - **The mount hides what was there.** An empty volume mounted at `/app/data` covers any `/app/data` that came from the build. Mount at a folder your app creates at runtime, not one that ships in your repository. You cannot mount over `/app` itself or the container's system folders. - **Removing a volume deletes its files.** There is no undo, and the service restarts afterwards. Download anything you need first (the Terminal tab can `tar` a folder, or your app can export it). - **One service, one volume.** Volumes are not shared between services. If two services need the same files, use a [storage bucket](/guide/storage) instead. ## Outbound network restrictions To keep the platform clean for everyone, customer containers are not allowed to make outbound connections on a few specific ports. The host blocks them at the firewall, so any code in your app that tries to reach these ports will time out. - **SMTP (25, 465, 587, 2525)** — direct mail submission. Use a transactional email API (Resend, Mailgun, Postmark, SendGrid) over HTTPS instead. Their REST/HTTP endpoints are not blocked. - **IRC (6667, 6697)** — typical command-and-control channels. - **Mining-pool stratum (3333, 4444, 5555, 7777, 14444)** and **Bitcoin RPC (8332, 8333)**. Everything else (HTTPS to third-party APIs, your database, Redis, CDNs, webhooks) works normally. If you need to send email, switch to your provider's HTTPS API. ## Rollback If a deployment fails or causes issues, you can redeploy a previous version from the **Deployments** tab. === # Using a Dockerfile > Deploy a project that already has its own Dockerfile, and what ZevCloud does differently when it finds one. Source: https://docs.zevcloud.net/guide/dockerfile --- If your repository already has a `Dockerfile`, ZevCloud uses it as-is. You've described your build; we don't second-guess it. ## What happens automatically When you create a service from a repository, we look for a `Dockerfile` in your **Root Directory**. If one is there: - The **Build Pack** is set to Dockerfile for you. - The **Port** is filled in from the file's `EXPOSE` line, when it has one. - Your install and build commands are ignored — your Dockerfile is the whole build. `dockerfile` (lowercase) and `Containerfile` are recognised too. We only look in the Root Directory, not in subfolders. > **Note: Automatic detection versus your Dockerfile** > > Without a Dockerfile, ZevCloud detects your stack and builds it for you. With one, none of that applies — the packages installed, the base image, and the runtime version all come from your file. Changing the Runtime Version setting has no effect on a Dockerfile build, which is why the picker is hidden for them. ## Requirements **Bind to `$PORT`.** ZevCloud sets a `PORT` environment variable on every service. Reading it is the most reliable option, because then the port can never drift out of sync with your service settings: ```dockerfile ENV PORT=8000 EXPOSE 8000 CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}"] ``` If you'd rather hard-code a port, make sure `EXPOSE`, your `CMD`, and the **Port** in Service Settings all agree. A mismatch produces a Bad Gateway on a container that's running perfectly. **Listen on `0.0.0.0`, not `127.0.0.1`.** An app bound to localhost inside a container is unreachable from outside it. This is the second most common cause of a healthy container that serves nothing. **Build for `linux/amd64`.** If you build and test locally on an Apple Silicon Mac, use `docker build --platform linux/amd64` to catch architecture problems before deploying rather than after. ## Environment variables Variables you set in the **Variables** tab are available as build arguments and at runtime. To use one during the build, declare it: ```dockerfile ARG DATABASE_URL ENV DATABASE_URL=$DATABASE_URL ``` > **Warning: Don't bake secrets into the image** > > Anything written into an image layer stays in that layer, including values you later overwrite. Keep secrets to runtime: set them in the Variables tab and read them with `os.environ` / `process.env` when the app starts, rather than at build time. ## Writing one that builds fast Docker reuses layers until one changes, and every layer after that point is rebuilt. So the order of your instructions decides how much work each deploy does. Copy your dependency manifest first, install, and only then copy your source. That way a code change reuses the install layer: ```dockerfile # Dependencies: only re-runs when the lockfile changes COPY package*.json ./ RUN npm ci # Source: changes on every commit, so it goes last COPY . . RUN npm run build ``` The mistake to avoid is `COPY . .` before the install step, which makes every commit reinstall every dependency. Add a `.dockerignore` so `node_modules`, `.git`, and build output aren't shipped into the build: ``` node_modules .git dist .env ``` A multi-stage build keeps compilers and dev dependencies out of the final image, which makes deploys faster and the running container smaller: ```dockerfile FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist CMD ["node", "dist/main.js"] ``` ## Compose files ZevCloud deploys one container per service, so `docker-compose.yml` isn't used as a build input. If your compose file describes several containers, create one ZevCloud service per container and point each at the folder holding its Dockerfile. For the things compose usually provides alongside your app: - **Databases** — create a [managed database](/guide/databases) and connect with the connection string it gives you. - **Object storage** — use [storage buckets](/guide/storage). - **Shared networking** — services in the same project reach each other over the internal network. ## Switching between build modes **Service Settings → Build & Deploy → Build Pack** lets you switch at any time. Changing to Dockerfile requires a `Dockerfile` in your Root Directory; if there isn't one, the change is rejected rather than silently falling back, so you don't end up with a build that quietly ignores the setting. Either way the change applies on the next deploy. === # WordPress Hosting Source: https://docs.zevcloud.net/guide/wordpress --- Deploy fully managed WordPress instances with MariaDB, SFTP access, and automatic SSL. ## Creating a WordPress Site 1. Go to your project → **Add Service** → **WordPress** 2. Enter: - **Site Name** — display name - **Site Title** — WordPress site title - **Deployment URL** — your subdomain (e.g., `myblog.zevcloud.app`) - **Admin Email** — WordPress admin login email 3. Select a plan 4. Click **Create** ZevCloud will: - Provision a WordPress container with PHP 8.x - Provision a MariaDB 11 database - Set up an SFTP server for file access - Auto-install WordPress with your admin credentials - Configure SSL via Let's Encrypt - Email you the login details ## What's Included | Component | Details | |-----------|---------| | WordPress | Latest version, PHP 8.x | | Database | MariaDB 11, auto-configured | | SFTP | Dedicated SFTP access for file management | | SSL | Automatic via Let's Encrypt | | Storage | Persistent volumes for files and database | ## Managing Your WordPress ### Dashboard Overview The WordPress service detail page shows: - **Site URL** and **WP Admin** links - **Admin credentials** (username, password — revealable) - **SFTP credentials** (host, port, username, password) - **Database credentials** (host, database, username, password) - **Container status** (WordPress, MariaDB, SFTP) ### Auto-Login to wp-admin Click **"WP Admin"** in the dashboard header to automatically log into your WordPress admin panel — no password needed. A one-time token is generated and you're redirected. ### File Management **Via Dashboard:** Navigate to the **Files** tab to browse your WordPress files. You can view the directory structure and navigate through `wp-content`, themes, plugins, etc. **Via SFTP:** Connect using any SFTP client (FileZilla, Cyberduck, WinSCP): ``` Host: your-slug.zevcloud.app (or deployment domain) Port: 2222 (unique per site) Username: sftp_yourslug Password: (shown in dashboard) Protocol: SFTP ``` Files are served from `/var/www/html`. ### Database Access Database credentials are shown in the **Database** tab. The database is accessible: - **Internally** — from other services on ZevCloud using the container hostname - **Via Terminal** — use the Terminal tab to run `mysql -u user -p dbname` ### Logs View WordPress access and error logs in the **Logs** tab. Logs are pulled directly from the container. ### Terminal Run commands inside your WordPress container from the **Terminal** tab. Useful for: - WP-CLI commands: `wp plugin list`, `wp theme activate` - Debugging: `cat wp-config.php` - File operations: `ls wp-content/plugins` ## Custom Domains 1. Go to the **Domains** tab 2. Click **"Add Domain"** 3. Enter your domain (e.g., `myblog.com`) 4. Add the DNS records shown (CNAME or A record) 5. Click **Verify** Once verified, your WordPress `siteurl` and `home` are automatically updated to the custom domain. > **Warning:** > > When you add a custom domain to WordPress, the WordPress database is updated with the new URL. The original deployment URL still works as a fallback. ## Backups Database backups can be performed via the Terminal: ```bash # Export database wp db export /tmp/backup.sql --allow-root # Or using mysqldump mysqldump -u user -p wordpress > /tmp/backup.sql ``` Download via SFTP or the Files tab. ## Updating WordPress WordPress core updates can be done via: - **wp-admin** → Dashboard → Updates - **Terminal**: `wp core update --allow-root` - **SFTP**: Upload updated files Plugin and theme updates work the same way through wp-admin. === # Static Sites Source: https://docs.zevcloud.net/guide/static-sites --- Deploy HTML, CSS, and JavaScript sites without Git or build steps. Upload via the dashboard or SFTP. ## Creating a Static Site 1. Go to your project → **Add Service** → **Upload / Drag & Drop** 2. Enter a site name and deployment slug 3. Optionally upload a ZIP file 4. Select a plan 5. Click **Create** If you don't upload a file, a branded placeholder page is deployed automatically. You can upload your files later. ## Uploading Files ### Via Dashboard (Files Tab) Navigate to the **Files** tab to browse and manage your site files. The file browser shows the Nginx document root at `/usr/share/nginx/html`. ### Via SFTP Connect with any SFTP client: | Setting | Value | |---------|-------| | Host | `your-slug.zevcloud.app` | | Port | `2222` (check your service details) | | Username | `sftp_yourslug` | | Password | Shown in service overview | | Protocol | SFTP | Upload your files to the root directory. They'll be served immediately by Nginx. ### Via ZIP Upload Upload a `.zip` file through the dashboard. It will be extracted and deployed automatically. ## Nginx Configuration Static sites run on Nginx Alpine with: - Gzip compression enabled - SPA fallback (`try_files $uri /index.html` for client-side routing) - Custom error pages - Automatic SSL via Let's Encrypt ## Custom Domains Add custom domains the same way as any other service — via the **Domains** tab. ## SFTP Password Reset If you need to regenerate your SFTP password: 1. Go to the service **Overview** tab 2. Click **"Regenerate password"** under SFTP credentials 3. The new password is emailed to you === # Databases > Provision managed databases on ZevCloud — PostgreSQL, MySQL, MariaDB, MongoDB, and Redis. Connect from your apps, browse the data with the built-in Database Manager, or open from your laptop. Source: https://docs.zevcloud.net/guide/databases --- Provision managed databases with one click. Use them from your apps running on ZevCloud, browse the data through the built-in Database Manager, or connect from your local machine. ## Supported engines | Database | Versions | Default port | |----------|----------|--------------| | PostgreSQL | 18, 17, 16, 15 | 5432 | | MySQL | 8 | 3306 | | MariaDB | 11 | 3306 | | MongoDB | 7 | 27017 | | Redis | 7 | 6379 | ## Creating a database 1. Open your project in the [dashboard](https://console.zevcloud.net) and click **Add Service → Database**. 2. Pick the engine and version. 3. For PostgreSQL, choose an **extension** variant if you need one (see below). You can change this later. 4. Give it a name and pick a plan. 5. Click **Create**. The database is provisioned immediately with auto-generated credentials. They appear on the service's **Overview** tab — username, password, database name, internal hostname, and a ready-to-use connection URL. ## PostgreSQL extensions Some projects need capabilities that aren't in a stock PostgreSQL build. You can pick a variant when you create the database, or switch an existing one from **Overview → Extensions**. | Variant | What it adds | Available on | |---------|--------------|--------------| | **Standard** | The built-in extension set | 18, 17, 16, 15 | | **pgvector** | Vector similarity search — embeddings, semantic search, RAG | 18, 17, 16 | | **PostGIS** | Geospatial types and queries — coordinates, distances, maps | 17, 16 | Choosing a variant makes the extension *available*; you still enable it inside your database: ```sql -- pgvector CREATE EXTENSION vector; -- PostGIS CREATE EXTENSION postgis; ``` Run that once per database, from the [database manager](#terminal-access), `psql`, or a migration in your app. ### Switching on an existing database Open the database, go to **Overview → Extensions**, and pick the variant you want. **Your data is preserved.** The variants are the same PostgreSQL version built with extra extension libraries, so nothing is copied or migrated — the database restarts and the new extensions become available. > **Warning: A restart is required** > > Switching restarts the database, which briefly drops open connections. Do it at a quiet moment, and make sure your app reconnects cleanly. Two things to know: - **You can't change the PostgreSQL version this way.** Moving between major versions (16 → 18) needs a dump and restore onto a new database, because the on-disk format differs between majors. See [Importing an existing database](#importing-an-existing-database). - **Not every variant exists for every version.** The table above shows what's available; unavailable combinations are greyed out in the dashboard. ## How to use it There are three ways to reach a database you've created. Pick the one that fits what you're doing: | If you want to… | Use | |-----------------|-----| | Connect from another ZevCloud service (a web app, worker, anything in the same project) | The **internal hostname** shown on the Overview tab. No public access needed. | | Browse tables, run SQL, import or export — from your browser | **Database Manager** (MySQL, MariaDB, PostgreSQL only — see below) | | Connect from your laptop or an external service (pgAdmin, TablePlus, MongoDB Compass, RedisInsight, your CI, etc.) | **Public access** (see below) | ### Connect from another ZevCloud service Use the **internal hostname** as the host. Services on the same project share an internal network and can talk to the database directly — no public exposure needed. ```bash # Example: web app reading from a Postgres database in the same project DATABASE_URL=postgres://user:pass@:5432/dbname ``` Add the connection string as an environment variable on your application service. Internal traffic doesn't count against any egress quota. ### Database Manager (web GUI) For **MySQL, MariaDB, and PostgreSQL** databases, the **Database Manager** is a web GUI built into the dashboard. Click **Open database manager** on the database's Overview tab and a new browser tab opens with the GUI logged in to your database. Browse tables, edit rows, run SQL, import dumps, export backups — all without installing anything. The Database Manager is **available on every plan** (free and paid) for standalone databases. Full walkthrough on the [Database management page](/guide/database-management). > **Note: MongoDB and Redis: use a desktop client** > > Adminer (the engine behind the Database Manager) is SQL-only — it can't browse MongoDB collections or Redis keys. For those engines, use a free desktop client: > > - **MongoDB** → [MongoDB Compass](https://www.mongodb.com/products/tools/compass) (official, free, cross-platform) > - **Redis** → [RedisInsight](https://redis.io/insight/) (official, free, cross-platform) > > Both connect using the public-access flow described below. ### Connect from your laptop or an external service Databases default to internal-only. To reach one from outside the ZevCloud network — your laptop, a Vercel deployment, your CI runner — turn on Public Access. 1. Open the database's **Overview** tab. 2. Find the **Public Access** section. 3. Toggle it on. Public access requires a **paid plan**. The dashboard shows a public host (something like `apps-sandbox.zevop.net`) and a unique port. Use those, plus your existing username / password / database name, in any standard client: ```bash # PostgreSQL psql "postgresql://user:password@apps-sandbox.zevop.net:54321/dbname" # MySQL / MariaDB mysql --host=apps-sandbox.zevop.net --port=54321 --user=user --password=password dbname # MongoDB (Compass connection string) mongodb://user:password@apps-sandbox.zevop.net:54321/dbname # Redis (RedisInsight or redis-cli) redis-cli -h apps-sandbox.zevop.net -p 54321 -a password ``` > **Warning: Public access exposes your database to the internet** > > Anyone who knows the host, port, and credentials can connect. Use strong passwords (the auto-generated ones are 32 characters — don't replace them with shorter ones), and turn public access off when you're done if you only need it occasionally. Switching the toggle off immediately closes the public port. ## Terminal access Every database service has a **Terminal** tab that opens a shell inside the database container. Use it for CLI tooling or quick one-off commands without enabling public access: ```bash # PostgreSQL psql -U -d # MySQL / MariaDB mysql -u -p # MongoDB mongosh # Redis redis-cli ``` The terminal is admin-only and rate-limited. ## Importing an existing database ### MySQL, MariaDB, PostgreSQL — use the Database Manager Open the Database Manager, click **Import** in the left sidebar, pick your `.sql` or `.sql.gz` file, click **Execute**. Streams up to 1 GB per file straight into the database. This is the easiest path for migrations from cPanel, another host, or a local backup. ### MongoDB — use `mongorestore` from your laptop ```bash # 1. Turn on public access on the dashboard (paid plan) # 2. From your laptop: mongorestore \ --uri="mongodb://user:password@apps-sandbox.zevop.net:54321/dbname" \ /path/to/dump-dir # 3. Turn public access off when the import finishes ``` ### Redis — use `redis-cli` with `--pipe` or restore an RDB ```bash # From your laptop, against a temporarily-public Redis: cat dump.txt | redis-cli -h apps-sandbox.zevop.net -p 54321 -a password --pipe ``` For full snapshot restores (`.rdb` files), open a support ticket — we can place the file on the container's data volume directly. ## Connection limits Every database plan includes a maximum number of simultaneous connections, shown on the plan when you create the database. When a database reaches it, new connections are refused until others close. Connections that are already open keep working. The **Metrics** tab shows open connections against your plan's limit, and the **Overview** tab tells you when a database is close to its limit or at it. The team owner is emailed when usage stays high. ### Using fewer connections Most apps open far more connections than they need. Before upgrading, check your pool settings: - **Use a connection pool.** Opening a new connection per request is the most common cause of hitting the limit. Most frameworks and ORMs pool by default; make sure yours does. - **Size the pool to the plan.** If you run several instances of an app, or several apps share one database, their pools add up. Four instances with a pool of 10 each need 40 connections. - **Close connections in scripts and background jobs** when they finish, rather than leaving them open. - **Serverless and short-lived processes** should keep their pool small (often 1 to 2) because each process holds its own. If your app genuinely needs more connections at once, upgrade the database plan from its **Billing** tab. The new limit applies automatically. ### When a limit changes When a database gets a new limit, such as after a plan change: - **MySQL, MariaDB and Redis** apply it straight away. - **PostgreSQL and MongoDB** need a brief restart to apply it. It's scheduled for **03:00 WAT**, at least 12 hours ahead, and the team owner is emailed the time. To get it over with sooner, click **Restart now** on the database's Overview tab. - **If the database is using more connections than the new limit**, nothing changes for 48 hours. The team owner is emailed first, so there's time to adjust pool sizes or upgrade. A new database gets its limit as soon as it's created. > **Tip:** > > A restart usually takes under a minute and doesn't affect your data. Apps that retry failed connections reconnect on their own. ## Backups The fastest path depends on the engine: - **SQL databases**: open the Database Manager → **Export** → choose `gzip` + `SQL`. Browser downloads the dump. Quick, safe, no public access needed. - **MongoDB**: enable public access, then `mongodump --uri="…" --out=./backup` from your machine. - **Redis**: enable public access, then `redis-cli -h … --rdb dump.rdb` from your machine. For routine off-site backups (any engine), run the export on a schedule from another ZevCloud service in the same project — internal-network access means you don't need to enable public exposure for automated backups. ## Plan + access summary | Engine | Internal access (other ZevCloud services) | Database Manager (web GUI) | Public access (your laptop / external) | |--------|--------------|--------------|----------------| | PostgreSQL | Every plan | Every plan | Paid plans | | MySQL | Every plan | Every plan | Paid plans | | MariaDB | Every plan | Every plan | Paid plans | | MongoDB | Every plan | Use Compass via public access | Paid plans | | Redis | Every plan | Use RedisInsight via public access | Paid plans | If you want a hosted browser-based GUI for MongoDB or Redis specifically, let us know at **support@zevcloud.net** — we're tracking demand for it. === # Object Storage > ZevCloud Storage — S3-compatible object storage with Naira pricing. Create buckets, upload files, host static assets, and connect storage to your apps. Source: https://docs.zevcloud.net/guide/storage --- ZevCloud Storage is S3-compatible object storage. You create buckets, upload objects, and consume them from any tool that speaks S3 — `boto3`, the AWS SDK, `aws-cli`, `rclone`, whatever you already use. Pricing is in Naira, billed by plan, and bandwidth out of your buckets is included in the plan rather than billed separately. ## When you'd use it Storage covers three common shapes of work: | If you want to… | Use | |-----------------|-----| | Host file uploads from your app — profile pictures, document uploads, attachments | A private bucket [connected to your service](/guide/storage-buckets#connecting-a-bucket-to-a-service) | | Serve static assets — images, video, downloads — at a public URL | A public-read bucket (paid plan) | | Back up databases, archive logs, store build artifacts from CI | Any bucket, accessed from your CI or scripts with the bucket's access key | You can mix all three on the same team. A typical setup might be a private bucket for an app's uploads, a public bucket for a marketing site's images, and a third bucket for nightly database backups. ## Concepts in two minutes - A **bucket** is a flat container for objects. It has a name, a region, an access key pair, and a status (`active`, `provisioning`, `error`). - An **object** is a file. Every object has a key (its path within the bucket) and content. - **Access keys** are scoped to a single bucket. A leaked key on one bucket cannot read or write any other bucket. Each bucket has exactly one active key pair at a time. - A **plan** caps how much storage your team can hold across all buckets. The first plan is free. Paid plans unlock public buckets, custom domains, and larger caps. - A **connected service** is one of your app services that has been wired to a bucket via the dashboard's **Variables → Connect storage bucket** action. The connection injects S3 environment variables into the service so your app code reads them directly. ## What you get out of the box **S3-compatible API.** Anything that speaks S3 works against your bucket. The four values your SDK needs — endpoint URL, region, bucket name, access key pair — all appear on the bucket detail page. **Per-bucket credentials.** Each bucket has its own access key pair. Rotating the key shows the new secret once in the rotation response. The previous key keeps working for 24 hours after rotation so any services using it have time to pick up the new value. **Connect to a service in one click.** From any of your app services, **Variables → Connect storage bucket** picks a bucket and injects five managed environment variables into the service: `ZEVSTORAGE_ENDPOINT`, `ZEVSTORAGE_REGION`, `ZEVSTORAGE_BUCKET`, `ZEVSTORAGE_ACCESS_KEY_ID`, and `ZEVSTORAGE_SECRET_ACCESS_KEY`. Your app reads them with no extra config. When you rotate the bucket's key, the connected service's env vars update automatically. The same values are also published under the older `S3_` names, so anything already deployed against those keeps working. Need a second bucket on the same service? Give it a name and its variables are prefixed with it, such as `ZEVSTORAGE_UPLOADS_BUCKET`. **Public-read buckets** *(paid plan)*. Mark a bucket public-read on create, and every object inside it is readable at a stable public URL with no signing required. Use for marketing assets, public downloads, or anything you want fetched from a browser. **Custom domains** *(paid plan)*. Attach your own hostname to a public-read bucket so files load at `assets.yoursite.com` instead of the default URL. Works with a domain you bought through ZevCloud (DNS configured automatically) or a domain hosted elsewhere (you set the CNAME). **Object Lock (WORM).** Optional flag at bucket creation for compliance workloads where data must not be overwritten or deleted within a retention window. Once enabled, it cannot be turned off. **Built-in object browser.** Every bucket has an Objects tab in the dashboard. Browse folders, upload, download, delete from the browser without installing any extra tool. ## Quick start: create a bucket and use it 1. Open the [dashboard](https://console.zevcloud.net), click **Storage** in the left nav, then **Create bucket**. 2. Give it a name (lowercase letters, digits, hyphens — same rules as S3) and pick a region. For a private bucket leave the **Public read** toggle off. For a bucket that serves files directly to browsers, turn it on. The toggle is only available on paid plans. 3. Click **Create**. Within a few seconds the bucket lands in `active` status and a one-time secret access key appears on screen. 4. **Copy the secret access key now.** It is shown once. The access key ID is visible later on the bucket detail page; the secret is not. You can now use the bucket from any S3 client. Three of the most common paths follow. ### From a service deployed on ZevCloud Use the **Connect storage bucket** flow so the credentials land as environment variables and stay in sync with key rotations. 1. Open the service in the dashboard. Click **Variables**. 2. Click **Connect storage bucket** and pick the bucket. 3. Five environment variables appear on the service: `ZEVSTORAGE_ENDPOINT`, `ZEVSTORAGE_REGION`, `ZEVSTORAGE_BUCKET`, `ZEVSTORAGE_ACCESS_KEY_ID`, `ZEVSTORAGE_SECRET_ACCESS_KEY`, each mirrored under its older `S3_` name. They are marked as managed, so you cannot edit them by hand; the values follow whatever the bucket currently has. 4. Redeploy the service so the runtime picks up the new env. Future key rotations are picked up automatically the next time the service restarts. In your app code, read them as normal environment variables. Example using the AWS SDK for JavaScript: ```js import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ endpoint: process.env.ZEVSTORAGE_ENDPOINT, region: process.env.ZEVSTORAGE_REGION, credentials: { accessKeyId: process.env.ZEVSTORAGE_ACCESS_KEY_ID, secretAccessKey: process.env.ZEVSTORAGE_SECRET_ACCESS_KEY, }, // S3-compatible providers require path-style addressing. forcePathStyle: true, }); await s3.send(new PutObjectCommand({ Bucket: process.env.ZEVSTORAGE_BUCKET, Key: 'uploads/profile.png', Body: imageBuffer, ContentType: 'image/png', })); ``` The same shape works for `boto3`: ```python import os, boto3 s3 = boto3.client( 's3', endpoint_url=os.environ['ZEVSTORAGE_ENDPOINT'], region_name=os.environ['ZEVSTORAGE_REGION'], aws_access_key_id=os.environ['ZEVSTORAGE_ACCESS_KEY_ID'], aws_secret_access_key=os.environ['ZEVSTORAGE_SECRET_ACCESS_KEY'], ) s3.upload_file('profile.png', os.environ['ZEVSTORAGE_BUCKET'], 'uploads/profile.png') ``` ### From your laptop or external service Grab the four values from the bucket detail page — endpoint, region, bucket name, access key ID — and the secret you copied at create time. Configure your tool of choice. Example with the AWS CLI: ```bash aws configure --profile zevcloud # AWS Access Key ID: # AWS Secret Access Key: # Default region: # Default output format: json aws s3 ls s3:// \ --profile zevcloud \ --endpoint-url ``` `rclone`, `s3cmd`, `mc`, and every cloud backup tool that takes "S3-compatible" credentials work the same way. ### From the dashboard's object browser Open the bucket, click **Objects**. Drag and drop to upload, click any file to download, right-click to delete. Useful for one-off uploads, quick checks, or letting non-developer teammates manage files without sharing the access key. ## Security model - The access key is **bucket-scoped**. It has no permissions on any other bucket on your team or on the platform. Leaking it limits the blast radius to the one bucket. - The **secret access key is shown once** at create time and again only at rotation time. The dashboard never re-displays it. If you lose it, rotate the key. - **Owner and admin team roles** can create buckets, fetch credentials, rotate keys, attach custom domains, and delete. **Member** role can browse object listings and download via presigned URLs but cannot fetch the raw access key. - Use **separate buckets** for separate concerns — one for user uploads, one for backups, one for public assets. The cost is the same and the blast radius is smaller. - **Rotate the key** anytime you suspect exposure. Old key keeps working for 24 hours so connected services have time to restart. ## How it's billed Storage is billed by plan, in Naira, monthly. Each plan caps how much storage your team can hold across all buckets. Bandwidth is included. The dashboard shows your current usage against the cap. At about 80% used you'll see a yellow notice; past the cap a red one. Writes continue to work past the cap — we don't refuse uploads mid-form-submission — but the bill nudge becomes prominent and you should switch to a larger plan. The first tier is free. See **[Plans & Pricing](/guide/plans)** for current Naira amounts and per-plan caps. ### If the plan invoice goes past due After switching to a paid plan, if the corresponding invoice is not paid: - **For about three weeks past due** — storage is still fully writable. The dashboard shows a clear "past due" notice with a link to the unpaid invoice. - **After about three weeks** — storage goes read-only. Existing objects continue to serve; new uploads return an error. - **After a further week** — storage is terminated. Buckets and their objects are removed. Pay the outstanding invoice at any point during the window and full access is restored immediately. Free-plan teams never enter this lifecycle since there is no invoice. ## Limits | Limit | Default | Notes | |-------|---------|-------| | Storage per team | Cap on your current plan | Switch plans to lift the cap | | Buckets per team | No hard limit | Subject to your plan's storage cap | | Object size | No platform-imposed limit | Use multipart upload for very large objects, same as standard S3 | | Public-read buckets | Paid plans only | Free plan buckets are always private | | Custom domains per bucket | No hard limit | Paid plans only | | Key rotation grace window | 24 hours | Old key keeps working for 24h after a rotation | | Presigned URL max lifetime | 24 hours | Default is 1 hour | ## Common questions **Can I move objects from S3 (or any S3-compatible provider) into a ZevCloud bucket?** Yes. Tools like `rclone copy`, `aws s3 sync`, and `mc mirror` work directly — point the source at your existing provider and the destination at your ZevCloud bucket using the endpoint/region/credentials from the detail page. **Can I use the same access key from multiple services?** Yes, the access key is bucket-scoped, not service-scoped. Connecting the same bucket to two services injects the same managed env vars on both, and a rotation updates both automatically. **What happens to a bucket's objects if I delete the bucket?** The objects are removed with the bucket. Bucket deletion is irreversible; export anything you need first. **Can I make an existing private bucket public-read later?** The flag is set at create time. To convert, create a new public-read bucket on a paid plan and copy the objects across with `rclone` or `mc mirror`. **What region should I pick?** Pick the region closest to where your users or app services are. If you're connecting the bucket to a service on ZevCloud, picking the same region the service runs in keeps the upload and download paths inside the same data centre. ## Where to go next - **[Buckets walkthrough](/guide/storage-buckets)** — creating buckets, choosing public vs private, connecting to services, attaching custom domains, rotating keys. - **[Storage API](/api/storage)** — every endpoint with request and response shapes for building against the API directly. - **[Plans & Pricing](/guide/plans)** — current per-plan storage caps and Naira prices. === # Migrate a WordPress site to ZevCloud > Move an existing WordPress site to ZevCloud, including the database, media uploads, themes, and plugins. The full step-by-step. Source: https://docs.zevcloud.net/guide/migrate-wordpress --- This guide walks through moving an existing WordPress site to [ZevCloud](https://zevcloud.net), whether the source site is still live, taken down, or you only have an export sitting on your laptop. By the end you will have your posts, pages, comments, and media files running on a fresh WordPress install at a `.zevcloud.app` subdomain or your own custom domain. > **Note: Paid plans only** > > Database import is available on **paid WordPress plans**. The free tier is built for evaluating ZevCloud with a fresh site; bringing an existing site across (with its full database, content, and URL rewriting) requires a paid plan. Upgrade from your service's **Billing** tab in the [dashboard](https://console.zevcloud.net), then come back here to import. ## What you need before you start Three things from the old site: 1. **A database dump.** Either `.sql` or `.sql.gz`, produced by `mysqldump`, phpMyAdmin's **Export** feature, or a backup tool like UpdraftPlus. The dump should include every table prefixed with `wp_` (or whatever the old install's table prefix was). 2. **Your `wp-content/uploads/` folder.** This is where every image, video, PDF, and attachment lives. Posts reference uploads by URL, so without this folder every media item in your archive breaks. Grab the whole directory, including its `YYYY/MM/` subfolders. 3. **A list of your themes and plugins.** Just names. You do not need the files. We reinstall fresh from the original source. That is it. You do not need `wp-config.php`, `wp-content/themes/`, `wp-content/plugins/`, or WordPress core files. The new install ships with all of that. > **Warning: Site already down?** > > If the old site is offline and you cannot run `mysqldump` directly, the database dump you have from a backup is fine. The flow below works the same way. If you do not have a database dump and the site is down, recovery is harder. Your previous hosting provider may still have a backup. Reach out to them before assuming the data is gone. ## What this guide does not cover - **Multisite / WordPress Network installs.** Same approach mostly works, but the `wp_blogs` table needs handling. Email support before you start. - **WooCommerce stores with live orders.** The migration itself works, but you need to coordinate the DNS cutover so customers do not land on the old store while orders are in flight. - **Restoring from a corrupted database dump.** If the dump is incomplete, the import will fail with a SQL error. You will need to repair the dump first. If any of those apply, write to **support@zevcloud.net** before you start. We can help. ## The migration 1. ### Create a fresh WordPress site On the [ZevCloud dashboard](https://console.zevcloud.net), open a project (or create one), add a new service, and pick **WordPress**. Choose a plan, a slug (`my-blog` becomes `my-blog.zevcloud.app`), and the region. Provisioning takes 30 to 60 seconds. When it finishes you will have: - The site URL: `https://.zevcloud.app` - The admin URL: `https://.zevcloud.app/wp-admin/` - An initial admin account (the credentials are shown once on the dashboard, copy them somewhere safe). 2. ### Open the credentials panel On the new WordPress service, open the **Credentials** tab. You will see two sets: - **SFTP** for uploading files: host, port, username, password. - **Database** for importing the SQL dump: host, port, database name, user, password. Keep the tab open. You need both sets in the next two steps. 3. ### Upload your media files via SFTP Connect with any SFTP client (FileZilla, Cyberduck, the `sftp` command line) using the credentials from the dashboard. On the remote side, navigate to `wp-content/uploads/`. Upload your old `uploads/` directory in full, preserving the date-based subfolder structure (`2024/01/`, `2024/02/`, and so on). ```bash # If you prefer the CLI, from inside your local uploads/ folder: sftp -P @ cd wp-content/uploads put -r * ``` On a typical site this is the slowest step. A few thousand images can take 10 to 20 minutes over a normal connection. 4. ### Import the database The reliable path for any dump size is **SFTP + Database tab import**: the file goes to your service via SFTP (resumable, no timeouts, no proxy in the way), then a single click on the dashboard kicks off a server-side import. 1. **SFTP the dump into your service.** Reuse the SFTP connection from step 3, but place the dump file at the root of your home directory (not inside `wp-content/uploads/`): ```bash sftp -P @ put old-site.sql.gz ``` Both `.sql` and `.sql.gz` work — gzipped files are decompressed transparently during import, so don't bother extracting first. 2. **Open the dashboard's Database tab** on your WordPress service. 3. **Pick the file in "Import database from SFTP"**. The right-hand card lists every `.sql` and `.sql.gz` file in your home directory. Select your dump. 4. **Tell us about the URL rewrite.** If your DNS already points to ZevCloud, tick *"My DNS is already pointing to this deployment"* — we'll skip the URL rewrite. Otherwise, paste your old site URL (e.g. `https://oldsite.com`) and we'll rewrite every reference in the dump to your new ZevCloud URL. 5. **Confirm and import.** Tick *"I understand this replaces my current database"*, click **Import**. The page stays on the dashboard while the import runs server-side — typically 30 s to 5 min depending on dump size. The persistent status row in the same card flips from "in progress" to "completed" or "failed" when done. > **Tip: Smaller dumps: the Database Manager also imports** > > For dumps under ~1 GB you can also use the **Database Manager** GUI (top of the Database tab) → **Import** → **Choose files** → upload from your browser. Useful when you want to preview the dump in Adminer before running it. For migrations of 100+ MB, the SFTP path above is more reliable. > **Tip: Other tools also work** > > If you'd rather use a desktop database client (Sequel Ace, DBeaver, MySQL Workbench, HeidiSQL), the credentials on the **Database** tab work for those too — just point them at the host and port shown there. The CLI works the same way: > > ```bash > # Plain SQL dump: > mariadb -h -P -u -p < old-site.sql > > # Gzipped: > gunzip -c old-site.sql.gz | mariadb -h -P -u -p > ``` The import will print warnings about existing tables (`wp_options`, `wp_users`, etc.) being overwritten. That is expected. The dump replaces what the fresh install set up. 5. ### Fix site URL and run search-replace After the import, the database still has your old domain in `wp_options.siteurl` and `wp_options.home`. WordPress reads those values to know where it lives, so loading `https://.zevcloud.app/wp-admin/` will redirect you to the old domain. Fix this in two parts. **Part A: update `siteurl` and `home` so the admin loads.** Open the **Database Manager** from the dashboard, navigate to the `wp_options` table, and edit the rows where `option_name` is `siteurl` and `home` — set `option_value` to your new site URL. Or run this SQL from Adminer's **SQL command** view: ```sql UPDATE wp_options SET option_value = 'https://my-blog.zevcloud.app' WHERE option_name IN ('siteurl', 'home'); ``` (If your old install used a different table prefix than `wp_`, adjust accordingly.) **Part B: run search-replace across the rest of the database.** The rest of the database is full of references to the old domain: post content, embedded images, plugin settings, theme customiser values. WordPress stores some of these inside serialised PHP arrays, which is why a plain SQL `UPDATE` or a `sed` on the dump file will corrupt them. You need a tool that knows how to walk serialised data. > **Danger: Do not skip or shortcut this step** > > Skip the search-replace and the site looks like it imports cleanly, then falls apart the moment you click anywhere. Do not run a plain `UPDATE` query against post content, and do not `sed` the dump file. Use one of the tools below. **Option 1: Better Search Replace plugin (recommended for non-CLI users).** Now that the admin loads, log in, install [Better Search Replace](https://wordpress.org/plugins/better-search-replace/) from WP.org, and run it with: - **Search for:** `https://olddomain.com` - **Replace with:** `https://my-blog.zevcloud.app` - **Select tables:** all of them. - Run it once with **Dry run** ticked to confirm the diff, then once without. Run it a second time for `http://olddomain.com` if your old site had mixed protocols. **Option 2: WP-CLI** if your service has shell access: ```bash wp search-replace 'https://olddomain.com' 'https://my-blog.zevcloud.app' --all-tables wp search-replace 'http://olddomain.com' 'https://my-blog.zevcloud.app' --all-tables ``` WP-CLI also handles `siteurl` and `home` correctly in the same pass, so if you go the WP-CLI route you can skip Part A and run only this step. 6. ### Reinstall themes and plugins Log into the new admin at `https://.zevcloud.app/wp-admin/` with the admin credentials from step 1 (not the credentials from your old site). - **Themes:** install the theme from its original source. WP.org, ThemeForest, a private GitHub repo, wherever you got it. Activate it. The customiser settings in the database are still there from the import, so the theme should look the way it did on the old site. - **Plugins:** install each plugin from WP.org one at a time and activate them. Most plugins read their settings out of the database, so they pick up where they left off. A few (Yoast SEO, Wordfence, anything with API keys) need their settings re-entered. 7. ### Reconfigure email sending If your old site used an SMTP plugin (WP Mail SMTP, Post SMTP, FluentSMTP), the credentials in the database are likely for an SMTP host you no longer have access to. Re-enter them, or swap to an SMTP provider you control. If you did not have an SMTP plugin and were relying on WordPress's default `wp_mail()` over the host's mail server, install one now. WordPress sites on a PaaS need an SMTP relay to send mail reliably. 8. ### Verify the site Walk through the public site: - Home page renders correctly. - Posts and pages display with their original layouts. - Images and other media load (URLs should now point to `.zevcloud.app`, not the old domain). - Internal links go to the new domain. - Contact forms submit successfully. Then walk through the admin: - You can log in. - Posts and pages are listed correctly. - Editing a post and previewing it works. - Cron jobs are firing (check **Tools → Site Health → Info → WordPress Constants** for `DISABLE_WP_CRON`). ## Connecting your custom domain When the migrated site looks right on the `.zevcloud.app` URL, you can point your custom domain at it. 1. **Add the domain in the dashboard.** On the WordPress service, open **Domains → Add domain**, enter the domain, and follow the DNS instructions you see. 2. **Update DNS at your registrar.** This is usually a CNAME (for subdomains) or an A record (for apex / root domains). The dashboard shows the exact value to use. If your domain is registered through ZevCloud already, this step is one click. 3. **Wait for DNS to propagate.** Typically 5 to 30 minutes, occasionally longer. The dashboard polls and turns the domain green when it picks up. 4. **Run search-replace one more time** to swap from the temporary `.zevcloud.app` URL to your custom domain: ```bash wp search-replace 'https://my-blog.zevcloud.app' 'https://example.com' --all-tables ``` Or use Better Search Replace as before. 5. **Update WordPress site URL** in **Settings → General** if it has not already been updated by search-replace. ## Things that go wrong, and how to fix them ### Mixed-content warnings, broken images, CSS not loading You skipped step 5, or search-replace did not cover all tables. Re-run with **all tables** selected (or `--all-tables` on WP-CLI), and clear any caching plugin's cache. ### "Error establishing a database connection" right after import The import overwrote `wp_options.siteurl` and `wp_options.home` with the old domain, and now WordPress tries to load resources from a domain that no longer exists. Two ways out: - Run search-replace from WP-CLI even before the front end renders cleanly. CLI does not need a working front end. - Open your database client, find `wp_options`, edit the `siteurl` and `home` rows by hand to the new URL, then load the site. ### Plugins crash with "Fatal error: Uncaught Error..." Almost always a PHP version mismatch. WordPress on ZevCloud runs current PHP. Plugins that have not been updated since 2022 sometimes use functions removed in PHP 8+. - Check the plugin's last update date on WP.org. - Look for an updated alternative. - If the plugin is custom and yours, fix the call. - Worst case: deactivate it and find a replacement. ### Some media URLs still point to the old domain after search-replace A few plugins and themes store URLs in custom tables outside the `wp_` prefix, or in option values that the search-replace tool needs to be told about. In Better Search Replace, make sure **Select all tables** is ticked rather than leaving the default. With WP-CLI, name custom tables explicitly: ```bash wp search-replace 'https://olddomain.com' 'https://example.com' \ --all-tables \ --include-columns=meta_value,option_value ``` ### My old admin password does not work The imported database brought over the user accounts, but in some cases the password hashes do not validate against current PHP. Use the dashboard's **Reset admin password** action on the WordPress service, or, with WP-CLI: ```bash wp user update --user_pass='new-password' ``` ### The old site used a non-standard table prefix If the old install used `xyz_` instead of `wp_`, every reference in this guide (`wp_options`, `wp_users`, etc.) becomes `xyz_options`, `xyz_users`. The fresh ZevCloud install creates tables with the `wp_` prefix by default. Either match prefixes by editing `wp-config.php` to set `$table_prefix = 'xyz_';` before the import, or rename the tables in your dump to match `wp_`. ## When to ask for help Write to **support@zevcloud.net** before you continue if any of these apply: - The database dump is over 1 GB. - The site is a multisite or WordPress Network install. - WooCommerce with live orders. - Custom database tables outside the `wp_` prefix. - The old site was on PHP 5.6 or PHP 7.0. Include the site URL, what you have access to (database dump, uploads, neither), and where you are in the steps above. Migrations are a normal support flow, especially during the iNTECH Cloud Hosting transition. === # Custom Domains Source: https://docs.zevcloud.net/guide/domains --- Add your own domain to any service on ZevCloud. SSL certificates are automatically provisioned via Let's Encrypt. ## Adding a Custom Domain 1. Go to your service → **Domains** tab 2. Click **"Add Domain"** 3. Enter your domain (e.g., `myapp.com`) 4. Configure your DNS records (see below) 5. Click **Verify** ## DNS Configuration You have two options: ### Option 1: CNAME Record (Recommended) Add a CNAME record pointing to ZevCloud's proxy: | Type | Name | Value | |------|------|-------| | CNAME | `@` or `www` | `proxy.zevcloud.app` | > **Tip:** > > CNAME records are recommended because if we ever change server IPs, your domain will automatically update. > **Warning: Apex Domains** > > Some DNS providers don't support CNAME on the root/apex domain (`myapp.com` without `www`). In that case, use an ALIAS/ANAME record if your provider supports it, or use Option 2. ### Option 2: A Record If CNAME isn't possible, point an A record to our server IP: | Type | Name | Value | |------|------|-------| | A | `@` | `162.55.133.205` | ### Verification TXT Record You'll also need to add a TXT record for domain ownership verification: | Type | Name | Value | |------|------|-------| | TXT | `_zevcloud-verify` | `zevcloud-verify-` | The exact values are shown in the dashboard after you add the domain. ## SSL Certificate After DNS verification, an SSL certificate is automatically issued via Let's Encrypt. This typically takes 1-2 minutes. > **Tip:** > > If you see a "connection not private" error immediately after adding a domain, wait a minute for the SSL certificate to propagate. ## Primary Domain If you have multiple custom domains on a service: - The first custom domain added is automatically set as **primary** - The primary domain is used for display and (for WordPress) as the `siteurl` - You can change the primary domain from the Domains tab - The auto-generated domain (`slug.zevcloud.app`) always remains active as a fallback ## www Redirect When you attach a root domain (like `myapp.com`), visitors who type `www.myapp.com` should land on your site too. ZevCloud handles this with a **www redirect**: `www.myapp.com` responds with a permanent redirect to `myapp.com`, with the path and query string preserved. You'll find the toggle on the domain's card in the **Domains** tab. It only appears for root domains — subdomains like `app.myapp.com` don't have a www variant. ### Domains managed by ZevCloud If your domain was registered through ZevCloud (or its DNS is managed by us), everything is automatic: - The `www` DNS record is created for you - The redirect and its SSL certificate are configured at our edge network - Turning the toggle on or off takes effect within a minute — no redeploy needed ### Domains on external DNS If you manage DNS yourself, ZevCloud can't create the `www` record for you. When you enable the toggle, the dashboard shows the record to add at your DNS provider: | Type | Name | Value | |------|------|-------| | CNAME | `www` | `proxy.zevcloud.app` | If your provider doesn't allow a CNAME there, use an A record for `www` pointing to the server IP shown in the dashboard. Once the record resolves, the redirect and the SSL certificate for `www` are set up on the next deploy (the toggle triggers one for you). > **Tip: www doesn't load?** > > If `myapp.com` works but `www.myapp.com` shows a security warning or an error page, check two things: the **www redirect** toggle is on in the Domains tab, and (for external DNS) the `www` record exists at your DNS provider. ## Removing a Domain Click the delete icon next to the domain in the Domains tab. The service will revert to using the auto-generated domain. For WordPress services, the `siteurl` and `home` settings will be reverted to the auto-generated domain. ## Cloudflare Users If you use Cloudflare for your own DNS, set the DNS record for your domain to **"DNS Only"** (gray cloud), not "Proxied" (orange cloud). ZevCloud issues and manages the SSL certificate at the origin — if Cloudflare also proxies the same hostname, the two SSL layers conflict. === # Environment Variables Source: https://docs.zevcloud.net/guide/env-vars --- Set configuration values for your services without hardcoding them in your code. ## Setting Variables ### Via Dashboard 1. Go to your service → **Variables** tab 2. Enter the key and value 3. Toggle **Secret** if the value should be hidden 4. Click **Add** ### Via CLI ```bash zevcloud env set DATABASE_URL postgres://... zevcloud env set API_SECRET sk_live_... --secret ``` ### Via API ```bash curl -X POST https://api.zevcloud.net/v1/services//env \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{"key": "DATABASE_URL", "value": "postgres://...", "isSecret": false}' ``` ## Secret Variables Variables marked as **secret** are: - **Encrypted at rest** using AES-256 - **Hidden in the dashboard** (shown as `••••••••`) - **Hidden in CLI output** - Available to your application at runtime as normal environment variables > **Tip:** > > Always mark database passwords, API keys, and tokens as secret. ## Redeploy After Changes For **web applications**, environment variable changes take effect on the next deployment. Click **Redeploy** after adding or changing variables. For **WordPress** and **static sites**, changes take effect immediately (the container reads them on restart). ## System Variables ZevCloud automatically sets these variables for every service: | Variable | Description | |----------|-------------| | `PORT` | The port your app should listen on | | `NODE_ENV` | Set to `production` | === # GitHub Integration Source: https://docs.zevcloud.net/guide/github --- Connect your GitHub account to deploy private repositories and enable auto-deploy on push. ## Connecting GitHub 1. Go to **Settings → Integrations** or click **"Connect GitHub"** when adding a service 2. You'll be redirected to GitHub to install the ZevCloud app 3. Choose which repositories to grant access to (or all) 4. After installation, you're redirected back to ZevCloud ### Organizations You can connect multiple GitHub accounts and organizations: 1. Go to **Settings → Integrations** 2. Click **"Add GitHub Account"** 3. Select the organization to install on Each installation is listed separately. When creating a service, you can switch between accounts. ## Auto-Deploy on Push When enabled (default), every push to the configured branch triggers a deployment: 1. You push code to GitHub 2. GitHub sends a webhook to ZevCloud 3. ZevCloud starts a build 4. If successful, the new version goes live 5. Email notification sent (success or failure) ### Disabling Auto-Deploy Go to **Service Settings → Build & Deploy** and toggle off auto-deploy. You can still deploy manually from the dashboard or CLI. ## Private Repository Access ZevCloud uses the GitHub App's installation token to clone private repositories. You don't need to share SSH keys or personal access tokens. The token is refreshed automatically before each deployment. === # Auto-Deploy Source: https://docs.zevcloud.net/guide/auto-deploy --- Automatically deploy your application when you push code to GitHub. ## How It Works ``` You push to main branch ↓ GitHub sends webhook to ZevCloud ↓ ZevCloud clones the latest code ↓ Nixpacks detects language/framework ↓ Docker image built on build server ↓ Container deployed to production server ↓ Traffic routed to new version ↓ Email notification sent ``` ## Supported Triggers | Trigger | When | |---------|------| | **Push to branch** | Any push to the configured branch (e.g., `main`) | | **Manual deploy** | Click "Deploy" in dashboard or `zevcloud deploy` | | **API trigger** | `POST /v1/services/:id/deployments` | ## Configuration Auto-deploy is enabled by default when you connect a GitHub repository. Configure in **Service Settings**: - **Branch** — which branch triggers auto-deploy - **Root Directory** — if your app is in a monorepo subdirectory - **Auto-Deploy Toggle** — enable/disable ## Monorepos: Only What Changed Gets Deployed When several services share one repository (each with its own **Root Directory**), a push only redeploys the services whose folder actually changed: - A commit touching only `frontend/` redeploys the frontend service — the backend keeps running untouched - Changes to shared files outside every service's folder (a root `package.json`, lockfile, or `packages/` directory) redeploy **all** services from that repository, since they can affect any build - If the change set can't be determined reliably (very large pushes, force pushes), all matching services are redeployed to stay safe No configuration needed — this works automatically from each service's Root Directory. ## Build Settings Customize the build process: | Setting | Default | Description | |---------|---------|-------------| | Build Pack | Nixpacks (auto) | Auto-detects your language | | Build Command | Auto-detected | e.g., `npm run build` | | Install Command | Auto-detected | e.g., `npm ci` | | Start Command | Auto-detected | e.g., `npm start` | | Output Directory | Auto-detected | e.g., `.next`, `dist` | ## Deployment Notifications Email notifications are sent for: - ✅ Deployment succeeded - ❌ Deployment failed (with error summary) Configure notification preferences in **Settings → Notifications**. === # Metrics > See how your services use CPU, memory, network, and storage, live and over time. Source: https://docs.zevcloud.net/guide/metrics --- Every service on ZevCloud reports what it is doing while it runs. You can see live usage and recent trends without leaving the dashboard, so you can tell whether a service is healthy, why it might be slow, and whether you are getting close to your plan limits. ## Where to find metrics There are two views: - **Per service.** Open any service and go to the **Metrics** tab. This shows that one service in detail, with charts. - **Across your team.** The **Metrics** page in the sidebar shows every service in your team at a glance, one card each. Click a card to jump straight to that service's Metrics tab. ## Time range At the top of the metrics view you can switch between **1H**, **24H**, and **7D**. The live tiles always show current values. The charts follow the range you pick, so you can look at the last hour for a spike or the last week for a trend. ## What each number means | Metric | What it shows | |--------|---------------| | **CPU** | Processor use as a share of the CPU your plan includes, so the percentage is against your own allowance (it can read above 100% on a multi-core plan). | | **Memory** | RAM in use versus your plan's memory limit. Sitting near the limit for long is a sign to optimize or move up a plan. | | **Network** | Traffic in and out of your service, shown as a live rate over the selected range. | | **Disk I/O** | How fast your service is reading and writing to disk. This is activity, not capacity. | | **Disk usage** | How much storage your service's own files take up. This is separate from Object Storage buckets. | | **Connections** | For a managed database: active client connections against the maximum your plan allows. | | **Build** | For apps that build from a repository: the size of the built image and how long recent builds took. | > **Warning: Disk usage is not your storage buckets** > > "Disk usage" on a service is the space that one service's own files use. Your [Object Storage](/guide/storage) buckets are a separate product with their own usage, shown under Storage. A database's disk usage and a bucket's size are two different numbers. ## Databases: connection count A managed database adds a **Connections** tile showing active connections against your plan's limit. Watching it helps you catch a connection leak (an app that opens connections and never closes them) before it exhausts the pool and new requests start failing. If this number climbs and stays high while traffic is normal, look at how your app manages its database connections. ## Applications: build size and time An application that builds from a repository shows a **Build** section with the size of its built image and the duration of recent builds. If a change makes your build noticeably slower or your image larger, you will see it here. > **Tip: Give new metrics a minute** > > Metrics start appearing a couple of minutes after a service is running, and the trend charts fill in over the first hour. A service that is stopped or still deploying does not report live metrics. ## Who can see metrics Anyone on your team who can view a service can see its metrics. There is nothing to switch on. Metrics are available by default for every running service, on every plan. === # Connect via SFTP > How to connect to your ZevCloud WordPress site over SFTP using FileZilla, Cyberduck, or WinSCP. The most common errors and how to fix them. Source: https://docs.zevcloud.net/guide/sftp --- Every WordPress service on [ZevCloud](https://zevcloud.net) ships with SFTP access so you can upload media files, replace plugin or theme code, or pull files for local debugging. This guide covers how to connect, the most common error people hit, and what each field on your dashboard maps to. ## Where to find your SFTP credentials Open the [dashboard](https://console.zevcloud.net), pick the WordPress service, and look at the **SFTP / File Access** card on the Overview tab. You'll see: - **Host:** something like `.zevcloud.app`. - **Port:** a number like `2224` (each service gets its own). - **Username:** something like `sftp_`. - **Password:** revealed by clicking the eye icon. - **Protocol:** SFTP. You'll need all five. > **Tip: Heads up: SFTP, not FTP** > > SFTP and FTP are different protocols. They use different ports, different encryption, and different code paths in your client. ZevCloud only supports SFTP. If your client tries plain FTP on port 21 (or implicit FTPS on 990), the connection will fail with "Cannot establish FTP connection to an SFTP server." Make sure your client is set to **SFTP**, not FTP. ## The shortcut: paste an `sftp://` URL Most modern SFTP clients accept a URL in this shape: ``` sftp://: ``` For example: ``` sftp://my-blog.zevcloud.app:2224 ``` If you paste that into your client's "Host" or "Quickconnect" field, the client recognises the `sftp://` scheme and switches to SFTP automatically. You then enter the username and password from your dashboard when prompted. **This is the easiest way to avoid the FTP-vs-SFTP confusion.** If your client doesn't accept the URL form, use the split-field setup below. ## FileZilla ### Option 1: Quickconnect (fastest) 1. At the top of FileZilla, fill in: - **Host:** `sftp://` (the `sftp://` prefix is what tells FileZilla to use SFTP) - **Username:** from the dashboard - **Password:** from the dashboard - **Port:** from the dashboard 2. Click **Quickconnect**. ### Option 2: Site Manager (cleaner, saves the connection) 1. **File → Site Manager → New site**. 2. On the **General** tab: - **Protocol:** `SFTP - SSH File Transfer Protocol` (this is the critical setting) - **Host:** `` (no prefix needed when Protocol is set) - **Port:** from the dashboard - **Logon Type:** `Normal` - **User / Password:** from the dashboard 3. **Connect**. The Site Manager approach saves the connection so you can reopen it in one click. ## Cyberduck 1. Click **Open Connection** in the toolbar. 2. At the top of the dialog, change the protocol drop-down from **FTP** to **SFTP (SSH File Transfer Protocol)**. 3. Fill in: - **Server:** your host - **Port:** your port - **Username:** from the dashboard - **Password:** from the dashboard (click the eye to reveal first) 4. **Connect**. To save it: tick **Add to Keychain** before connecting. ## WinSCP 1. Open WinSCP. The **Login** dialog appears. 2. **File protocol:** `SFTP`. 3. Fill in: - **Host name:** your host - **Port number:** your port - **User name:** from the dashboard - **Password:** from the dashboard 4. **Save** (optional, names the connection so it's reusable), then **Login**. ## What you can do with SFTP Once connected, the home directory contains your WordPress install. The folders that matter: - `/wp-content/uploads/` — every image, video, PDF, and attachment your posts reference. This is what migrations bring over from old hosts. - `/wp-content/themes/` — installed themes. You can SFTP-upload a theme's folder here as a faster alternative to the WP admin uploader, especially for themes >2 MB. - `/wp-content/plugins/` — same, for plugins. - `/wp-config.php` — WordPress core config. Generally do not edit by hand unless you know what you're doing. ZevCloud writes the database and salt values here at provisioning time. ## Common errors and fixes ### "Cannot establish FTP connection to an SFTP server" The classic. Your client is trying FTP, but the server only speaks SFTP. Fix: - In FileZilla, prefix the host with `sftp://` or change the Site Manager protocol to **SFTP - SSH File Transfer Protocol**. - In Cyberduck, change the protocol drop-down to **SFTP**. - In WinSCP, set **File protocol** to **SFTP** in the login dialog. ### "Connection refused" Either you're using the wrong port (each service has its own — check the dashboard), or your network blocks outbound traffic on that port. Most home / corporate networks allow it; some hotel WiFi and school networks don't. Try a mobile hotspot to confirm it's a network issue, not a credentials issue. ### "Authentication failed" or "Wrong password" The dashboard masks the password by default. Click the eye icon to reveal it, then copy. Watch out for trailing spaces if you select-and-copy. If you're sure the password is right, regenerate it from the dashboard's **SFTP / File Access** card and try the new one. ### "Host key has changed" warning You'll get this the first time you connect (your client wants to remember the server's identity) and never again. Accept the key. If you see it on a *subsequent* connection, the server's been rebuilt — common after a redeploy. Accept the new key and continue. If you're paranoid, contact support before accepting an unexpected change. ### Connection works but uploads are slow Each WordPress service runs on shared infrastructure. SFTP throughput is enough for typical WordPress operations (theme uploads, media files), but a multi-gigabyte one-time transfer might be slower than copying to a local SSD. For the initial migration of a large `wp-content/uploads/` folder, expect 10-20 minutes per few thousand images on a normal connection. That's network, not the server. ## Next steps - Migrating a WordPress site from another host? See [Migrate a WordPress site to ZevCloud](/guide/migrate-wordpress). - Setting up a custom domain? See [Custom Domains](/guide/domains). - Need shell access (not just file transfer)? Use the **Terminal** tab on your service detail page in the [dashboard](https://console.zevcloud.net). === # Database management > Browse, query, import, and export your database from the browser using the built-in Database Manager. Works for WordPress, MySQL, MariaDB, and PostgreSQL services. Source: https://docs.zevcloud.net/guide/database-management --- The **Database Manager** is a web GUI scoped to one database, accessed straight from the dashboard. Use it to browse tables, run SQL, import or export data, and anything else you'd normally do in phpMyAdmin or Adminer. It's available for: - **WordPress services** — manages the site's MariaDB database (paid plans only) - **Standalone SQL databases** — `mysql`, `mariadb`, and `postgresql` engines (every plan, free and paid) - **Not available** for MongoDB and Redis services — see the [Databases guide](/guide/databases#database-manager-web-gui) for the desktop-client path instead ## Opening the Database Manager 1. Open your service in the [dashboard](https://console.zevcloud.net). 2. **WordPress services:** click the **Database** tab. **Standalone databases:** open the service's **Overview** tab. 3. Click **Open database manager**. A new browser tab opens into the GUI. 4. The host, driver, and database name are pre-filled. Sign in with the database credentials shown on the same tab — username and password. You can only sign into your own service's database. The session lasts for the browser tab's lifetime. If you close the tab and come back later, click **Open database manager** again — it issues a new short-lived URL (30 minutes) so you don't need to copy URLs around. ## Common tasks ### Browse and edit data The left sidebar lists every table in the database. Click a table name to see its rows in a paginated grid. **Edit** modifies a row; **New item** inserts one. Use **Search** at the top of the row view to filter — it generates a `WHERE` clause behind the scenes. For WordPress this is most useful for `wp_options` (changing `siteurl`, `home`, mail-from address, anything stored as a key/value), `wp_users` (resetting a stuck account), and `wp_posts` (rescuing an autosave that the admin won't load). ### Run SQL Click **SQL command** in the left sidebar. The query runs against your service's database only — there's no way to escape into another service or another tenant. Useful examples: ```sql -- See your current siteurl after a migration SELECT option_name, option_value FROM wp_options WHERE option_name IN ('siteurl', 'home'); -- Find every post that mentions an old domain SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%olddomain.com%' AND post_status = 'publish'; -- Count comments per post SELECT post_id, COUNT(*) AS comments FROM wp_comments WHERE comment_approved = '1' GROUP BY post_id ORDER BY comments DESC LIMIT 20; ``` > **Warning: Don't run a plain UPDATE for URL search-replace** > > WordPress stores some option values inside serialised PHP arrays. A plain `UPDATE` (or `sed` on a dump file) corrupts them. For URL replacements across post content, plugin settings, and theme customiser values, use the **WP-CLI search-replace** flow described in the [migration guide](/guide/migrate-wordpress) — it walks serialised data correctly. ### Export a backup Click **Export** in the left sidebar. - **Output:** `gzip` (smallest file, recommended) - **Format:** `SQL` - Tick **Tables**, **Data**, and either every table or just the ones you need. - Click **Export**. Your browser downloads the result. This is the fastest way to grab a quick backup before doing anything risky. For routine off-site backups, run the export on a schedule from a script (most teams use a `cron` job or a CI workflow that calls `mariadb-dump` against the same credentials). ### Import a `.sql` or `.sql.gz` dump Click **Import** in the left sidebar, pick your `.sql` or `.sql.gz` file under **File upload → Choose files**, click **Execute**. The Database Manager streams the dump straight into the database. The upload limit is **1 GB per file**, which covers virtually every WordPress migration. Both `.sql` and `.sql.gz` work — gzipped files are decompressed transparently, so you don't need to gunzip first. > **Warning: Don't use 'From server' for migration dumps** > > You'll see a "From server" option below the file picker. That reads files from the Database Manager's own filesystem — which is a separate container that doesn't have access to your WordPress site's files. Even if you SFTP'd a `.sql` file into your service, the Database Manager won't see it there. Always use **File upload → Choose files** instead. A 400 MB dump typically takes 1–5 minutes depending on your upstream connection — most of that is the browser-to-server transfer; the import itself is fast once the file lands. > **Tip: Importing replaces what's there** > > Importing a dump that contains your full schema and data overwrites anything that's already in the matching tables. For WordPress migrations specifically, that's exactly what you want — the dump replaces the fresh install. For other situations (merging data, partial restores), be more careful: export first, then import. ## Permissions and access The credentials shown on the dashboard give your service its own database user with full rights inside its own database, and nothing outside it. The Database Manager logs in as the same user. There's no way to reach another service's data — neither via the GUI nor via direct credentials. By default the database is reachable only through the Database Manager URL or the service's internal hostname (from other ZevCloud services). If you need to connect from your laptop or an external service, turn on **Public Access** on the service's Overview tab (paid plans only) — see [Databases → Connect from your laptop](/guide/databases#connect-from-your-laptop-or-an-external-service). ## Database manager URL details A few things worth knowing about the URL the dashboard opens: - **Short-lived.** The URL contains a signed token that expires after 30 minutes. After that, requests inside the manager start returning 401 — close the tab, click **Open database manager** again, and you get a fresh URL. - **Service-scoped.** A URL signed for one service won't work on another service's subdomain, even if you copy it across. - **Don't share it.** While it's valid, anyone with the URL can reach the login page (they still need the database password). The 30-minute expiry limits the blast radius, but treat the URL as semi-sensitive. ## Troubleshooting ### "Invalid or expired token" when I open the manager The signed URL has expired (30-minute limit) or you're trying to reuse a URL from yesterday. Close the tab, go back to the dashboard's **Database** tab, click **Open database manager** again. You'll get a fresh URL. ### Adminer's login form rejects my password You're using your ZevCloud account password instead of the **database** password. The Database Manager logs into the database directly, not into ZevCloud. Open the **Database** tab on your service and copy the password under **Database Connection** (the masked one with the eye icon). ### "From server" doesn't show the file I just uploaded Two common causes: - The file landed in `wp-content/uploads/` rather than the root of your home directory. Adminer's "From server" looks in the home directory only. SFTP the file to the top level — `put old-site.sql.gz` without changing directories first. - The file isn't named `.sql` or `.sql.gz`. Adminer filters for those extensions. If your dump is named `dump` or `dump.txt`, rename it to `dump.sql` or `dump.sql.gz`. ### Nothing happens when I click Open database manager Browser popup blocker. The button opens the manager in a new tab; if your browser is set to block all popups, the new tab gets killed before it can navigate. Allow popups from `console.zevcloud.net` and try again. Most browsers also let you click an "allowed?" indicator in the address bar to permit the popup just-blocked. ## When to ask for help Write to **support@zevcloud.net** if: - You see a 500 error inside the Database Manager (rare; usually means the database container is misbehaving). - You're trying to do something that needs cross-service database access (which we don't expose by design — we'll discuss the right approach). - You broke the database with a bad query and don't have a recent export. We keep nightly backups for paid plans; we can restore one for you. Include the service URL, the time the issue started, and what you were doing. === # Buckets > Create, connect, and manage ZevCloud Storage buckets. Public vs private, connecting to services, custom domains, key rotation, and lifecycle. Source: https://docs.zevcloud.net/guide/storage-buckets --- A bucket is the unit you create, name, and pay against in ZevCloud Storage. This page walks through everything you can do with one: create it, connect it to a service, attach a custom domain, rotate its key, and tear it down. If you're new to the storage feature, start with the [Object Storage overview](/guide/storage) first. ## Creating a bucket 1. Open the [dashboard](https://console.zevcloud.net) and click **Storage** in the left nav. 2. Click **Create bucket**. The form asks for four things: - **Name suffix.** The piece you choose; the platform prefixes your team slug to it so bucket names stay unique across the whole platform without you needing to coordinate with anyone. Allowed: lowercase letters, digits, and hyphens. Maximum length is the same as S3's. - **Region.** Pick the region closest to where your app runs or where your users are. If you're connecting this bucket to a ZevCloud service, picking the same region keeps the traffic inside one data centre. - **Public read.** Off for private buckets (uploads from your app, backups, anything that should require credentials to read). On for buckets whose objects should be loadable from a browser at a stable URL. Only available on paid plans. - **Object Lock.** Off by default. Turn on for compliance workloads where data must not be overwritten or deleted within a retention period. **This is permanent**. Object Lock cannot be turned off later on the same bucket. 3. Click **Create**. The bucket lands in `active` status within a few seconds and the create response includes the access key ID and a one-time secret. 4. **Copy the secret access key now.** The dashboard shows it once. The access key ID stays visible on the detail page; the secret never comes back. If you closed the tab without copying the secret, no problem. [Rotate the key](#rotating-keys) and you'll get a fresh secret to copy. ### Picking public-read vs private This decision is at create time, so it's worth thinking through. - **Private** is the default and the right answer for most things. App uploads, document storage, backups, build artifacts, anything where you don't want random people fetching your objects without credentials. Your app and other authorised tools read and write using the access key. - **Public-read** makes every object in the bucket fetchable at a stable URL with no credentials, the same way an asset on a CDN works. Right answer for: marketing site images, video files, public PDFs and downloads, any static file you want loaded directly by a browser. Writes still require the access key. Only reads become public. You can flip a bucket between private and public-read after creation from the bucket detail page (see [Toggling public-read on an existing bucket](#toggling-public-read-on-an-existing-bucket)). Enabling public-read requires a paid plan. ### Bucket names: what to expect The dashboard checks availability as you type. The name suffix you pick becomes part of the full bucket name, which is what you'll see in tools and use in your SDK calls. The "Bucket name" line on the detail page is what to copy into your S3 client, not the suffix you typed. If the suffix you want is already taken by another team's bucket on the platform, the dashboard tells you up front and the create flow appends a short unique tail so the create still succeeds. Your bucket name might end up with a tail like `-abcd` in that case. ## Listing and inspecting buckets Open **Storage** in the dashboard's left nav. The Buckets tab shows every bucket on the team, with current status, region, size, and a quick "owner can view credentials" badge. Click any bucket to open its detail page. You get: - **Bucket name and endpoint** to plug into S3 clients. - **Access key ID** (owners and admins only). - **Usage** as a percentage of the team's plan cap. - **Objects tab** for browsing, uploading, and downloading directly. - **Connected services tab** for seeing which of your app services have this bucket wired in. - **Custom domains tab** for attaching your own hostnames (paid plans). - **Settings tab** for rotation, deletion, and the public-read state. The **Refresh usage** button on the detail page recalculates the bucket size right now. The dashboard's usage counter otherwise updates on a short interval, so if you just uploaded or deleted a lot of data through an external tool the displayed size may be stale until the next refresh. ## Authenticating with the S3 endpoint ZevCloud Storage speaks the standard S3 API. Any S3-compatible client works: AWS SDKs (boto3, aws-sdk for JavaScript, etc.), the AWS CLI, `rclone`, `mc`, `s3cmd`, anything that takes an endpoint URL. You need four values, all from the bucket's detail page: | Field | Where it shows up in the dashboard | Notes | |-------|------------------------------------|-------| | **Endpoint** | "S3 configuration" card, Endpoint row | Same for every bucket on the platform. Always `https://storage.zevcloud.net`. | | **Region** | Region row | Required by AWS SDKs for signing. Use exactly the value shown. | | **Bucket** | Bucket row | The full bucket name, including the platform prefix. Copy this verbatim. | | **Access Key ID** | Access Key ID row | Visible to team owners and admins. Other members see a "credentials hidden" stub. | | **Secret Access Key** | Shown ONCE at bucket creation and again at every key rotation | Not stored in a way ZevCloud can read back. If you lose it, rotate the key to get a fresh one. | > **Warning: The secret is shown once** > > Capture the secret access key when the dashboard displays it after bucket creation or rotation. ZevCloud encrypts it at rest and never exposes the plaintext again. If you missed it, click **Rotate key** on the bucket detail page to mint a fresh pair. The old key keeps working for 24 hours so anything currently using it has time to update. ### Code examples **JavaScript (AWS SDK v3)** ```js import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3' const s3 = new S3Client({ endpoint: 'https://storage.zevcloud.net', region: 'eu-central-1', credentials: { accessKeyId: process.env.ZEVSTORAGE_ACCESS_KEY_ID, secretAccessKey: process.env.ZEVSTORAGE_SECRET_ACCESS_KEY, }, forcePathStyle: true, }) await s3.send(new PutObjectCommand({ Bucket: process.env.ZEVSTORAGE_BUCKET, Key: 'images/avatar.png', Body: imageBuffer, ContentType: 'image/png', })) ``` **Python (boto3)** ```python import boto3 s3 = boto3.client( 's3', endpoint_url='https://storage.zevcloud.net', region_name='eu-central-1', aws_access_key_id=os.environ['ZEVSTORAGE_ACCESS_KEY_ID'], aws_secret_access_key=os.environ['ZEVSTORAGE_SECRET_ACCESS_KEY'], ) s3.put_object( Bucket=os.environ['ZEVSTORAGE_BUCKET'], Key='images/avatar.png', Body=image_bytes, ContentType='image/png', ) ``` **AWS CLI** ```bash export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... export AWS_REGION=eu-central-1 aws s3 ls s3:/// --endpoint-url https://storage.zevcloud.net aws s3 cp ./file.png s3:///images/avatar.png --endpoint-url https://storage.zevcloud.net ``` You'll likely want a shell function or alias since the endpoint flag is repetitive. ### A few things worth knowing **Use path-style addressing.** Set `forcePathStyle: true` (JavaScript) or `s3.client.config.path_style = True` (boto3 by default works). Path-style means URLs look like `https://storage.zevcloud.net//` rather than `.storage.zevcloud.net/`. Path-style is what ZevCloud Storage expects. **The region value is required even though there's one region today.** AWS SigV4 (the signing algorithm every S3 client uses) bakes the region into the signature. The value must match what the bucket detail page shows. Don't make one up. **Presigned URLs work for time-limited shares from private buckets.** When you don't want to make a bucket fully public but want to hand a single object to a browser temporarily, mint a presigned GET URL on your backend and return it to the client: ```js import { getSignedUrl } from '@aws-sdk/s3-request-presigner' const url = await getSignedUrl( s3, new GetObjectCommand({ Bucket: '', Key: 'private/report.pdf' }), { expiresIn: 300 }, // 5 minutes ) ``` The URL works in a browser without any auth header and stops working when it expires. **Credentials hidden for members.** If you're a team member (not owner or admin), the dashboard hides the Access Key ID and never shows the secret. Ask an owner or admin for the credentials, or have them rotate on your behalf. ## Connecting a bucket to a service The cleanest way for a ZevCloud-deployed app to use a bucket is the **Connect storage bucket** flow on the service's Variables tab. This injects the bucket's credentials as managed environment variables and keeps them in sync with key rotations automatically. 1. Open the service in the dashboard. Click **Variables**. 2. Click **Connect storage bucket**. Pick the bucket from the dropdown. 3. Five managed environment variables now appear on the service: | Variable | Value | |----------|-------| | `ZEVSTORAGE_ENDPOINT` | Region-scoped S3 endpoint URL | | `ZEVSTORAGE_REGION` | The bucket's region | | `ZEVSTORAGE_BUCKET` | The full bucket name | | `ZEVSTORAGE_ACCESS_KEY_ID` | Bucket's active access key ID | | `ZEVSTORAGE_SECRET_ACCESS_KEY` | Bucket's active secret access key | These are marked as managed in the dashboard. You can't edit them by hand. The platform owns the values; if you rotate the bucket's key, these update automatically. Alongside each one, the same value is also published under an older `S3_` name (`S3_BUCKET`, `S3_ACCESS_KEY_ID`, and so on). Those names came first and still work, so nothing you have already deployed needs changing. Use the `ZEVSTORAGE_` names in new code: ZevStorage speaks the S3 protocol, but the storage is ours, not Amazon's, and the clearer name saves the next person reading your config from guessing. 4. Redeploy the service so the runtime picks up the new env. From this point on, your app code can use the variables exactly as it would with any S3 SDK. ### Connecting more than one bucket A service can hold one main bucket plus any number of named ones. Give the second bucket a name when you connect it, and that name becomes part of its variables: | Variable | Value | |----------|-------| | `ZEVSTORAGE_UPLOADS_ENDPOINT` | Region-scoped endpoint URL | | `ZEVSTORAGE_UPLOADS_REGION` | The bucket's region | | `ZEVSTORAGE_UPLOADS_BUCKET` | The full bucket name | | `ZEVSTORAGE_UPLOADS_ACCESS_KEY_ID` | Bucket's active access key ID | | `ZEVSTORAGE_UPLOADS_SECRET_ACCESS_KEY` | Bucket's active secret access key | Names are upper-case letters, numbers and underscores, starting with a letter, up to 24 characters. `UPLOADS`, `BACKUPS` and `USER_MEDIA` all work. A named bucket gets no `S3_` mirror, because there is no existing code to keep working. Only the main bucket carries the older names. > **Note:** > > The dashboard asks for a name as soon as a service already has a main bucket. Two unnamed buckets would write to the same five variables, so whichever connected last would quietly take over the first one's settings inside the container. Code-side, you just read the env vars. Full example with the AWS SDK for JavaScript: ```js import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; const s3 = new S3Client({ endpoint: process.env.ZEVSTORAGE_ENDPOINT, region: process.env.ZEVSTORAGE_REGION, credentials: { accessKeyId: process.env.ZEVSTORAGE_ACCESS_KEY_ID, secretAccessKey: process.env.ZEVSTORAGE_SECRET_ACCESS_KEY, }, forcePathStyle: true, }); // Upload await s3.send(new PutObjectCommand({ Bucket: process.env.ZEVSTORAGE_BUCKET, Key: 'uploads/user-123/avatar.png', Body: imageBuffer, ContentType: 'image/png', })); // Presigned read URL the browser can fetch (private bucket) const url = await getSignedUrl( s3, new GetObjectCommand({ Bucket: process.env.ZEVSTORAGE_BUCKET, Key: 'uploads/user-123/avatar.png', }), { expiresIn: 3600 }, ); ``` `boto3` and every other S3 SDK follow the same pattern with the same four config values. ### Disconnecting On the service's Variables tab, click the connected bucket entry and choose **Disconnect**. The five managed env vars are removed. The bucket itself is untouched. Your app loses access; redeploy after disconnecting if it needs to start cleanly without those vars. ### Connecting one bucket to multiple services Yes. Connect it from each service separately. Each connection injects the same managed env vars, all pointing at the same bucket and key pair. A key rotation updates every connected service simultaneously. ### Connecting several buckets to one service Also yes, and you no longer need to enter the second one's credentials by hand. Connect it the same way and give it a name, and it arrives as its own set of managed variables under that name. See [Connecting more than one bucket](#connecting-more-than-one-bucket) for the naming rules. If you already worked around this with hand-entered variables, they keep working, but they are outside rotation. Connecting the bucket properly and dropping the manual copies is worth the one code change to the new variable names. ## Public buckets and custom domains Every public-read bucket exposes its objects at **two different URLs**. They do different jobs and a typical integration uses both. ### The S3 endpoint vs the public URL | Surface | Looks like | Use for | Auth | |---------|-----------|---------|------| | **S3 endpoint** (under "S3 configuration") | `https://storage.zevcloud.net` | Programmatic access from your app or scripts using an S3 SDK. Uploads, deletes, listing, anything that needs auth or logic. | Required (access key + secret) | | **Public URL** (under "Public URL") | `https://cdn.zevcloud.net//` | Loading objects directly in a browser. ``, video players, download links, anywhere you want a plain HTTP fetch. | None. Anyone with the URL can read. | The S3 endpoint never changes between private and public buckets. It's how every authenticated S3 client talks to ZevCloud Storage regardless of bucket visibility. The public URL only appears for public-read buckets and only resolves while the bucket is public AND the team is on a paid plan. A typical integration: your backend uploads `images/avatar.png` to a public bucket using the S3 endpoint plus credentials. Your frontend then loads it from `https://cdn.zevcloud.net//images/avatar.png` with no token and no signing. Two URLs, one bucket, different purposes. If a bucket is private, the public URL section is hidden. The only way to read objects is via the S3 endpoint with credentials, or by minting a short-lived presigned URL from your backend. ### Toggling public-read on an existing bucket You don't need to create a new bucket to change visibility. From the bucket detail page: 1. Open the bucket. The header row shows the current state (Public-read or Private) next to the region. 2. Click **Make public** or **Make private** in the action row. 3. Confirm in the modal. The dashboard explains what changes. Flipping to public means anyone with the URL can read every object. Flipping to private means saved public URLs stop working immediately. Apps using S3 credentials are not affected either way. 4. The change applies instantly. If you went public, the Public URL section appears with your base URL. If you went private, that section disappears and public requests stop serving on the next request. Public-read requires a paid plan. On the Free plan the button opens an upgrade prompt instead of flipping the bucket. ### Using the public URL in your app The public URL is a plain HTTP URL. Any client that can fetch HTTP can read it. A few practical patterns: **HTML image and download links** ```html Avatar Download ``` **Frontend fetch from JavaScript** ```js const res = await fetch('https://cdn.zevcloud.net//data/menu.json') const menu = await res.json() ``` No SDK, no signing, no headers. The bucket name goes after the host; the object key goes after that. Object keys can contain slashes and are treated as the full path. **Range requests work** for partial downloads and HTML5 video seek: ```bash curl -H "Range: bytes=0-1023" https://cdn.zevcloud.net//videos/demo.mp4 ``` Returns `HTTP/2 206 Partial Content` with the requested byte range. **ETag and Last-Modified are returned** so conditional requests (`If-None-Match`, `If-Modified-Since`) get `304 Not Modified` responses and your caching layer can skip transferring unchanged objects. ### CORS for browser uploads If your frontend uploads directly to ZevCloud Storage using presigned URLs from your backend, you'll need CORS configured on the bucket. Configure it in the dashboard: **Storage → your bucket → CORS**. Add a rule with your app's origin, the methods you need (`GET` and `PUT` for presigned uploads), and save. Your bucket access key cannot set CORS itself — `put-bucket-cors` and `get-bucket-cors` return `AccessDenied` for bucket keys by design. Bucket-level configuration is applied by the platform on your behalf, which keeps your key scoped to reading and writing objects rather than reconfiguring the bucket. Use the dashboard, or the API: ```bash curl -X PUT https://api.zevcloud.net/v1/storage/buckets//cors \ -H "Authorization: Bearer " \ -H "x-team-id: " \ -H "Content-Type: application/json" \ -d '{ "rules": [ { "allowedOrigins": ["https://yoursite.com"], "allowedMethods": ["GET", "PUT", "HEAD"], "allowedHeaders": ["*"], "maxAgeSeconds": 3600 } ] }' ``` A browser always sends an `OPTIONS` preflight before a `PUT` whose content type isn't CORS-simple — which includes `image/*` and `application/json`. Without a matching rule the preflight fails and the upload never leaves the browser. GET requests for public-read objects don't strictly need CORS unless you're using `fetch()` with credentials or reading response bodies in a Worker. Browsers happily load `` and `