# Download a finished export Source: https://docs.pivocloud.com/api-reference/download-a-finished-export /openapi.yaml get /api/v1/dbs/{id}/exports/{eid}/download Downloads the archive produced by an export. The link is the one the export request returned, signature and expiry included, so it needs no token of its own. Request it verbatim rather than rebuilding it. The export is still being prepared when the link is first handed to you, so the first requests answer 503 and carry a short JSON reply instead of an archive. Always pass `--fail` and `--retry`. Without them, curl saves that JSON reply into your output file and reports success, and you are left with a small text file named like a backup: ```bash curl --fail --retry 30 -o backup.sql.gz "$URL" ``` `--fail` stops the reply body being saved when the answer is not an archive, and `--retry` makes curl wait for the interval in the Retry-After header and ask again until the archive is ready. Compare the file you saved against the X-Export-SHA256 header before you rely on it. # Start a database export Source: https://docs.pivocloud.com/api-reference/start-a-database-export /openapi.yaml post /api/v1/dbs/{id}/export Starts a fresh export of one of your databases and returns a signed download URL straight away. The export itself runs after this request returns, so the link is not ready at the moment you receive it: it answers 503 while the export is still being prepared, then serves the archive once it finishes. A script can therefore request an export and poll that one link, without asking for a status anywhere else. See the download endpoint for the request that does this safely. # How do I call the API with a token? Source: https://docs.pivocloud.com/api/personal-access-tokens How to create a personal access token in the PivoCloud console, how to send it with a request that starts a database export, what the reply carries, and what to do when the API answers that you should wait rather than starting one. ## Calling the PivoCloud API with a personal access token The API lets a script do one thing today: start an export of one of your own databases, and download the result. This page covers that in three parts, in the order you meet them. Creating a token in the console. Sending a request with it and reading what comes back. And what to do when the API answers that you should wait rather than starting an export. ### Create the token A personal access token is created in the console, and it is the only credential the API accepts. Every token is fixed to one thing, pulling backups of your own databases, so there is no scope to choose and no permission to set. 1. Open your profile in the console and find the `API Tokens` section. 2. Press `Create token`. A dialog opens, titled `Create API token`. 3. Type a name into `Token name`. The name is the only thing the dialog asks for, and it is there so that you can recognise this token later in the list. 4. Press `Create token` in the dialog. The full secret is then shown to you once, in a panel that warns you it will not be shown again. That warning is literal. The list below the panel only ever shows a masked prefix of each token, and nothing anywhere re-fetches the secret, so the moment the panel is on your screen is the only moment you can copy it. Put it wherever your script reads it from before you dismiss the panel. If you lose it, revoke the token and create a new one. The same section lists the tokens you already have, with the date each was created and when each was last used, and a `Revoke` button on every row. Revoking takes effect at once, so anything still calling the API with that token starts failing straight away. ### Call the API One request starts an export. It is a `POST` to the export path for the database you want, and it carries your token as a bearer credential in the request's authorization header: ```bash theme={null} curl -X POST "https://api.pivocloud.com/api/v1/dbs/00000000-0000-0000-0000-0000000000db/export" \ -H "Authorization: Bearer pvc_REDACTED000000000000000000000000000000000000" ``` Two values in that line are placeholders and the rest is exactly what a working call looks like. Put the identifier of the database you want to export in place of the one in the path, and your own secret in place of the token. The reply is `200`, and its body carries two fields: ```json theme={null} { "expires_at": "2027-01-15T08:00:00Z", "url": "https://api.pivocloud.com/api/v1/dbs/00000000-0000-0000-0000-0000000000db/exports/00000000-0000-0000-0000-00000000e401/download?exp=1800000000&sig=REDACTED00000000000000000000000000000000000000000000000000000000" } ``` `url` is the link your export will be served from. `expires_at` is the moment that link stops working, so a script that stores the link is better off storing that with it. Both are absolute values the server has already assembled, so there is nothing on your side to build and nothing to guess at. The expiry in that example is a placeholder like the other values in it, and it is not a typical one. A real link is short-lived, minutes rather than days, so read the moment out of `expires_at` rather than budgeting from what is printed above. A link asked for after that moment answers `401` instead of serving the archive, which is the link having expired rather than anything wrong with your request. Mint the link close to when you will use it, and start a new export if the one you have has lapsed. The link carries its own authorisation. The signature inside it is what grants access, so downloading needs no token and no header at all: a plain `GET` on that address is the whole download. Treat the link exactly as you treat the token, because anyone holding it can fetch that dump until it expires. It is not a public address that merely happens to be long. The export itself starts after this request returns, so the link is not ready at the moment you receive it. It answers `503` while the archive is still being prepared, then serves the archive once it is finished. A script can therefore ask for an export and poll that one link until it succeeds, without asking for a status anywhere else. Poll on a wait and stop on anything else. `503` is a wait, and so is `429`; both carry the interval to sleep for in their `Retry-After` header. Every other reply is the end of that loop rather than a step in it. An export that failed is the one to plan for: the link answers `404` from then on and keeps answering `404` however long you keep asking, so a loop that reads it as "not ready yet" runs forever. Treat anything that is not a wait as the export not coming, and start a new one. The full list of replies each endpoint can send is in the generated reference rather than on this page, so there is one copy of it and it comes from the same document the API itself is described by. ### When the API tells you to wait Two of the export endpoint's answers are a refusal to start a new export right now rather than a failure, and both arrive as `429`. From the status code alone they are indistinguishable, and a script that treats them as the same thing sits out a whole cooldown window for a wait that should have been seconds. Read the code in the body to tell them apart, and read the wait from the `Retry-After` header of the reply rather than from a figure written down anywhere. That header carries the number of seconds to wait, the server sets it per reply, and it is the only value that is true at the moment you receive it. This page deliberately prints no figure: the cooldown window is read from configuration when the API starts, so a number published here would quietly stop being true. A code you do not recognise is safest treated as a wait as well: ask again after the interval the header names, rather than stopping the job outright. `cooldown_active` means this database was exported recently. The wait is the whole cooldown window and there is nothing to poll, because your request started no export. This is the one to sleep through. `export_in_flight` means an export for this database is already running. The wait is short, seconds rather than a cooldown window, and sleeping through a full cooldown here is the mistake this section exists to prevent. The better move is not to ask again at all. The request that started that export already returned a link, and that link resolves to the archive once the export finishes, so poll the link you were given. A third code also arrives as `429` and it is not about your database at all. `RATE_LIMIT_EXCEEDED` is the limit on how often you may call the API, it applies to every endpoint here rather than to one database, and the same rule covers it: read the wait from `Retry-After` and ask again. ### The full reference Both endpoints are documented in full, with every reply each one can send, and with a form you can send a real request from: * [Start a database export](/api-reference/start-a-database-export) * [Download a finished export](/api-reference/download-a-finished-export) For backups and exports taken from the console rather than from a script, see [how do I get my data back](/databases/recovery). # How do I connect GitHub to PivoCloud? Source: https://docs.pivocloud.com/apps/connect-github Install the PivoCloud GitHub App, see exactly which repositories PivoCloud can read and why, including private and organisation-owned ones, and change that set from GitHub whenever you need to. ## Connect GitHub PivoCloud pulls your code through the PivoCloud GitHub App. You install it once, you choose which repositories it may read, and every app you create afterwards picks from that set. Private repositories work through this connection. So does deploying on every push. ### Install the App Open `Integrations` in the console sidebar. Before you connect, that page shows one heading, `No GitHub connection`, the line `Connect the PivoCloud GitHub App to deploy private repositories and enable automatic push-to-deploy.`, and one button: `Connect GitHub`. Press it and you land on GitHub, where the App installation is set up. GitHub asks the question that decides everything on this page: **all repositories, or only the ones you select.** Answer it, confirm the install, and GitHub sends you back to the console. Once you are back, `Integrations` shows a `GitHub App` card carrying a `GitHub connected` badge, a count of the repositories the installation can reach, and the GitHub account it belongs to. Under the card, an `Authorized Repositories` section lists them one by one. Two links sit on the card: `Manage on GitHub`, which opens this installation's settings on GitHub, and `Disconnect`. Start from the console's own `Connect GitHub` button rather than from a bookmarked GitHub address. The console builds that link for the environment you are signed in to, so it always points at the right App. ### Which repositories PivoCloud can see The set is exactly the repositories your GitHub App installation is authorized for. Nothing more, nothing less. That is a choice you make on GitHub at install time, and you can change it whenever you like. PivoCloud adds no filter of its own. It does not ask GitHub for public repositories, or for personal ones, or for recently updated ones. It asks the installation for its repositories and lists what comes back. Four cases come up, and all four are normal. **A private repository appears like any other.** Every row carries a badge, either `Private` or `Public`. Being private is never the reason a repository is missing: reading private code is what this connection exists for. **Repositories owned by an organisation sit on the same footing as your own,** as long as the installation covers them. Installing on an organisation is a separate install from installing on your personal account, and some organisations require an owner to approve the request before it takes effect. That approval happens on GitHub. PivoCloud has no part in it and cannot hurry it. **A long list is a complete list.** If your installation covers hundreds of repositories, you see hundreds of them. There is no cut-off, no first-page-only behaviour, and nothing you need to click to reach the rest. If a repository you authorized is not there, the selection on GitHub is what to look at, not the length of the list. **An empty list means the installation is authorized for nothing.** The picker says `No repositories found. Check your GitHub App permissions.` Read that as a question about the repository selection rather than about permissions in general: open the installation on GitHub and look at which repositories you granted it. An App installed with "only select repositories" and an empty selection produces exactly this message. ### Changing which repositories are visible The list is read from GitHub every time the page loads. PivoCloud keeps no copy of it, so there is nothing stale to clear. Three steps, and the third is the one people miss: 1. Open `Manage on GitHub` on the `Integrations` page. It takes you straight to this installation's settings. 2. Change the repository selection there, on GitHub. 3. Come back to PivoCloud and load the page again. The console has no control for re-reading the list, and it does not need one: loading the page **is** the re-read. If you changed the selection on GitHub and the old set is still on screen, you are looking at a page that was loaded before you made the change. ### When the page shows a button and no error If your account has no installation, or the installation was removed or suspended on the GitHub side, the console shows the `Connect GitHub` button and says nothing at all. No error, no warning, no explanation. That silence is deliberate rather than broken. A brand-new account and a lapsed installation look identical from here, and the first of those is not a failure worth alarming anyone about. The practical reading is simple: if you expected to be connected and you are looking at that button, you are not connected. Install the App again from it. ### Disconnecting `Disconnect` sits on the `GitHub App` card. The confirmation asks `Disconnect GitHub App?` and tells you what it costs: `Your deployed apps will keep running. Auto-deploy will stop until you reconnect.` So disconnecting is not a way to take an app down. Anything already deployed keeps serving traffic; what stops is PivoCloud's ability to read your code, which means no automatic deploy on push and no new build until you connect again. ### Where you actually pick the repository Not on this page. The picker lives on the create-app form: a control labelled `Pick a repository`, with a `Search repositories…` field inside it once it opens. Everything on this page decides what that picker contains. The same picker appears on an app's settings page too, reached from the `Edit` button on the app page. There it shows the repository that app deploys from. If the installation can no longer read that repository, the picker still shows its name, with this line underneath: `This repository is not in the list your GitHub connection can read. Give the PivoCloud GitHub App access to it on GitHub, then reload this page.` See [changing which repositories are visible](#changing-which-repositories-are-visible) above to fix that. For the rest of that form, the branch, the build settings, the plan and the subdomain, see [deploying your first app](/apps/deploy). # How do I put my own domain on my app? Source: https://docs.pivocloud.com/apps/custom-domains Add your own domain to a PivoCloud app: the order that works, the CNAME case for a subdomain, the A record case for the root of your domain, what each status means, and what to do when the check says your DNS is not pointing here yet. ## Your own domain on an app Every app you deploy already answers on a PivoCloud address. A custom domain puts your own name in front of it, so visitors reach the same app at a name you own. Both addresses keep working; the custom one is added, not swapped in. There are two cases and they need two different DNS records. A subdomain such as `shop.example.com` uses a `CNAME` record. The root of your domain, such as `example.com`, cannot carry a `CNAME` at most DNS providers, so it uses an `A` record instead. Each case is written out below. ### Before you start Your app has to be deployed and running. A custom domain points visitors at a live app, so there has to be one for it to point at. If your app has never deployed, or you stopped it, deploy or start it first and come back. Your plan has to allow custom domains. They are available on Starter and above. You do not have to work out whether yours qualifies: the `Domains` tab on your app tells you. The console shows a decision the server made, so what you see there is what the server will enforce when you press the button. ### The order, and why it is this way round Add the domain in PivoCloud first, then create the DNS record. In three steps: 1. Open your app in the console, go to the `Domains` tab, type your domain into the field and press `Add domain`. 2. Read the target the tab now shows for that domain. There is a copy control beside it. 3. Go to your DNS provider and create the record against that target. Doing it the other way round is the most common way this goes wrong, and it fails quietly: a record created before the domain exists in the tab points at a target you had to guess, and a guessed target is wrong in a way nothing reports back to you. ### Case one: a subdomain, using CNAME This is the normal case, for a name such as `shop.example.com`. The record type is `CNAME`. The value to copy is the one the `Domains` tab shows next to `CNAME` for that domain, with a copy control beside it. Copy it from there rather than from anywhere else: it is built from your app's own identity, so it is different for every app. At your DNS provider, create a `CNAME` record whose name is the subdomain and whose value is what you copied. ### Case two: the root of your domain, using an A record This is the case where a name such as `example.com` cannot carry a `CNAME`, because most DNS providers do not allow one at the root of a domain. The record type is `A`. The value to copy is the one the `Domains` tab shows next to the apex row for that domain, again with a copy control beside it. That row appears only where PivoCloud has an address to give for the environment your app runs in. If you do not see it, the root case is not available to you and the `CNAME` case is the one to use: point a subdomain at your app and, if you want the root to reach it too, use whatever redirect your DNS provider offers at the root. ### What happens next, and what you will see Once the record exists, PivoCloud checks it and moves the domain through four states. The tab shows the current one as a badge: * `Pending DNS` means the domain is registered with us and the check has not passed yet. This is where every domain starts. * `Verifying…` means the check passed and the route is being put in place. * `Active` means the domain is live and serving your app over HTTPS. * `Error` means a check failed. The reason is shown under the domain. You do not have to do anything to move it along. A waiting domain is re-checked by itself every 45 seconds, and once a check passes the route is picked up within a few seconds. The certificate is obtained automatically and is trusted by browsers with no step on your side. There is also a `Verify now` control if you would rather not wait, and it behaves differently from the automatic re-check in one way worth knowing. Pressing `Verify now` before your record has spread parks the domain in the error state with the reason shown. A domain left alone is simply checked again on the next cycle until it passes. Neither one damages anything, but the manual one is why a domain sometimes shows an error a minute after you added it. From the moment a check passes, the domain answers over HTTPS in roughly seven to ten seconds. That is a range because it is what we measured over several runs, and one run would not tell you what to expect. Before that comes the part we cannot time for you: your own DNS change spreading, which is usually the longer wait of the two. ### If it says the DNS does not point here The check reports this: ```text theme={null} DNS does not point to PivoCloud yet ``` The rest of that message tells you to set the record and try again. Three things to look at, in this order: * The record type matches the case you chose. A `CNAME` where the root needs an `A` record, or the reverse, fails the check even though the value is right. * The value matches what the `Domains` tab shows for that domain today. Copy it again rather than trusting a value you saved earlier. * Enough time has passed. A DNS change is not instant: it can take as long as the TTL your provider sets on the record, which is often several minutes and is sometimes an hour or more. If all three are right, leave it alone. The automatic re-check keeps running. ### The cap You can map up to 3 custom domains to one app. ### Taking a domain down Remove the domain from the `Domains` tab. The mapping goes away and that hostname stops answering for your app. Your app itself is untouched and keeps serving on its PivoCloud address and on any other custom domain you have left in place. Deleting the DNS record at your provider is worth doing too, so the name stops pointing somewhere it is no longer served. # How do I deploy my first app? Source: https://docs.pivocloud.com/apps/deploy Fill every field on the PivoCloud create-app form without guessing: name, subdomain, plan, repository, branch and build settings. Plus the port rule the platform really enforces, and the two messages a failed first deploy prints. ## Deploy your first app ### What the platform enforces PivoCloud builds the Dockerfile in your repository, reads the first `EXPOSE` line in it, and probes your app on that port. There is no build detection and no framework guessing: the runtime is whatever your base image pins. Three facts follow from that, and between them they account for almost every first deploy that fails. **No `PORT` variable is set for you.** PivoCloud does not inject one. Your process has to be listening on the port its own `EXPOSE` line declares. A Dockerfile with no `EXPOSE` line is not refused: PivoCloud probes port `8000` instead, and unless your app is listening there the deploy fails with the second message at the bottom of this page. Reading `PORT` and falling back to that number is the portable way to write it, because other hosts do set the variable: ```js theme={null} // Dockerfile says: EXPOSE 8080 const port = process.env.PORT || 8080; app.listen(port, "0.0.0.0"); ``` **Bind `0.0.0.0`, not `localhost`.** A server bound to `127.0.0.1` inside a container is reachable by nothing outside it. **An app serves exactly one HTTP port.** A backend plus a frontend is either one image serving both, or two apps built from one repository, each with its own build settings. The other three rules, the Dockerfile itself, your migrations and the ephemeral filesystem, are on [what your repository needs](/apps/deployment-contract) with worked examples. Read that page once before you create anything. ### The list the form shows you The create form displays a short checklist headed `Platform Requirements` above the fields. Two of its three items are out of date, and where it and this page say different things, this page is correct. No environment variable carries the port for you, and the port your app must listen on is the one the first `EXPOSE` line in your Dockerfile declares. Your Dockerfile does not have to sit at the repository root either. The `Root directory` and `Dockerfile path` fields, both documented further down this page, build from wherever it actually lives. ### Create the app, field by field On `My Apps` with nothing deployed yet, the page shows an empty state and one button, `Create New App`. It opens the form. The fields below are in the order the form renders them. **`App Name`.** The name you and your team see in the console. The helper reads `Use lowercase letters, numbers, hyphens, and underscores only` and the field suggests `my-awesome-app`. Pick something you would recognise in a list a year from now. **`Subdomain`.** The hostname your app answers on. It has its own section below, because it is the one field with a decision in it. **`Plan`.** How much machine your app gets, and what it costs per month. `Lite` is the entry plan at 1,200 DA per app per month, which is exactly what the starting credit covers for a first month. The list shows each plan next to its monthly price, and once you pick a paid plan the form tells you what creating the app will charge and what your balance becomes. **The repository.** If GitHub is not connected yet, the form shows a `Connect GitHub` button and the line `You'll be taken to GitHub to authorize the PivoCloud App. You'll return here afterward.` Once connected, the picker is labelled `Pick a repository`, with a `Search repositories…` field inside it. Which repositories appear there is decided entirely by your App installation: see [connecting GitHub](/apps/connect-github). **`Auto-deploy on push`.** A toggle, offered on the GitHub App path. Its helper reads `Deploys automatically when a push targets the deploy branch.` Leave it on unless you want every deploy to be a deliberate act. **`Advanced`.** A collapsed section, covered below. It folds itself away as soon as the form can read your repository through the App, so on the recommended path you will not see it at all. **`Environment variables (optional)`.** Its own collapsed section, offered only when you are creating an app. Anything your app needs at runtime, API keys, database URLs, feature switches, can go in here now or be set afterwards. See [environment variables](/apps/environment-variables). **`Deploy branch`.** Which branch is built. It suggests `main`, and its helper reads `Branch to deploy. Defaults to your repository's default branch.` **`Root directory`.** The helper: `The folder Docker builds from. Leave it blank to build from the repository root.` **`Dockerfile path`.** The helper: `The path to the Dockerfile, relative to the root directory above. Leave it blank to use the default filename.` Submit with `Create App`. ### When your Dockerfile is not at the repository root Two fields cover this, and getting the second one wrong is the most common mistake on the whole form. * `Root directory` is the build context: the only part of your repository Docker can see. Nothing above it exists as far as the build is concerned. Leave it blank and the context is the repository root. * `Dockerfile path` is the file to build, **resolved relative to the root directory above**, not to the repository root. Leave it blank and the default filename is used. So an API living in `backend/` with a `Dockerfile` beside it wants `Root directory` set to `backend` and `Dockerfile path` set to `Dockerfile`. Not `backend/Dockerfile`, which would resolve to `backend/backend/Dockerfile`. The form prints the resolved pair back to you as you type, in this shape: ``` Builds backend/Dockerfile with build context backend. ``` Read that line before you submit. It is the cheapest way to catch the doubled prefix. Two apps can be built from one repository this way, each with its own root directory. The [monorepo example](https://github.com/PivoCloud/example-monorepo-two-apps) is that shape end to end. ### Choosing the subdomain The field starts filled in for you, derived from the app name as you type it. The moment you edit it yourself, that link is cut permanently: the field stops following the name, even if you clear what you typed. The rules are: between 3 and 63 characters, lowercase letters, numbers and hyphens. It cannot start or end with a hyphen, and it cannot start with `app-`, which is reserved for the addresses PivoCloud generates. A further set of names is reserved as well, and when one of them applies the verdict beside the field names the rule you broke. The console appends your account's app domain after it, and shows you the full address you are about to get. The two rules a first name most often trips print their own message: `Can't start or end with a hyphen.` and `Subdomains can't start with "app-". That prefix is used for automatic URLs.` Leaving it blank is a perfectly good choice. Do that and PivoCloud builds a hostname from the app's own id, and shows it to you before you submit in the shape `app-4f3c1a2b…`. You can pick a real name later. As you type, a small verdict appears beside the field. It reads `Checking…` while the console asks, then one of `Available`, `Taken`, `Reserved`, `Invalid` or `Check unavailable`. Treat that verdict as a hint rather than a reservation. It tells you what was true a second ago, not what will be true when you submit: a name can show `Available` and still be refused if someone else creates it first. Nothing holds a subdomain for you until the app exists. Until the first deploy you can still change it. The console says so where the address is shown: `This URL starts working the first time you deploy. You can change it until then without using up a certificate.` ### Deploying without the GitHub App The `Advanced` section is the path for a repository the App connection cannot reach: a repository you do not want to grant the App, or a one-off you would rather not install anything for. It holds three controls: a repository URL, a toggle marked `Private repository`, and, once that toggle is on, a field for a GitHub token with read access to the repository contents. The section hides itself as soon as the form successfully reads your repository through the App, which is why most people never open it. Prefer the App connection where you can: it is what makes deploying on every push possible, and it means no token of yours has to live here. When the repository was picked through the App, the form does not ask for a token at all. Under `Private repository` it shows this line instead: `Access to this repository goes through your GitHub connection. No token is needed.` An app that still holds a token saved from before it moved onto the App connection shows one more line next to a `Remove token` button: `A token saved earlier is still stored but is not used for this app.` ### Migrations Nothing runs your migrations. There is no release phase and no automatic migration step, so a schema change is yours to trigger explicitly. The entrypoint pattern that makes it opt-in, so a container restart cannot surprise you, is on [what your repository needs](/apps/deployment-contract). ### Watch the build, then reach your app Creating the app takes you to its page, which is organised as five tabs: `Overview`, `Deployments`, `Environment`, `Domains` and `Billing`. * `Deployments` carries the build log. Watch it here on the first deploy: this is where a failing build tells you why. * `Domains` carries the address your app answers on, with its certificate state, and is where you change the subdomain before the first deploy. * `Environment` is where you add or change environment variables. Before the first deploy, saving only stores them: there is no container yet to replace. Saving a change afterwards replaces the running container rather than rebuilding the image, so it is fast. See [environment variables](/apps/environment-variables). * `Overview` carries the app's status and which repository it came from. * `Billing` carries what this app costs and what it has cost. The first deploy starts when you press `Deploy` on the app page. It does not begin on its own, so add your environment variables first if your app needs them at startup. After that, the button on the app page reads `Redeploy` and rebuilds from your deploy branch on demand. ### If the first deploy fails Two messages account for most first failures, and both are worth reading literally. The first says the build found nothing to build at the path your two settings resolved to, and then lists the Dockerfiles it did find in your repository. One of the two settings is off. Compare the path in the message against the list underneath it, and check `Root directory` first. The second message reports that your container started but nothing answered on the port PivoCloud probed. Check your `EXPOSE` line against the port in your own startup log, then read the container logs. If your Dockerfile has no `EXPOSE` line at all, PivoCloud probed port `8000`, so there is nothing for you to compare and the fix is to declare the line. Both messages, and every other one PivoCloud prints when a deploy does not work, are on [why did my deploy fail](/apps/troubleshooting) with what each one really means. The contract your repository has to satisfy, including the whole `EXPOSE` rule, is on [what your repository needs](/apps/deployment-contract). # What your repository needs Source: https://docs.pivocloud.com/apps/deployment-contract The six rules a repository must meet for PivoCloud to build and run it: a Dockerfile, the EXPOSE port contract, binding 0.0.0.0, one HTTP port per app, explicit migrations, and an ephemeral filesystem. ## The deployment contract Six rules. Meet them and your app deploys. Most deploy failures are one of the first two. ### 1. A Dockerfile PivoCloud builds from your Dockerfile. There is no build auto-detection, no buildpack, and no framework guessing. This is deliberate, and it is the answer to "which Node/Python/Go versions do you support": **whichever your base image pins.** The runtime is yours to choose, not ours to bless, so nothing breaks under you when we upgrade. By default it builds a file named `Dockerfile` at the root of the repository. Two optional per-app settings move that, on the creation form and on the app's settings page afterwards: * **Root directory** is the build context, the directory Docker can see. Default: the repository root. Nothing above it exists as far as the build is concerned, so `COPY ../shared` cannot work. * **Dockerfile path** is the file to build, **relative to the root directory**, not to the repository root. Default: `Dockerfile`. Leave both blank and nothing changes. Set them and a repository with no Dockerfile at its root deploys fine, and two apps can build two different Dockerfiles out of one repository. See [example-monorepo-two-apps](https://github.com/PivoCloud/example-monorepo-two-apps). When the resolved path is wrong, the deploy fails. The message names the path it resolved, the directory it built from, and the Dockerfiles it did find in your repository. Read that list first: it usually shows you exactly which of the two settings is off. [Why did my deploy fail](/apps/troubleshooting) quotes the wording, in both of the forms this failure takes. ### 2. Listen on the port you declare with `EXPOSE` **PivoCloud does not set a `PORT` environment variable.** It reads the first `EXPOSE` line in the Dockerfile it builds, and probes your app on that port, so that is the port your process has to be listening on. Write it so the same image is correct everywhere. Read `PORT` if something set it, because Render, Railway and Heroku all do, and fall back to the value you declared with `EXPOSE`: ```js theme={null} // Dockerfile says: EXPOSE 8080 const port = process.env.PORT || 8080; app.listen(port, "0.0.0.0"); ``` Do not hardcode a value that differs from `EXPOSE`, and do not set `ENV PORT` in the Dockerfile: a variable baked into the image is one more place for the two numbers to drift apart. Three consequences are worth knowing before your first deploy: * **A Dockerfile with no `EXPOSE` line is not refused.** The build proceeds and PivoCloud probes port `8000` instead. If your app is listening on anything else, the deploy fails with the message below, and there is no `EXPOSE` line for you to check against it. Declare one. * **It is the first `EXPOSE` in the file, not the one your final stage declares.** In a multi-stage Dockerfile, keep `EXPOSE` out of your builder stages, or put the runtime one first. * **Write a literal port number.** `EXPOSE ${PORT}` is not read at all, and falls back to the same port `8000` a missing line falls back to. A line naming two ports takes the first of them. When this is wrong, the deploy fails with a two-sentence message about the `PORT` environment variable. [Why did my deploy fail](/apps/troubleshooting) quotes it word for word. Read the first sentence as **"we could not reach your app on the port it declared"**, and ignore the second: no such variable is set, so there is no value for your app to listen on. The message is emitted for almost every boot failure, not only for port mistakes. If you see it, check `EXPOSE` against the port in your startup log first, then read the container logs. If your Dockerfile has no `EXPOSE` line at all, PivoCloud probed port `8000`, so there is nothing for you to compare and the fix is to declare the line. ### 3. Bind `0.0.0.0`, not `localhost` A server bound to `127.0.0.1` inside a container is reachable by nothing outside it. ### 4. One HTTP port per app An app exposes exactly one HTTP port. If your project is a backend plus a frontend, you have two choices: * **Serve the built frontend from the backend.** One repo, one Dockerfile, one container, one billed service. See [example-node-express-vite](https://github.com/PivoCloud/example-node-express-vite). * **Deploy them as two apps, out of one repository.** Two Dockerfiles, two billed services, one repo: give each app its own root directory and it builds its own Dockerfile. Sometimes the right call, especially mid-migration when you would rather not change application code at the same time as changing host. See [example-monorepo-two-apps](https://github.com/PivoCloud/example-monorepo-two-apps). A worker with no HTTP surface does not fit the app model. Neither do services that must scale independently. ### 5. Nothing runs your migrations No release phase, no automatic `migrate` step. If your schema needs migrating, do it explicitly, and make it opt-in so a container restart cannot surprise you: ```sh theme={null} if [ "$RUN_MIGRATIONS" = "true" ]; then npx prisma migrate deploy fi exec "$@" ``` [example-node-express-vite](https://github.com/PivoCloud/example-node-express-vite) and the API in [example-monorepo-two-apps](https://github.com/PivoCloud/example-monorepo-two-apps) ship this pattern in their entrypoint. ### 6. The filesystem is ephemeral Containers are replaced on every deploy, and on an environment-variable change. Anything written to local disk is gone. There is no persistent disk product. Uploads belong in object storage (Cloudinary, S3-compatible, anything with an API). Sessions and caches belong in your database or a managed store, not on disk. ## Working examples Three repositories you can deploy on PivoCloud as they are, each one showing a different shape of the contract above. * [example-node-express-vite](https://github.com/PivoCloud/example-node-express-vite): Express API serving a built Vite frontend. **One service, one bill.** * [example-vite-static-nginx](https://github.com/PivoCloud/example-vite-static-nginx): A built Vite frontend served by nginx, as its own service. * [example-monorepo-two-apps](https://github.com/PivoCloud/example-monorepo-two-apps): An API and a frontend in one repository, deployed as two apps. **Two Dockerfiles, no Dockerfile at the root.** # How do I set environment variables for my app? Source: https://docs.pivocloud.com/apps/environment-variables Paste a .env file into the PivoCloud console, see which five key names are refused and why, what the console keeps from your file and what it drops, what saving actually changes, and how your running app reads the values. ## Environment variables Everything your app reads at runtime goes here: API keys, database URLs, feature switches. They live on the app's `Environment` tab, in a panel headed `Environment variables`. You can also set them while you create the app, in the collapsed `Environment variables (optional)` section of the create form. Same variables, same rules, one less trip. Every example value on this page is made up. Do not paste a real secret into an app that prints its own environment to a web page. ### Two ways to edit The panel has two modes, `Editor` and `.env text`. `Editor` gives you one row per variable, which is what you want for changing a single value. `.env text` gives you the whole set as one file, which is what you want if you already have a `.env` and would rather paste it than retype it. ### Pasting a .env file Values are hidden until you press `Reveal values to edit`. Until you do, the text is read-only, because the panel will not put your secrets on screen unless you ask for it. Once they are showing, the console says so: `All values are visible. Avoid this while screen sharing.` The text area is labelled `Environment variables as .env text`, and its placeholder shows the shape it expects: `KEY=value, one variable per line`. Paste your file in. The console reads it with its own rules, and it will tell you what they are: press `How this is read` beside the text area and a panel headed `How PivoCloud reads this file` opens. Four of those rules are worth knowing before you paste. **A comment line is read and then dropped.** A line beginning with `#` is a comment and is never stored as a variable. A `#` inside a value with no space in front of it is part of the value, not the start of a comment: `DB_PASS=pa#ss22` stores `pa#ss22`, where some other tools would cut the value at the `#`. **A dollar-brace reference is kept as text.** `${VAR}` and `$(cmd)` are stored literally and never expanded. Your container receives the characters exactly as you typed them, so a password that happens to look like a shell expression arrives intact. **A multi-line block survives the paste.** A `-----BEGIN` block is read as one multi-line value, up to its matching `-----END` line, so a PEM-formatted key can go in as it stands rather than being folded onto one line. **What comes back is the same variables, not the same file.** Comments, blank lines and quote style are not stored. Save, reopen, and you see every variable you pasted, formatted by PivoCloud rather than in your original layout. The values round-trip; the layout does not. ### Merging, or replacing everything Under the text area sits a switch labelled `Replace all variables`. It decides what happens to variables that are set on the app but absent from the text you pasted. Leave it off and the console says `Merging. Variables not in your text are kept.` Your text adds and updates, and nothing is deleted. This is the safe default and the one you want when you are pasting a partial file. Turn it on and it says `Replacing. Variables not in your text will be deleted. You will confirm the list before anything is written.` The save button changes to `Review and replace…`. Nothing is deleted before you have seen the list. Pressing `Review and replace…` opens a dialog titled `Replace all variables?` that names every variable that would be permanently deleted and asks you to type a word to confirm, prompting `Type REPLACE to confirm`. Values are encrypted and kept without history, so a deletion cannot be undone afterwards. ### The five names the platform keeps Five key names are refused. Submit one and the console shows your key followed by the sentence `is reserved by the platform.` The match is exact. Only those five exact names are refused, so a key that begins with one, contains one, or ends with one is an ordinary key and goes in like any other: * `PORT` is refused, while `PORTAL` and `PORT_NUMBER` are accepted. * `PATH` is refused, while `MY_PATH` and `PATHFINDER` are accepted. * `HOME` is refused, while `HOMEDIR` and `HOME_PAGE_URL` are accepted. * `HOSTNAME` is refused, while `HOSTNAME_PREFIX` and `DB_HOSTNAME` are accepted. * `USER` is refused, while `USER_ID` and `DB_USERNAME` are accepted. Four of them are set by the container's own operating system, and shadowing one breaks your app in a way that is hard to read from the outside: an entrypoint that cannot find its binaries, or a home directory that does not exist. `PORT` is the fifth, and it is the one that surprises people. **The name is reserved, and PivoCloud does not set a value for it.** Your app has to listen on the port your image declares with `EXPOSE`. That contract, and the one-line pattern that keeps the same image portable to other hosts, is on [what your repository needs](/apps/deployment-contract). ### A lowercase key is refused for a different reason Keys are uppercase letters, digits and underscores, and may not start with a digit. A lowercase key is invalid, and the console shows `Use uppercase letters, digits, and underscores for the key.` A lowercase spelling of one of the five reserved names breaks both rules at once, and the console shows both messages for that single row rather than picking one. A row keyed `path` gets the invalid-key message and `path is reserved by the platform.` together. Both belong to that one row, and their order on screen means nothing. Uppercasing `path` to `PATH` clears the invalid-key message and leaves the reserved one, because the name itself is what is refused and no spelling of it is accepted. Pick a different name, such as `MY_PATH`. A key that is only lowercase, such as `database_url`, gets the invalid-key message on its own, and uppercasing it to `DATABASE_URL` clears it and the row goes in. ### The size limit on a value Each value is capped at 64 KiB. Above that the console refuses the row with `This value exceeds the 64 KiB limit.` A certificate or a private key fits comfortably. A data file you meant to ship with the app does not, and belongs in object storage instead. ### What saving actually does **Before your app's first deploy there is no container to replace, so saving only stores the variables.** The console shows `Saved. They will be used at your first deploy.` and the first deploy starts with them already in place. `Apply now` after attaching or detaching a database behaves the same way before the first deploy. Once your app has deployed, saving replaces the running container. It does not build a new image, and knowing that changes what you wait for. PivoCloud starts a second container from the existing image, hands it your new variables, waits for it to answer a health check, moves traffic across, and only then stops the old one. If the new container fails its check, the old one keeps serving and your app never goes down. The swap takes seconds rather than the minutes an image build takes, so if you were watching for a build log, there will not be one. **A save that changes nothing does nothing.** If your text matches what is already stored, the button reads `No changes to save` and no container is touched. Reopening the panel and saving again is safe. Two messages can come back instead of a save: * `A deployment is in progress. Try again in a moment.` Another change to this app is still being applied. Wait a few seconds and save again. * `Environment variables changed. Refresh and re-apply your changes.` Someone else, or another tab of your own, saved while you were editing, so what is on your screen is out of date. Nothing of yours was written. Reload the page, look at the current set, and re-apply your change on top of it. ### How your running app reads them They arrive as an ordinary process environment. There is no client to install, no SDK, and no file to read. ```js theme={null} // Both of these are set on the Environment tab. // The fallback is what you get when you run the same image locally. const databaseUrl = process.env.DATABASE_URL || "postgres://localhost:5432/app_dev"; const signups = process.env.FEATURE_SIGNUPS || "off"; ``` The same shape works in any language, because this is the language's own environment lookup and nothing of ours. Read the variable, fall back to something harmless, and the one image runs on your laptop and on PivoCloud without a branch. # What does each button on my app page do? Source: https://docs.pivocloud.com/apps/manage Every control on a deployed app in the PivoCloud console, in the order the console shows them, with what each one does and what each one does to your bill before you press it. Plus the two separate places your logs appear. ## Manage your app Your app's page in the console carries everything you do to an app after it is created. Seven controls, and two separate places logs appear. Some of them change what you are charged and some of them do not. Two of the confirmations tell you so, the rest say nothing about money at all, and you only read the two after you have already pressed the control. So each control below gets two answers in the same order: what it does, then what it does to your bill. This page covers all seven controls and both log surfaces. Controls appear here in the order the console renders them: the buttons in the app header first, then the ones inside the `More actions` menu at the end of that row. Two buttons in that row do not act on the running app and are not covered here. `Edit` opens the app's settings. `Top up to resume` appears only when an app has been suspended for lack of credit, and it takes you to your wallet. ### Deploy and Redeploy The first button in the header. On an app that has never deployed it reads `Deploy`. Once a deployment exists it reads `Redeploy`, and while a build is running it reads `Deploying…` and cannot be pressed. Pressing it builds your repository again from your deploy branch and replaces the running container with the result. Your app keeps serving the old container until the new one is ready. **What it does to your bill:** nothing, on an app that has already been charged for the current period. No new charge, no refund, and the paid period runs on exactly as before. You can redeploy as often as you like. A first deploy is the exception, and it is the only one. An app with a monthly price is charged when it first deploys, not when you create it. If your wallet cannot cover that first charge, the deploy is refused and no money moves. ### Stop While your app is running, a `Stop` button sits beside `Redeploy` in the same row. Pressing it opens a short confirmation headed `Stop this app?`, and the button that carries it out reads `Stop app`. Your app goes offline. Its address stops answering, its container is stopped, and nothing about your repository, your settings or your environment variables changes. It is a pause, not a deletion. **What it does to your bill:** stopping stops the charge. A stopped app leaves the billing run entirely, so nothing at all accrues while it is stopped. The confirmation says the same thing before you press it, and it is right. ### Start Once your app is stopped, `Start` takes the place of `Stop` in the same row. It brings the container back on the plan the app is currently on. **What it does to your bill, and this is the good news the console never tells you:** starting again moves the **end** of your paid period forward by exactly the time the app spent stopped. The start of the period does not move. A pause therefore costs you no paid time at all: whatever you had left when you stopped is what you have left when you start. An app that has never been charged has no paid period to move, so starting it changes nothing. ### Restart In the `More actions` menu. It bounces your app's container in place: same image, same build, no rebuild and no clone. Use it when your app is misbehaving rather than when your code has changed. For new code, use `Redeploy`. **What it does to your bill:** nothing. A running app's paid period keeps running straight through a restart. ### Change plan In the `More actions` menu. It opens a dialog listing the plans you can move to, with your current one marked, and your wallet balance beside them. **What it does to your bill:** nothing at the moment you change it. No charge, no refund, no proration. The new price applies from your next billing cycle, and the dialog says the same thing where you press the button. What happens straight away is the resource limits. A running app restarts briefly to take up the new ones, so expect a few seconds of downtime. A stopped app records the new plan and takes up its limits the next time you start it. ### Delete Last in the `More actions` menu, on its own below a divider. It opens a confirmation naming your app and warning that the action cannot be undone. Your container, your image, your deployment logs and the app's own record all go. **What it does to your bill, and the confirmation now says so:** billing ends immediately, and the paid remainder of the current period is **not** refunded. Both halves are true at once. You stop being charged from the moment you delete, and the days you have already paid for are not credited back to your wallet. A new app starts with no paid period of its own. So if you delete an app and create a new one afterwards, even with the same name and the same repository, its first deploy charges the full plan price again. That is a second payment for a plan you already paid for once. If an app's deploy failed, fix the cause and press `Deploy` again from the app page instead of deleting and re-creating it. If what you want is to stop paying for an app you may come back to, `Stop` is the control for that, not `Delete`. A stopped app is not charged, and the paid time you have left waits for you. ### Where your logs are There is no `Logs` control. Open the `Deployments` tab and you get two sub-tabs, `Deployment logs` and `Runtime logs`. They are different places and neither one contains the other. * `Deployment logs` is the build. It is everything that happened while PivoCloud cloned your repository and built your image, and the panel beneath the sub-tab is headed `Deployment Logs`. If a deploy never produced a running container, the reason is here. * `Runtime logs` is your app talking, live, once it is running. It is what your process writes while it serves traffic. If your app started and then crashed, the reason is here. Looking for one in the other is the most common way to conclude there is nothing to see. A crash on startup is in `Runtime logs`, not in the build output. A build that failed leaves `Runtime logs` empty, because nothing ever ran. Below both, a `Deployment History` section lists your recent deployments so you can see which attempt is which. **What it does to your bill:** nothing. Reading either log costs nothing and changes nothing about your app. ### If Redeploy seems to do nothing Pressing `Redeploy` while a deploy is already running produces no feedback at all. No message, no error, nothing moves on screen. The refusal is real, but the console does not show it to you, so the natural reaction is to press again. Open the `Deployments` tab instead and watch the deploy that is already running. When it finishes, `Redeploy` works normally again. ### If a delete is refused An app cannot be deleted while a deploy is running. The failure you get back says only that the delete did not work, with nothing to suggest that waiting would fix it, and it is the whole of the explanation you are given. It is a timing refusal and nothing more. Open the `Deployments` tab, wait for the running deploy to finish, then delete. `Stop`, `Restart` and `Change plan` are refused the same way while another change to the app is still in flight, and there the message does tell you to try again in a moment. Wait a few seconds and press the control again. ### An app you have not deployed yet A brand new app looks different from the one described above, and none of it is a fault. The first button reads `Deploy` rather than `Redeploy`, because there is nothing to redeploy yet. There are no logs: `Deployment logs` has no build to show and `Runtime logs` has no container to stream from. `Deployment History` is empty. `Stop` is absent, because an app that is not running cannot be stopped. All of that resolves itself the moment your first deploy runs. ### When a message needs explaining Every failure message PivoCloud can show you, what each one really means, and what to change, is on [why did my deploy fail](/apps/troubleshooting). Search that page for the exact sentence you were shown. # Why did my deploy fail? Source: https://docs.pivocloud.com/apps/troubleshooting The messages PivoCloud shows when a deploy does not work, what each one really means, and what to change. Search this page for the exact sentence you were shown and it will take you to that failure. ## Find the sentence you were shown, then read what it really means A failed deploy puts two different sentences in front of you about the same failure, and they are not the same words. A red panel at the top of the app page carries a short explanation written for a human. The build log underneath it carries the sentence PivoCloud wrote at the moment the deploy stopped, which is longer and names your own paths. Both are quoted here for every failure. Whichever one you copied, searching this page for it takes you to the right entry. **If the red panel and the log disagree, believe the log.** The explanation in the red panel is worked out from the text of the log, and it can land on the wrong one. The log is a record of what happened. The panel is an interpretation of it. ## What is on this page, and what is not This page is the index of the messages that **stop a deploy**: the ones that leave your app undeployed and put a red panel on the app page, plus the ones that refuse a deploy before it starts. It is an index of those. It is not an index of everything PivoCloud can tell you. Two families of message are deliberately not here. **The build agent's own rejections of a repository URL.** PivoCloud checks your repository URL when you save the app and again when you press `Redeploy`, so a URL it will not accept is refused at that moment and never reaches a build. What you read is the wording of that check, and that wording is quoted on this page. There is no second set of sentences from the build to look for. **Failures that leave your app running but not reachable at its public address.** These do not fail the deploy at all. Your app is up; only its address is not configured yet. They appear in the app's public URL panel on the `Overview` tab, not in the deployment logs, and this page does not cover them. ## When PivoCloud cannot tell you why The red panel reads: ```text theme={null} Deployment failed. Please check the logs below for details. ``` This is what you get when PivoCloud did not recognise the failure from the log text. It has nothing specific to say, so it says nothing specific. **The sentence that does say something is hidden for this one.** Under the red panel there is a `Technical details` toggle. Click it to open it, and inside is the sentence PivoCloud wrote when the deploy stopped. That is the one to read, and the one to paste into a search or into a chat with your assistant. **And one case where there is nothing to read at all.** If the failed deploy recorded no message, no red panel appears. The app shows as failed with no explanation beside it. Open the `Deployments` tab, pick the attempt that failed, and read its build log directly. ## Before the build starts: Redeploy refused because of the repository When you press `Redeploy`, PivoCloud checks your repository before it builds anything. If that check fails, nothing is built and nothing is charged. The answer carries `Repository validation failed` and one of the eight sentences below, and the sentence is the part worth reading. ```text theme={null} The repository URL format is invalid. Please use a valid GitHub HTTPS URL (e.g., https://github.com/owner/repo). ``` The URL is not in a shape PivoCloud can use. Copy it out of your browser's address bar on the repository page rather than typing it. ```text theme={null} SSH-style Git URLs are not supported. Please use an HTTPS URL (e.g., https://github.com/owner/repo). ``` You gave the `git@github.com:owner/repo.git` form. Use the `https://` form of the same repository. ```text theme={null} Only GitHub repositories are supported. Please provide a GitHub HTTPS URL. ``` The URL points somewhere that is not GitHub. GitHub is the only host PivoCloud builds from today. ```text theme={null} This repository is private or does not exist. If it's private, connect the PivoCloud GitHub App or add a Fine-Grained PAT with Contents: read-only access. Otherwise, verify the URL is correct. ``` PivoCloud asked GitHub for the repository and GitHub did not show it. From the outside, a private repository and a repository that was never there look the same, which is why one sentence covers both. ```text theme={null} This repository is marked private but has no credentials attached. Connect the PivoCloud GitHub App or add a Fine-Grained PAT with Contents: read-only access. ``` You told PivoCloud the repository is private and then gave it no way to read it. This one is exact: attach credentials. ```text theme={null} The repository could not be found. Please verify the URL and ensure the repository exists. ``` The owner or the repository name does not resolve. Check the spelling of both, and check the repository has not been renamed or deleted. Two more sentences arrive in the same place. They are quoted here only up to the point where they change subject, because the platform writes them with a punctuation mark this documentation does not use. The rest of each one is given below in plain words. ```text theme={null} Token rejected ``` The token was read and GitHub refused it. Check it has not expired and that it grants `Contents: read-only` on **this** repository, not on a different one. ```text theme={null} Could not reach GitHub ``` PivoCloud could not reach GitHub at all. Nothing is wrong with your repository or your token. Wait a minute and press `Redeploy` again. ## The clone ### PivoCloud could not clone your repository The red panel reads: ```text theme={null} Failed to clone the repository. Please verify the URL is correct and the repository is public. ``` **Read the second half of that sentence loosely.** Your repository does not have to be public. PivoCloud deploys private repositories, and `The repository is private, or PivoCloud cannot read it` says how. Three different build-log sentences produce this same red panel: ```text theme={null} git clone failed: The repository could not be found. Please verify the URL and ensure the repository exists. ``` ```text theme={null} git clone failed: Network error during clone. Please check your connection and try again. ``` ```text theme={null} git clone failed: Git clone failed: exit status 128 ``` The first means GitHub did not show the repository: check the owner and the repository name, and check whether it is private. The third is the catch-all, carrying the raw exit code, and its cause is in the lines above it in the build log. **A badly formed URL lands in the second one.** If your repository URL is not merely wrong but malformed, the clone fails in a way that is reported as a network problem. So if you read the network sentence and your connection is fine, read the repository URL character by character before you look at anything else. ### The repository is private, or PivoCloud cannot read it The red panel reads: ```text theme={null} This repository appears to be private or inaccessible. Provide a Fine-Grained Personal Access Token (Contents: read-only) to deploy private repositories. ``` The build log for the same failure reads: ```text theme={null} git clone failed: Authentication failed. Check that the repository token is valid and has Contents: read-only access. ``` **What it really means:** PivoCloud reached GitHub, offered whatever credential it has for this app, and GitHub said no. Either there is no credential, or the one attached does not open this repository. **What to do, and what not to do.** Do not make your repository public. PivoCloud deploys private repositories and there are two supported ways to let it read yours. * **Connect the PivoCloud GitHub App** and pick the repository from the list. This is the shorter path and there is nothing to renew. * **Or attach a fine-grained personal access token.** On the app form, open the `Advanced` section, turn on `Private repository`, and paste the token into the `Fine-Grained Personal Access Token` field. The token needs exactly one permission on exactly one repository: `Contents: read-only`. Give it nothing else. A token is a password. Paste it into that field and nowhere else, and never commit one to your repository. ### The repository URL does not look valid The red panel reads: ```text theme={null} The repository URL does not appear to be a valid public GitHub HTTPS URL. ``` The word "public" in that sentence is not a requirement. What PivoCloud could not do is read the URL as a GitHub HTTPS address. The build log sentence that pairs with this panel is: ```text theme={null} git clone failed: The repository URL format is invalid. Please use a valid GitHub HTTPS URL. ``` Without the clone prefix, the same sentence reads: ```text theme={null} The repository URL format is invalid. Please use a valid GitHub HTTPS URL. ``` **In practice you are unlikely to be shown either of them for a malformed URL.** A URL that is badly formed rather than merely wrong is reported as a network problem instead, so the sentence you actually meet is the network one, under `PivoCloud could not clone your repository`. Both are quoted here so that a paste of either lands on this entry. The sentences shown when the same URL is refused before a deploy starts are more specific, and they are the ones to act on. They are quoted under `Before the build starts: Redeploy refused because of the repository`. ## The build ### PivoCloud could not find a Dockerfile to build The red panel reads: ```text theme={null} We couldn't find a Dockerfile in the repository root. Please ensure your repo contains a Dockerfile. ``` The build log says the same thing with your own paths in it, and this is the version worth reading. It begins with `No Dockerfile at ` followed by the file it looked for, then `(build context:` followed by the directory it looked inside, so a whole instance reads like *"No Dockerfile at `` (build context: ``)."* There is a second form of this failure. If the path you gave points at something that is not a file, a directory of that name for example, the log says *"Dockerfile path `` is not a regular file (build context: ``)."* instead. Same cause, different mistake: the first means nothing is there, the second means something is there and it cannot be built. Read the red panel's wording loosely. It says "repository root", but PivoCloud does not require a Dockerfile at your repository root at all. What failed is the path it actually resolved, which is the one printed in the log. **Two settings decide that path, and one of them is wrong.** `Root directory` is the directory the build can see, and it defaults to your repository root. `Dockerfile path` is the file to build, relative to `Root directory`, and it defaults to `Dockerfile`. Both live on the app page. Compare the path printed in the log against what you set. A Dockerfile at `api/Dockerfile` with `Root directory` left blank needs `Dockerfile path` set to `api/Dockerfile`. The same file with `Root directory` set to `api` needs `Dockerfile path` set to `Dockerfile`. How the two combine, with a worked example, is on [what your repository needs](/apps/deployment-contract). **The log then hands you the answer.** After that sentence it appends `Dockerfiles found in this repository:` and lists the ones it did find, each in quotes. Compare that list against the path it says it was looking for and the mistake is usually obvious. That list is taken from your **whole repository**, not only from inside the `Root directory` you chose. That is deliberate, and it is the single most useful thing in the message: the commonest cause of this failure is a Dockerfile that exists and simply sits outside the directory you pointed the build at. If your Dockerfile is in that list but the build still could not find it, the two settings are the thing to change, not your repository. The list stops at ten entries and does not tell you that it stopped. Add up what the message does show you: the paths it printed, plus the number in its own `and N more` note if it carries one. If that total reaches ten, read the list as a sample and search your repository for the rest. That `and N more` note counts only the entries the message dropped to stay inside its own size limit. It never counts the ones the search stopped looking for at ten. A list below ten did not hit that limit. It is still not a promise that your repository holds nothing else. The search behind it is bounded. It looks only a few directory levels down from the repository root, it never descends into the directories a build generates or vendors into, and on a very large repository it stops early. `node_modules` and `dist` are two examples of the directories it skips rather than the whole set, and the set is a platform detail that can change. If the Dockerfile you expected is not in the list, check whether it sits deeper than that or inside one of those generated directories. Then set `Dockerfile path` to it directly instead of waiting for the list to name it. One more place this sentence turns up. The failure recorded against the deployment itself is the same text with `docker build failed: ` in front of it, so if you copied it from there rather than from the log, everything above still applies. **What to do:** fix whichever of the two settings is wrong, then redeploy. The button on the app page reads `Redeploy`. Nothing else needs changing and you do not need to push a commit for the new settings to take effect. ### The build itself failed The red panel reads: ```text theme={null} The Docker build failed. Please check the logs below for details. ``` **This panel is deliberately empty of detail.** The build ran and something inside your own Dockerfile failed: a package that would not install, a compile error, a command that returned non-zero. PivoCloud has no opinion about what your build does, so it has nothing to add. Read the build log from the bottom upwards. The last command that ran before the failure is the one to look at. One case reaches this panel that is not an error in your Dockerfile at all. If `Dockerfile path` points at something that exists but is not a file, the log carries the `is not a regular file` wording rather than a build error. That is a settings mistake, and `PivoCloud could not find a Dockerfile to build` covers it. ### The build ran out of time The red panel reads: ```text theme={null} Your build ran out of time and was stopped. Redeploy to continue: the build resumes from the layers already cached, so each attempt gets further. ``` The build log carries the same fact with the limit in it: ```text theme={null} Build timed out after 30m0s, the current build limit on this platform. Redeploy to continue: the build restarts from the layers already cached, so it gets further each time. ``` **Press `Redeploy`, and keep pressing it until the build completes.** This is not a retry in the hopeful sense. The layers your build already finished are kept, so the next attempt does not repeat them: it starts where the previous one stopped and reaches a later stage. Repeat and it eventually gets all the way through. Note what this does **not** promise. A build that runs out of time runs for the whole limit every time, because the limit is what ends it, so two failed attempts take the same wall-clock time even though the second did more work. What changes between attempts is how far the build gets, not how long you wait. The attempt that finally completes is the short one. **You do not have to hit the limit to find out what it is.** The first lines of every build log carry a line beginning `Build limit for this deployment: `, followed by the limit for that deployment and the time it would be cancelled. So you can read your budget before you start waiting for it. If your build keeps running out of time, the thing to change is the Dockerfile, not the settings: order it so the slow, rarely-changing steps come first and are cached, and copy your source in as late as possible. ## Starting your app ### The container started but your app did not answer ```text theme={null} The container started but your app did not respond on the PORT environment variable. Please ensure your app listens on the port provided via the PORT environment variable. ``` **This is the one failure where the red panel and the build log say exactly the same thing, word for word.** There is one sentence here, not two. Do not go looking for a second, different one in the log. **Read the first sentence, and disregard the second.** The first is true: your container started and PivoCloud could not reach your app on the port it declared. The second is wrong. **PivoCloud sets no `PORT` variable**, so there is no value being provided for your app to listen on. The port PivoCloud probes is the one your Dockerfile declares with `EXPOSE`. That is a contract with a few sharp edges: which `EXPOSE` line is read, what happens when there is no `EXPOSE` line at all, and why `ENV PORT` in the Dockerfile makes things worse. All of it is on [what your repository needs](/apps/deployment-contract). This message is also emitted for almost any failure to start, not only for a port mistake. If your `EXPOSE` line and your listening port already agree, your app is crashing on boot instead, and the reason is in the container logs. ## When Redeploy or the other buttons refuse Pressing `Redeploy` can come back seven different ways. **This is the list for that one button.** The other buttons on the app page have their own answers, and this is not every response the platform can give you. | What comes back | What it means | Do you meet it in the console? | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `Not authenticated` | Your session has expired. Sign in again and retry. | Only with an expired session. | | `Invalid app ID` | The app identifier in the request is not a valid one. | No. The console never builds a bad one, so this means something other than the console is calling. | | `Repository validation failed` | The repository check refused the deploy. The useful sentence is the one beside it, and it is one of the eight under `Before the build starts`. | Yes. | | `App not found` | The app is gone, usually deleted in another tab. Reload the page. | Yes. | | `App is already deploying` | A deploy for this app is already running. | Yes, but you will not see it. | | `A deployment is in progress for this app, retry in a moment` | Another change to this app is in flight. Wait a few seconds and press again. | Yes. | | `Failed to trigger deployment` | The catch-all. Read the paragraph below before you assume it is a fault. | Yes. | **The already-deploying answer is swallowed, so pressing `Redeploy` during a deploy does nothing visible.** No message, no error, nothing changes on screen. If `Redeploy` appears to do nothing at all, a deploy is already running: open the `Deployments` tab and watch it there rather than pressing the button again. **The same in-progress refusal is worded two ways, depending on which control you pressed.** From `Redeploy` you get the sentence in the table. From `Stop`, `Restart` or `Change plan` you get `A deployment is in progress for this app. Try again in a moment.` instead. The two sentences are the same condition and the same advice: wait a moment, then try again. `Start` is not in that list. The platform has no dedicated sentence for pressing `Start` while a deploy is running, so do not go looking for one. If `Start` comes back with a failure and a deploy is running, open the `Deployments` tab, wait for that deploy to finish, then press `Start` again. ### A first deploy that fails with a trigger failure is usually about credit `Failed to trigger deployment` is the sentence PivoCloud falls back to when it has nothing more specific. One cause reaches it often enough to be worth naming, and the sentence gives you no hint of it: **not enough credit in your wallet.** An app with a monthly price is charged when it is first deployed, not when it is created. If your wallet balance does not cover that charge, the deploy is refused, no money moves, and what you read is the catch-all sentence. It is about credit, not about your code, and there is nothing wrong with your repository. **What to do:** open your wallet, top it up, and press `Redeploy`. This can only happen **before the app's first successful charge**. Once an app has been charged, redeploying it takes no further payment, so a redeploy of a running app can never fail for lack of credit. If you are seeing this on an app that has already been billed, the cause is something else. ## What happens to your credit when a deploy fails If a deploy fails **after** the charge has already gone through, PivoCloud refunds it automatically and in full. You do not have to ask for it and there is nothing to claim. One consequence is worth knowing before you read your wallet history, because it looks wrong and is not: **the next deploy that succeeds charges again.** The refund cancels the month you paid for, so the paid month starts when your app actually runs rather than when you first tried. A wallet showing a debit, then a credit, then a second debit for the same app is one month paid for, not two. # What does it cost, and when am I charged? Source: https://docs.pivocloud.com/billing/credit-and-charges How PivoCloud credit works in Algerian dinars: adding credit and who approves it, what every app plan and database tier costs, when a charge happens and how long a paid period runs, why your price stays fixed, and what happens if your balance runs out. ## Credit and charges Everything on PivoCloud is paid for out of one balance, in Algerian dinars. This page covers what that balance is, how you add to it, what each thing costs, when the money actually leaves, and what happens if it runs out. ### Your credit, and what it is One account, one balance. Apps and databases are both paid from it, so there is nothing else to set up and no card kept on file anywhere. `Wallet` in the sidebar is where that balance lives. The page header reads `Wallet` with the balance beside it, and the same page carries the `Top Up` button and the history of every top-up you have asked for. A new account starts at zero. There is a starting credit you claim once, and [how do I start using PivoCloud](/index) covers claiming it and what it buys. ### Adding credit Credit is added by BaridiMob transfer, and a person at PivoCloud approves it. No part of that approval is automatic. Three of the four steps are yours. 1. Open `Wallet` and press `Top Up`. The dialog is titled `Top Up Wallet`. Choose one of the `Quick amounts` or type your own figure into `Custom amount (DA)`. The smallest amount you can ask for is 100 DA and the largest is 10,000,000 DA. 2. Press `Request Top-Up`. The dialog moves to a second step titled `BaridiMob Payment Instructions`, which shows you three things: the account to pay, the `Amount` to send, and a `Payment Reference` to quote on the transfer. Send that exact amount and quote that reference. PivoCloud does not publish the account to pay anywhere, on this page or on any other: the one to use is the one this step shows you, and it is issued with your request. 3. Your request now sits in the table below, under `Date`, `Amount`, `Status` and `Actions`. Press `Upload proof` on its row and attach a screenshot of the transfer. Then comes the step that is not yours. Someone at PivoCloud reviews the request by hand, so the credit does not appear the moment you upload the proof. `Status` is where you watch it. `Pending` means the request is waiting to be reviewed. `Approved` means the credit is already on your balance. `Rejected` means it was not accepted, and the reason appears under the row. An email reaches you when the request is decided, either way, so you do not have to sit on the page waiting. If a promotion is running, the approval email and your top-up history show the extra credit as a separate bonus line. Nothing announces one in advance, so treat it as a surprise rather than as something to count on. ### When you are charged Two rules, and they are not the same rule. **An app is charged when it first deploys**, not when you create it. Creating an app takes nothing from your balance. The first deploy opens a paid period and takes that app's monthly price. Every deploy after that takes nothing at all: you can redeploy as often as you like inside a period you have already paid for. An app whose monthly price is zero is never charged. **A database is charged when you create it.** There is no separate deploy step for a database, so the tier's monthly price leaves your balance at the moment the database is created. A paid period is 30 days long in both cases. When it renews, the next period starts exactly where the last one ended, so there is no gap and no day is paid for twice. Your app's `Billing` tab shows its `Monthly price` and a `Next charge` line carrying the amount and the date it falls on. That tab also carries the `Auto-renewal` switch, and this is the page that covers it. Turn it off and the app stops renewing: at the end of the period you have already paid for, it expires instead of opening another one. Turning it back on needs a period that is still running, so an app that has none has the switch disabled until you deploy it again. Two things this page deliberately does not repeat. What each button on a running app does to your bill, control by control, is on [what does each button on my app page do](/apps/manage). What happens to a charge when a deploy fails, including the part of your history that looks wrong and is not, is on [why did my deploy fail](/apps/troubleshooting). ### What an app plan costs Three plans you can buy today. The price is per app, per month. | App plan | Price per month | Memory | Processor | Disk | Bandwidth | Apps | Custom domain | | --------- | --------------- | ------ | ----------- | ----- | --------- | ---- | ------------- | | `Lite` | 1,200 DA | 512 MB | half a core | 5 GB | 10 GB | 1 | no | | `Starter` | 3,600 DA | 2 GB | 1 core | 10 GB | 50 GB | 3 | yes | | `Dev` | 7,500 DA | 4 GB | 2 cores | 20 GB | 100 GB | 5 | yes | The apps column is how many apps that plan lets you run at once. The custom domain column is whether you may put a name you own in front of an app on that plan, and [how do I put my own domain on my app](/apps/custom-domains) is the page for doing it. ### What a database tier costs Three tiers. The price is per database, per month. | Database tier | Price per month | Storage | Backups | Backups kept | Service level | | ------------- | --------------- | ------- | ------------- | ------------ | ------------- | | `Starter` | 1,200 DA | 5 GB | daily | 30 days | 99.00% | | `Growth` | 2,500 DA | 25 GB | daily | 30 days | 99.50% | | `Business` | 5,000 DA | 50 GB | every 6 hours | 30 days | 99.90% | The tiers differ in how far back you can go as well as in how much they hold. `Starter` brings a database back to one of its backups. `Growth` and `Business` can bring it back to a moment you choose rather than only to a backup, and how far back that reaches depends on the tier. What a restore actually produces, what it costs and how far each tier lets you go is on [how do I get my data back](/databases/recovery). ### Two names that mean two things Read every price above together with the kind of thing it prices, because two names collide and so does one price. `Starter` is an app plan at 3,600 DA and, separately, a database tier at 1,200 DA: the same word, two different products, two different prices. And 1,200 DA is the price of the `Lite` app plan and also the price of the `Starter` database tier: the same price, two different products. An app plan never includes a database, and a database tier never includes an app, so what you pay each month is one line for every thing you created, read off whichever of the two tables it belongs to. ### Your price does not change under you The price of an app or a database is fixed the moment you create it, and it stays at that figure for as long as that app or that database exists. If our published prices go up, yours does not. One thing changes it, and that thing is you. Changing an app's plan takes the new plan's price as it stands on the day you change it, and that becomes the app's fixed price from then on. Nothing else moves it. A database has no tier change at all, so a database keeps the price it was created at for its whole life. ### If your credit runs out Nothing is taken away without warning. Four steps, in this order. 1. **A low-balance warning, 5 days ahead.** When a charge is coming that your balance cannot cover, an email reaches you 5 days before it is due. 2. **Suspension, when the charge fails.** The app or the database stops serving. Nothing is deleted and nothing is lost at this step. 3. **A notice 7 days before deletion, naming the date.** One email per suspended app or database, carrying the exact date its data goes. 4. **Permanent deletion, 30 days after suspension.** After that the data is gone and cannot be brought back. Three things worth knowing about that ladder. **The low-balance warning is one per account, not one per app or database.** If you are short on two things at once you get one email rather than two, so read it as a warning about your balance and not as a warning about the one thing it happens to name. **The date you are told is the date that is used.** The notice names a day, and that is the day the deletion runs. **Deletion can be extended for some accounts. Suspension never is.** Where an extension applies it delays the deletion only, and when it ends the full wait starts again from that point, with a fresh notice before anything goes. The good news is the simplest part of this page. Top up, and your suspended apps and databases come back on their own, up to what the new balance covers. There is nothing else to press. # How do I connect to my database Source: https://docs.pivocloud.com/databases/connect The connection string PivoCloud gives you for a PostgreSQL database, the setting each client library needs before it accepts that string, what those settings encrypt and what they do not verify, how to pin your database's certificate, and how to turn on pgvector. ## Connecting to your PostgreSQL database This page covers PostgreSQL databases. Your database is reachable from anywhere: your laptop, a build running on another provider, an application you host yourself. What it needs is the connection string PivoCloud generated for it. One honest thing to know before you paste it anywhere. Not every client library accepts this string unchanged, and the settings each one needs are on this page. Read the string first, then the section for the library you use. ### The connection string This is the shape PivoCloud hands you: ```text theme={null} postgresql://:@:5432/pivodb?sslmode=require ``` The three values in angle brackets are yours, and they are already filled in on the string the console shows you. Copy it whole rather than rebuilding it by hand from the parts. ### Where to find it Open your database from the console. The `Connection` section holds it, under the heading `External connection string`. It stays masked until you ask for it: * `Reveal` fetches the string and shows it. * `Copy` puts it on your clipboard. The same string is what your PivoCloud apps receive when you attach this database to them, so an app running here and a script running on your laptop are talking to the database the same way. ### The database name is always the same Every PostgreSQL database on PivoCloud is named `pivodb`. That name is fixed and it is not something you pick when you create the database. It is also not the name you typed on the creation form. That one is the label the console lists your database under, so you can tell your databases apart. It never reaches the server, and a client asking for it will not find it. ### The port The port to connect on is the one in the connection string, `5432`, and nothing else. If you are filling in a client that wants host, port, user and password as separate fields, take all four from the string. Any other number you see alongside them is used internally and will not accept a connection from outside. ## The setting your client library needs Five clients were run against a real PivoCloud database on 2026-09-09, and every setting below is one that connected. Each section names the exact version that was tested, because the correct answer for a client can change with its version, and a version you can check is what makes this page falsifiable. Four of the five take the string unchanged. node-postgres does not. Prisma depends on which of its two connection paths you use: its built-in connector takes the string as it comes, and its `PrismaPg` driver adapter runs node-postgres underneath and inherits its answer. That difference is the reason this section exists. ### node-postgres **The string from the console needs one change.** Tested with `pg` 8.23.0 and `pg-connection-string` 2.14.0. Pasted unchanged, node-postgres refuses to connect: ```log theme={null} self-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca ``` It reads `sslmode=require` as an instruction to verify which server answered, which is not what that setting means in PostgreSQL itself and not something that can succeed here. The shortest working change is one more parameter on the end of the string: ```js theme={null} import pg from 'pg' const client = new pg.Client({ connectionString: process.env.DATABASE_URL + '&sslmode=no-verify', }) ``` `sslmode=no-verify` asks node-postgres to encrypt the connection without verifying the server, which is what `require` already means to the other clients on this page. **One trap worth reading twice.** If you would rather pass an `ssl` object in the configuration than change the string, you must also remove `sslmode` from the connection string. When both are present the object is thrown away and every setting you put on it is lost. The two halves look independent and they are not. ### postgres.js **The string from the console works unchanged.** Tested with `postgres` 3.4.9. ```js theme={null} import postgres from 'postgres' const sql = postgres(process.env.DATABASE_URL) ``` No `ssl` option is needed. postgres.js reads `sslmode=require` out of the string itself and negotiates encryption from it. That the session really was encrypted is not the client's opinion: the server was asked directly, and it answered TLS 1.3. ### Prisma **The string from the console works unchanged with Prisma's built-in connector, and needs one change with the driver adapter.** Tested with `prisma` 7.10.0, `@prisma/client` 7.10.0 and `@prisma/adapter-pg` 7.10.0. With the built-in connector, put the string in your datasource url and change nothing about it: ```bash theme={null} export DATABASE_URL="postgresql://:@:5432/pivodb?sslmode=require" ``` If you use the `PrismaPg` driver adapter instead, it runs node-postgres underneath and inherits its answer exactly, so it needs the same parameter: ```js theme={null} import { PrismaPg } from '@prisma/adapter-pg' import { PrismaClient } from '@prisma/client' const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL + '&sslmode=no-verify', }) const prisma = new PrismaClient({ adapter }) ``` The node-postgres section on this page explains what that parameter does and carries the trap that comes with it. It applies to the adapter word for word. ### psycopg **The string from the console works unchanged.** Tested with `psycopg` 3.3.5, built against libpq 180006. ```python theme={null} import os import psycopg conn = psycopg.connect(os.environ["DATABASE_URL"]) ``` psycopg follows PostgreSQL's own definition of `sslmode=require`: encrypt the connection, do not verify who answered. ### pgx **The string from the console works unchanged.** Tested with `github.com/jackc/pgx/v5` at v5.7.6. ```go theme={null} conn, err := pgx.Connect(ctx, os.Getenv("DATABASE_URL")) ``` pgx implements the same definition of `require` as psycopg does, which is why the two behave identically here. ### One file on your own machine changes the answer for psycopg and pgx Both of them follow libpq, and libpq quietly upgrades `sslmode=require` into a verification mode when a root certificate file happens to exist at `~/.postgresql/root.crt`. If you have that file for some other database, these two clients start verifying, the verification cannot succeed, and a string that works for a colleague fails for you with no obvious reason. Deleting or renaming that file restores the behaviour described above. ## Two ways a connection fails before any of these settings matter ### Connect using the hostname, never an address Use the hostname exactly as the connection string carries it. If you resolve that hostname to an address and connect to the address instead, the connection does not work. What you get is not a certificate error. It is the connection ending with nothing in it. From a Go client: ```log theme={null} failed to write startup message: write failed: EOF ``` From Python: ```log theme={null} SSL error: unexpected eof while reading ``` Both of those read like a network problem or a database that is down, which is exactly why it is worth knowing in advance rather than discovering at the point you are already looking for an outage. ### A connection that asks for no encryption Changing the string to `sslmode=disable` does not connect. The connection is closed. psycopg reports it as: ```log theme={null} server closed the connection unexpectedly ``` and Prisma reports it as `P1017`. Neither message says what refused it, and this page does not guess. Leave the `sslmode` parameter as the console hands it to you, or replace it with one of the settings above. ## What these settings do, and what they do not Every setting above encrypts the connection. Nobody sitting between your application and your database reads the traffic, and each of these sessions was confirmed as encrypted by asking the server itself rather than by trusting the client that opened it. None of them verifies which server answered. The certificate your database presents is generated for that one database and signed by itself, so there is no third party inside it that a client can check it against. That is why a client told to verify has nothing to verify with, and why setting `verify-full` on its own does not work here. The difference is real rather than a formality. A connection that is encrypted but unverified is protected from being read and is not protected from being answered by something that is not your database. You can close that gap yourself, on your own machine, with four commands. Your database's certificate is yours to read, and pinning it is the deliberate version of what libpq does by accident when it finds a root certificate file. ## Pin your database's certificate Do this once per database. `` in every command below is the hostname from your connection string, written exactly as the string carries it. **1. Read the certificate your database presents.** ```bash theme={null} openssl s_client -starttls postgres \ -connect :5432 -servername :5432 -servername /dev/null \ | openssl x509 -noout -fingerprint -sha256 ``` The answer is one line in this shape, and yours is a different value: ```log theme={null} sha256 Fingerprint=99:03:78:8B:60:85:B4:B3:17:9C:CB:E5:91:82:50:B3:60:BF:3F:C8:63:A8:60:EA:2A:A0:A1:77:D3:F5:90:6C ``` Record it somewhere you will look again, because it is what tells you later whether the certificate in front of you is still the one you pinned. One limit worth stating plainly. The certificate you read in step 1 arrives over the same unverified connection you are trying to protect. Pinning it therefore defends every connection after the first one and not the first one itself. If that matters to you, read the fingerprint a second time from a different network and check that the two answers agree before you record it. **3. Save the certificate to a file.** ```bash theme={null} openssl s_client -starttls postgres -connect :5432 -servername /dev/null \ | openssl x509 -out server.crt ``` **4. Prove the pin works before you trust it.** ```bash theme={null} openssl s_client -starttls postgres -connect :5432 -servername \ -CAfile server.crt verify return:1 Verification: OK ``` Run the same command without `-CAfile server.crt` and it ends differently: ```log theme={null} depth=0 CN = verify error:num=18:self-signed certificate verify return:1 depth=0 CN = verify return:1 Verification error: self-signed certificate ``` Those two answers being different is the whole point of this step. A command that prints something reassuring either way has told you nothing. **5. Give the saved file to your client.** Each setting below was run with a saved certificate, and then run again against a completely different database's certificate to check that the client really does refuse the wrong one. Four of the five clients on this page pin, and this list is exactly the set that was tested. **node-postgres.** Pass the file as the trusted certificate and take `sslmode` out of the connection string, for the reason given in its section above: ```js theme={null} import fs from 'node:fs' import pg from 'pg' const url = new URL(process.env.DATABASE_URL) url.searchParams.delete('sslmode') const client = new pg.Client({ connectionString: url.toString(), ssl: { ca: fs.readFileSync('server.crt', 'utf8') }, }) ``` **postgres.js.** The same file, passed in a list: ```js theme={null} import fs from 'node:fs' import postgres from 'postgres' const sql = postgres(process.env.DATABASE_URL, { ssl: { ca: [fs.readFileSync('server.crt', 'utf8')] }, }) ``` **psycopg and pgx.** Both take it as connection parameters. Set `sslmode` to `verify-full` and point `sslrootcert` at the saved file: ```text theme={null} ?sslmode=verify-full&sslrootcert=server.crt ``` **Prisma's built-in connector has no pin, and it is more useful to say so than to publish one that looks right.** With `sslmode=verify-full` and `sslrootcert` pointing at a completely different database's certificate, it still connected. So those parameters do not produce verification on that connector at 7.10.0. What causes that was not established and this page does not guess at it. If you want a verified connection from Prisma, use the `PrismaPg` driver adapter with the node-postgres pin above, which was tested and does refuse the wrong certificate. ### Two things break a pin **Your database being rebuilt.** If PivoCloud has to recreate your database somewhere else, your hostname is kept and the certificate is generated again, so the fingerprint changes and a pinned client stops connecting. The fix is step 1 and step 3 again: read the certificate and replace your saved `server.crt`. It is worth knowing this in advance for an unhappy reason. The moment it happens is the moment your database has just been recovered, which is already a bad enough day without a client refusing to connect for a reason nobody wrote down. **A restore.** A restore gives you a new database with its own hostname, its own credentials and its own certificate, and it leaves your original untouched. A pin made against the original does not carry over to it: take the new connection string from the console and pin the new certificate the same way. The [recovery page](/databases/recovery) covers restoring itself. ## pgvector pgvector is available on every PostgreSQL database here, whatever plan the database is on. Nothing needs to be requested and nothing needs to be upgraded. It is not switched on by default, because an extension is enabled per database. Connect to your database and run this once: ```sql theme={null} CREATE EXTENSION vector; ``` From then on `vector` columns and the similarity operators work as usual, and the extension survives restarts and backups. # How do I get my data back Source: https://docs.pivocloud.com/databases/recovery What PivoCloud keeps of your PostgreSQL database, how to take a backup or an export yourself, how to download and check an export, what a restore really produces and what it costs, how far back each plan lets you go, and what survives losing the machine. ## Backups, exports and restores This page covers PostgreSQL databases. Everything on this page is on your database's own page in the console and you can do all of it yourself. Read the restore section before you use it. A restore does not do what the word suggests, and it takes money from your wallet at the moment you ask for it rather than when it finishes. ### What you can do yourself * **Take a backup now.** `Trigger backup`, in the `Backups` section. * **See the backups you have.** `View history`, with the number of backups you have in brackets after it. It opens a sheet listing them under the columns `When`, `Status`, `Kind`, `Size`, `Duration` and `Action`. * **Restore one of them.** `Restore`, on the row of the backup you want. * **Take an export now.** `Export database`, in the `Exports` section. * **Download an export.** `Download`, on that export's row once it is ready. Backups are also taken for you on a schedule. `Trigger backup` is for the moment you want one before you do something risky, and it adds to the scheduled ones rather than replacing them. Two answers you can get instead of a backup starting: ```text theme={null} A backup is already running for this database. Please wait for it to complete. ``` One backup runs at a time for a database. Wait for the one in flight to finish, then ask again. ```text theme={null} Database not found ``` The database is not there, or it is not one of yours. Open it from your list of databases rather than from a saved link, which may point at one you deleted. ### What a restore actually does, and what it costs **A restore gives you a second database. It does not put your data back into the database you restored from.** `Restore` opens a dialog titled `Restore into a new database`, and the dialog says the same thing in its own words: `This creates a new database from this backup. The source database stays untouched.` You name the new database in `Name for the new database`, and `Confirm restore` starts it. Four things follow from that, and all four are worth reading before you click. * The new database gets **its own hostname, its own credentials and its own certificate**. It is a different database, not a repaired copy of the old one. * The database you restored from is **untouched**. It keeps running, keeps its connection string, and keeps its own backups. * **You are charged when you ask, not when it finishes.** The monthly price of the plan your source database is on is taken from your wallet at the moment the restore is accepted. The dialog states the amount on its own line before you confirm. * The new database then bills on its own cycle like any other database, so **you are paying for two databases until you delete one of them**. Nothing moves your application over for you. Take the new connection string from the console and point your application at it when you are ready, and if you pinned the old database's certificate, pin the new one as well. The [connect page](/databases/connect) covers both. A restore is a job you watch rather than a click that returns. The new database appears in your list immediately and stays in a restoring state until it is ready to accept connections. **A restore cannot be run over the existing database**, and asking for that is refused. Every restore produces a new database. If what you want is your original database back under its original name, restore into a new one, move your application to it, and delete the original once you are satisfied. Six answers you can get instead of a restore starting. All six leave your source database exactly as it was. ```text theme={null} Insufficient wallet balance. ``` Your wallet does not hold the source database's monthly price. Top it up, then ask again. The console also disables the confirm button when it can already see the balance is short, so you may meet this as a button you cannot press rather than as a sentence. ```text theme={null} Backup is not in a restorable state (must be 'success'). ``` That backup did not complete, so there is nothing to restore from it. Pick a row whose `Status` reads as successful, or take a new backup and restore from that one. ```text theme={null} Database or backup not found ``` Either the database or the backup you named is gone. Reopen the history sheet and pick a row from it rather than reusing an identifier you saved earlier. ```text theme={null} Database capacity is temporarily unavailable. ``` There was no free capacity to build the new database into. Nothing was charged. Wait a minute or two and ask again. ```text theme={null} this database was created before the platform started recording which server runs it, so a restore has nowhere to run. Contact support to have it linked, then restore again ``` This one needs us. Email `contact@pivocloud.com` with the database's name, and the restore will work once it is linked. ```text theme={null} the platform could not reserve capacity on the server that runs this database, so a restore would have started somewhere else. Nothing was changed and no credit was taken. Please try again in a few minutes, and contact support if it keeps happening ``` The message says what to do: nothing happened, no credit was taken, try again shortly. If it keeps happening, email `contact@pivocloud.com`. ### Exports, and downloading one An export is a compressed plain SQL dump of your database, produced when you ask for one and downloaded from the console. It is a file you keep. A backup lives on the platform and is what a restore reads; an export is yours to store wherever you like and to load into anything that speaks PostgreSQL. `Export database` starts one. The row underneath moves through four states while it works: it is queued, then it is running, then it either finishes with a `Download` button beside it or it does not finish and the row tells you why. Only one export runs at a time for a database, and there is a wait between one export and the next. The file you get is named after the export's own identifier with a `.sql.gz` suffix, not after your database, so rename it if you are keeping several. The console shows a SHA-256 checksum next to the export and the download carries the same value in an `X-Export-Sha256` response header, so you can prove the file you have is the file the platform made: ```bash theme={null} sha256sum ``` Compare that against the checksum on the row. The export is not encrypted, so where you put it is where its security comes from. Exports do not stay available forever. Each export's own row shows when it expires, and that is the number to read: it is computed for that export, and a figure written on this page would not be. Four answers you can get instead of an export starting or downloading: ```text theme={null} An export is already running for this database. Please wait for it to complete. ``` ```text theme={null} Please wait before requesting another export for this database. ``` Both of those mean wait and ask again, and which of the two you get depends on timing rather than on anything you did differently. There is no need to work out which one applies to you. ```text theme={null} Database not found ``` The database is not there, or it is not one of yours. ```text theme={null} Export not found ``` The export is gone, which is most often because it expired. Ask for a new one. ### How far back you can go Two different things decide that, and reading them as one number is how people end up surprised. **The plan your database is on decides whether you can go back to an arbitrary moment.** * `Starter` has no point-in-time window at all. What Starter has is its backups, described below. * `Growth` advertises a window of 7 days. * `Business` advertises a window of 14 days. **Backups are kept as a count, never as a stretch of calendar.** PivoCloud keeps the 30 most recent successful backups, plus the most recent backup in each of up to 12 further calendar months. Read those as counts of backups, because that is what they are: 30 backups is not the same promise as 30 days, and how far back your 30 reach depends on how often they were taken. This is the figure every plan has, Starter included, and it is what a self-serve restore restores from. Separately from both of those, a copy of your database is taken off the platform every 24 hours. What that copy protects is a different thing, and the next section is about it. ### If the machine holding your database is lost The window above protects you against something that went wrong inside a database that is otherwise healthy: a bad migration, a delete without a where clause, an application writing nonsense for an hour. That is the common case and the window is the right answer to it. It is not what protects you against losing the machine your database runs on. The copy taken off the platform every 24 hours is what protects you against that, and in that situation it is the only thing that survives. So your honest exposure to that second case is those 24 hours rather than the window your plan advertises. If you want it shorter than that for a particular moment, take your own export before you do anything risky and keep it somewhere else. That is the one lever you hold yourself. ### Restoring to a chosen moment `Growth` and `Business` advertise a window you can be restored to any point inside, and there is no control in the console for it today. To use it, email `contact@pivocloud.com` with your database's name and the exact moment you want to go back to, and we will do the restore for you. Be as precise about the moment as you can: the point of the window is that it is not limited to the moments a backup happened to be taken. The self-serve restore described above is a different thing. It restores a backup, so it takes you to the moment that backup was taken, and it is available on every plan. # How do I start using PivoCloud? Source: https://docs.pivocloud.com/index Sign up, verify your email, complete your profile and claim the 1,200 DA starting credit, then follow three steps to a running app: check your repository, connect GitHub, deploy it. ## Get started PivoCloud builds your app from your own Dockerfile, runs it on infrastructure in Algeria, and bills you in dinars. This page takes you from a new account to a deployed app. ### 1. Sign up and verify the email Register in the PivoCloud console with an email address and a password. The submit button is `Create account`. The panel that follows says `Registration successful!` and asks you to check your email for a verification link. Click that link before you do anything else. Signing in is gated on the email being verified, so an account whose link has not been clicked cannot log in at all. If the message never arrives, the same panel carries a `Resend verification` link that sends it again. ### 2. Complete your profile and claim the credit A new account starts with a wallet balance of zero. The 1,200 DA is not granted automatically at signup. You claim it, and a person approves the claim. Two steps, in this order. **Fill in your profile.** On the `Profile` page, fill `First name`, `Last name` and `Phone number`, then submit `Save profile`. All three are required before a claim is possible. While any of them is empty, the button on the dashboard banner reads `Complete Profile` and brings you to this page. `Phone number` has to be an Algerian mobile: ten digits beginning `05`, `06` or `07`, in the shape the field's own placeholder shows, `e.g. 0555 12 34 56`. International notation for the same number is accepted, so there is nothing to guess: a leading `+213` is understood, and spaces and hyphens between the digits are ignored. Anything else is refused with `Enter a valid Algerian mobile number (05, 06, or 07)`, and the profile stays incomplete until it is fixed. **Claim the credit.** Once the profile is complete, the dashboard banner's button reads `Claim Now`. The `Wallet` page carries the same action on its `Free Credits` card, where the button carries the amount instead and reads `Claim 1,200 DA`. Either one works, so click it once. Then expect a wait. The approval is not automatic: someone at PivoCloud reviews the claim by hand, so the credit does not appear the moment you click. While the claim is pending, the dashboard banner hides itself entirely and the dashboard shows nothing about it. The `Wallet` page in the sidebar is the only place the pending claim is visible. Look there. A quiet dashboard does not mean the claim failed. ### 3. What 1,200 DA buys The credit covers one month of `Lite` hosting for one app, at 1,200 DA per app per month, or one month of a `Starter` database at 1,200 DA per month. The full catalogue, including the larger app plans and database tiers, is on [what does it cost, and when am I charged](/billing/credit-and-charges). ### 4. The three things to do first In this order. The failure that costs a new customer their first hour is a repository that cannot build, so the check comes before everything else. **1. Check that your repository meets the deployment contract.** PivoCloud builds from the Dockerfile in your repository and reads the first `EXPOSE` line in it to know which port to reach your app on. Six rules decide whether a repository deploys, and most first deploys that fail, fail on the first two. Read [what your repository needs](/apps/deployment-contract) before you create an app, and fix anything it turns up while you still have an empty account. **2. Connect your GitHub account.** PivoCloud pulls your code through the PivoCloud GitHub App, which you install once and point at the repositories you want it to see. Private repositories work through that connection, and so does deploying on every push. See [connecting GitHub](/apps/connect-github). **3. Deploy it.** Create the app, pick the repository and the branch, choose the subdomain your app answers on, and watch the build. If your app needs API keys, database URLs or any other configuration, set them as environment variables on the app before or after the first deploy; changing one replaces the container, so the new value is live within a deploy. See [deploying your first app](/apps/deploy). # What's new on PivoCloud? Source: https://docs.pivocloud.com/whats-new/2026-09 What you can do on PivoCloud right now: read every guide at docs.pivocloud.com, hand the whole site to an assistant as plain text, follow step by step pages for deploying an app, running a database, billing and custom domains, and call the API with a token you create yourself. ## What's new This page lists what you can do on PivoCloud today. Every entry links to the page that walks you through it, and every one of those pages stands on its own, so you can start from whichever one answers your question. ### September 2026 * Read the whole PivoCloud documentation at [docs.pivocloud.com](https://docs.pivocloud.com), in your browser, without an account. * Hand your assistant one address, [docs.pivocloud.com/llms-full.txt](https://docs.pivocloud.com/llms-full.txt), and it receives every page as plain text in a single request. * Ask an assistant about one page by giving it that page's address with `.md` on the end, which returns the raw markdown instead of the rendered page. * Point a coding assistant at PivoCloud as an MCP server, so its answers come from these pages rather than from whatever it remembers about hosting in general. * Deploy an app straight from a GitHub repository. [What does my repository need](/apps/deployment-contract) is the contract your Dockerfile has to meet, and [how do I deploy my app](/apps/deploy) walks the form field by field. * Connect your GitHub account and choose which repositories PivoCloud can see, including private and organisation ones: [how do I connect my GitHub account](/apps/connect-github). * Give your app its configuration by pasting a `.env` file, and see which keys the platform reserves for itself: [how do I set environment variables](/apps/environment-variables). * Work out why a build or a deploy stopped, message by message, in the words the platform printed: [why did my deploy fail](/apps/troubleshooting). * Run a live app from its page: logs, redeploy, change of plan, stop and delete, with what each one does to your balance: [what does each button on my app page do](/apps/manage). * Create a PostgreSQL database and connect to it from your own code, with the TLS setting your driver needs: [how do I connect to my database](/databases/connect). * Download an export of your data, or put a database back to an instant in the recovery window: [how do I get my data back](/databases/recovery). * See what each plan and tier costs in dinars, when the money actually leaves, and what happens if your balance runs out: [what does it cost, and when am I charged](/billing/credit-and-charges). * Put your own domain in front of an app, with the record to create and the certificate handled for you: [how do I put my own domain on my app](/apps/custom-domains). * Create a personal access token in the console and call the PivoCloud API from your own code or from a script: [how do I call the API with a token](/api/personal-access-tokens). * Browse every public endpoint, its parameters and its replies, in the generated [API reference](/api-reference). New entries appear here when there is something new for you to use. There is no schedule, so nothing on this page is waiting for a date. # What can I find out before I sign up? Source: https://docs.pivocloud.com/whats-new/2026-09-15 What you can learn about PivoCloud before creating an account: answers to the most common questions on the front page, what the signup credit covers in dinars, what both products cost on one pricing page, and the recovery window each database tier includes. All readable on a phone. ## What you can find out before you sign up You do not need an account to answer any of the questions below. Each one is answered on a public page, and the money figures on those pages are the ones the platform charges from, not fixed examples written by hand. ### September 2026 * Read the answers to the questions people ask most before signing up, on the front page at [pivocloud.com](https://pivocloud.com). They sit near the foot of the page and open one at a time, so you can read only the one you came for. * See what the credit you receive at signup actually covers, in dinars, beside those answers. The amount shown is the amount the platform grants, so it cannot drift from what you will really get. * Compare what both products cost on one page at [pivocloud.com/pricing](https://pivocloud.com/pricing): the app plans and the database tiers, each with its monthly price in dinars. * Check how far back each database tier lets you recover before you choose one. The window shown on the pricing page is the window that tier really covers. [How do I get my data back](/databases/recovery) explains what recovering to a past instant involves once you have a database. * Read all of it on a phone. The front page, the pricing page and the signup and sign in pages are laid out for a narrow screen, and every button and link is large enough to press with a thumb. * Send someone a PivoCloud link in a message, a chat or a post and have it arrive with a title, a short description and a preview image instead of a bare address. If you have already signed up, [what does it cost, and when am I charged](/billing/credit-and-charges) is the page that covers charges on your account, and it goes further than the pricing page does. New entries appear here when there is something new for you to use. There is no schedule, so nothing on this page is waiting for a date.