Projects
Group containers, volumes and networks into an isolated unit.
Last updated: September 8, 2026
What is a project?
A project bundles an application's containers (front, back, database, sidecars) inside its own Docker networks, with its dedicated quota. It belongs to a single organization. A project's detail page is split into eight tabs: Containers, Topology, Deployments, Repositories, Logs, Secrets, Backups and Network groups.
A project's detail page is split into nine tabs: Containers, Topology, Deployment, Repositories, Logs, Secrets, Backups, Security and Network groups. Each is described below, except Security, which has a page of its own.
The project list
The Projects page lists every project in the active organization. Each row carries an icon, the name and slug, the number of containers used (against the quota), the creation date and actions (open, rename, delete). A counter at the top shows your usage against your plan.
Each row also shows a 7-day health bar, the same visual language as the dashboard: one colored cell per day of the current week, summarizing the worst container state seen that day. Hover a cell to see the date, the status and the healthy versus total container count. Click a cell to open that day's detailed history, scoped to this project.
Create a project
Click Create a project. Give it a name, the slug is derived from it and locked at creation. Depending on your deployment target, pick the agent (server) that will host the project. Confirm to create the empty project, ready to receive containers.
The number of projects and containers per project depends on your plan. When the quota is reached, the create button offers to upgrade to a higher plan (upgrade dialog) instead of creating.
Creating a container
Inside a project, the dedicated button opens container creation. You give a name, from which the slug is derived, then:
- The type. A web container is reachable over HTTP and gets a subdomain; a worker container, for a message queue or a scheduled job, exposes no port and is not probed over HTTP. Pick it at creation, it cannot be changed afterwards.
- The GitHub repository, the branch to watch and the folder holding the code, with a browser to find it.
- The framework. If your Dockerfile carries a LABEL framework line, Pierrr picks it up on its own and locks the choice; otherwise you select it, for information only when a Dockerfile exists, or to choose the template Pierrr will generate when there is none.
- A "Monorepo: build from the repository root" switch, for when the Dockerfile lives in a subfolder but the build needs to see the whole repository.
Containers tab
The Containers tab shows each container of the project as a card: detected framework logo, name and slug, and live state. You can:
- Read each container's live state, Running, Stopped, Creating or Failed, with a colored dot.
- Start a stopped container, individually or all at once via Start all.
- Stop one or all containers to pause traffic.
- Restart a container, useful after changing environment variables or rotating a secret.
- Deploy a container with the lightning button on its card: one click builds and deploys the latest version of the watched branch, and the chevron next to it lets you pick a different commit.
- Create a container via the dedicated button (blocked by an upgrade dialog when the project quota is reached).
- Adjust a container's instance count with the +/- stepper next to "Instance count", directly on the card.
Project-level actions
The project header exposes global actions (start / stop / restart all containers) along with the deployment mode (the targeted agent and how deployments are triggered).
Scaling instance count
Every container card shows an "Instance count" stepper that lets you raise or lower the container's instance count without going through a new deployment. Until the number of running instances catches up with the target, a hint like "2/3 active" stays visible on the card.
The maximum instance count per container depends on your plan: 1 on Free, 2 on Starter, 5 on Pro and 15 on Business, with an absolute ceiling of 50 instances regardless of plan. Past the plan's quota, the stepper opens an upgrade dialog instead of increasing the instance count.
Processor and memory limits
Every container exposes its resource limits. Two values, not to be confused: the ceiling, which the container cannot go past, and the guaranteed share, which stays its own even when a greedy neighbour runs on the same machine. The second protects your service, the first protects everyone else's.
The ceiling you may ask for depends on the plan: 512 MB of memory and half a core on Free, 1 GB and one core on Starter, 2 GB and two cores on Pro, 4 GB and four cores on Business.
The /health endpoint is mandatory
Pierrr enforces its own healthcheck on every container: a GET request to http://127.0.0.1:<PORT>/health, regardless of any HEALTHCHECK declared in your Dockerfile (which is ignored: Pierrr's compose-level healthcheck always takes precedence). Your app MUST respond 200 on /health, without your framework's global prefix if it has one (e.g. NestJS with setGlobalPrefix('api'): explicitly exclude the health route from the prefix). Without this, the container will never turn healthy and every deployment will fail after 90 seconds, even if the app starts and runs fine.
Answering 200 is not enough to make the signal useful. Answer 200 only once your service can actually serve: database reachable AND schema in place, required dependencies ready. A route that returns 200 as soon as the process is up reports a live process, not a working service, and can never surface a failure. A real case: a `/health` checking a Postgres extension that Pierrr installs before any customer code answered 200 while the database had no tables at all.
// NestJS - exclude /health from the global prefix
app.setGlobalPrefix('api/v1', { exclude: ['health'] });- No configuration on Pierrr's side: the /health route just needs to exist and return 200 on your app's port. A HEALTHCHECK in your Dockerfile isn't required (it's ignored), no need to maintain one in parallel.
- During a deployment, Pierrr waits for the new instance to turn healthy on /health (up to 90 seconds) before switching traffic over; if it doesn't, the old instance stays in place and the deployment is rolled back.
- The healthy/unhealthy state reported by Docker feeds the server's aggregate health count and the unhealthy-container notification, on top of the plain run state.
/health must stay reachable without authentication and without a prefix, on the same port as the rest of your app.
Your Dockerfile and the preflight
When you deploy from a GitHub repo, Pierrr uses your own Dockerfile if one exists, otherwise it generates a minimal per-framework template. Before each build, Pierrr checks your Dockerfile and a few project files to catch the most common mistakes. A blocking error stops the build with a clear message ; warnings do not stop the build and show up in the deployment logs, prefixed with preflight.
Vault variables are injected at container RUNTIME, never as build arguments: `docker build` does not see them. This matters for frameworks that inline their public variables at compile time (`NEXT_PUBLIC_*` with Next.js, `VITE_*` with Vite): the vault value will not reach your bundle, which keeps the fallback hard-coded in your source. Nothing fails at deploy, and the defect only shows up in the browser's network calls. Read those values server-side at startup and pass them to your pages instead of freezing them at build time.
- LABEL before the first FROM (blocking) : Docker refuses to build a Dockerfile whose LABEL line comes before the first FROM. Always place your LABEL framework line after a FROM. This case is also flagged in red as soon as you pick the repo, before the build even starts.
- Prisma without openssl or schema (warning) : the prisma generate postinstall needs the schema and openssl at build time. On an alpine base, add RUN apk add --no-cache openssl and copy COPY prisma ./prisma before installing dependencies.
- reactCompiler without @swc/helpers (warning) : if reactCompiler is enabled in next.config, add @swc/helpers to your dependencies, otherwise Next's standalone build won't bundle it and the container crashes at startup.
These checks save you from a build that fails with no message or a container that crashes at startup. Warnings stay advisory : you keep full control over your Dockerfile.
Values your framework needs at build time
Some frameworks resolve values while compiling rather than at runtime. The common case is Next.js' `metadataBase`, which canonical URLs and Open Graph tags depend on: if your pages are statically prerendered, the value is frozen during `docker build`, before Pierrr injects anything. Pages then ship with `http://localhost:3000` as their canonical, which is the URL a crawler or a link preview will actually use.
Pierrr does not pass vault variables as build arguments, and will not: an `ARG` stays readable in the image history (`docker history`), so the mechanism quietly invites values that have no business being there. A declared but unset `ARG` also yields an empty string rather than an absent variable, which turns an oversight into `new URL('')` instead of a clear error.
The fix is two rules. First, read the variable lazily inside a function, never in a module-level `const`: a `const` is evaluated once and can be frozen into the bundle, whereas a function call re-reads `process.env` in the running container.
// env.ts - read on every call, server-side, in the running container.
export function getPublicOrigin(): URL {
return new URL(process.env.PUBLIC_WEB_ORIGIN ?? 'http://localhost:3000');
}
// app/layout.tsx
export async function generateMetadata(): Promise<Metadata> {
return { metadataBase: getPublicOrigin() };
}Second, make sure the metadata is really evaluated per request. A statically prerendered page runs `generateMetadata` at build time, where the variable does not exist yet. Incremental regeneration does not quite close it either: the first visit after a deploy still gets the build output. To be correct from the very first request, derive the origin from the `Host` header instead of a variable, which makes the page dynamic and removes the need to configure anything:
import { headers } from 'next/headers';
export async function generateMetadata(): Promise<Metadata> {
const h = await headers();
const host = h.get('x-forwarded-host') ?? h.get('host');
return { metadataBase: new URL(`https://${host}`) };
}For a value a client component needs, the principle is the same: read it in a server component and pass it down as a prop. The client component receives it at runtime, never depending on a value frozen at compile time.
Topology tab
Topology shows containers, your managed database and, once linked, your Redis add-on as a hierarchical graph organized by tier: proxy, frontend, backend, cache, database. Docker networks aren't separate nodes anymore: they show up as edges between the containers that share them, click an edge to see the underlying network links (name, aliases). Select a node to view its details (mount paths, attached networks, aliases, isolation) and act on it, start, stop or restart a container, mount or unmount a volume, unlink a Redis instance (the managed Postgres now unlinks from the Secrets tab instead, see below). The database node is labeled shared (Free plan) or dedicated (paid plan), a plan-driven distinction today, not yet a physically separate instance. When the project has no container yet, an empty state invites you to create one.
Volume and Redis add-ons
A volume and a Redis instance are purchased as add-ons from Billing, then Add-ons. A purchased volume starts unmounted, open its node in Topology to attach it to a container and a mount path. A purchased Redis instance has no node until it's linked : link it to a project from the Redis panel above Topology, then unlink it from its node in Topology once connected.
Connecting a resource to a container
A managed resource only reaches a container if you connect it there explicitly: buying object storage or linking a database is not enough, and nothing is broadcast to the whole project by default. The control lives on the RESOURCE node, not on the container's: open the database, cache or storage node in the Topology and tick the containers that should receive it. The matching variables (DATABASE_*, CACHE_*, STORAGE_*) are injected on the next deploy, and a container left unconnected will start without them. On a container, the Connected resources line shows what it is attached to, or says plainly that it is attached to nothing.
Deployments tab
The history lists every deployment of the project, newest first (paginated). For each row:
- The four pipeline stages, build (image build), push (push to the registry), load (image pull) and start (container create + launch), each with its status icon.
- A mono status badge (Succeeded, Failed, Running, Pending, Canceled).
- The container (logo + name + slug), the branch and commit (clickable to GitHub), the start time and the duration.
- The number of attempts when a deployment was retried.
Deploying a specific commit
The deploy button on each container card is split in two. The main action has not changed: one click builds and deploys the latest version of the watched branch, with no extra step and no extra request.
The chevron next to it opens a dialog listing the recent commits of that branch. A search box filters the list by message, by author or by hash, in its short form as well as its full one. Each row shows the commit message, its short hash and its author.
Selecting a row immediately deploys that version instead of the latest one. This is how you roll back to a known-good version without touching your repository, or ship a fix you already validated while the branch has moved on.
The chosen commit is recorded on the deployment: its hash is what you find in the history, and that is the code actually put online. If the container is already running and the last image built matches the commit you picked, that image is reused rather than rebuilt.
The list is fetched when the dialog opens, never before, and covers the last thirty commits of the watched branch. If it cannot be retrieved, for instance when no repository is linked to the container, the dialog says so and deploying the latest version stays available.
Where the project runs
The Deployment tab carries the project's deployment mode: on Pierrr's infrastructure, the default, or self-hosted on one of your servers, which you then pick from the list of online agents.
Fast mode
By default, stacking several pushes makes each deployment wait behind the previous one: the version you actually wanted online goes last, after builds that became pointless. The Fast mode switch changes that: a new deployment cancels the one still running for that container and takes its place.
The effect stays bound to the container involved: redeploying your api container never interrupts the build running for your web container. The setting is off by default, and a deployment dropped this way carries its own reason in the history, not to be confused with a manual cancellation.
Cancel, re-run, export
- Cancel, while the deployment is pending or running.
- Re-run, on a failed deployment. The new attempt restarts from the stage that failed, and the stages that already succeeded are reused as they are.
- Download an attempt's full log, to read it outside the console or attach it to a support request.
Build logs are kept for a length of time that depends on your plan, recalled on the deployment detail page.
Deployment detail
Click a row to open the detail: the stage timeline, each attempt with its status and duration, the failure reason if any, and the full line-by-line logs (tagged per stage). This is where you diagnose a broken build or a container that won't start.
Repositories tab
The Repositories tab lists the GitHub repositories linked to the project's containers, with the watched branch and the Dockerfile path. It is the link between your code and each container deployed from a push.
Each linked repository shows an Auto-deploy switch, on by default. While it stays on, a push on the watched branch triggers a build and a deployment for that container. Turning it off only affects that container: other containers on the same project or repository keep deploying normally, and the GitHub webhook stays in place, it simply does nothing while the switch is off.
Click Edit next to the watched branch to point the container at a different one, for example to promote a feature branch to production, without deleting and recreating it. This only changes which future pushes trigger a deploy: it does not redeploy the container that is currently running. Trigger a deploy afterward with a push on the new branch, or with the container's Build button.
Logs tab
The Logs tab shows container logs. Pick the container in the selector; when the project has no container, an empty state says so.
By default, logs refresh periodically. Toggle Live to stream them in real time instead; the two modes never run at the same time.
Secrets tab
The Secrets tab manages a per-project encrypted vault (environment variables, API keys…). Each secret can be injected into containers in one of three modes:
- Bound, a container only receives the secrets you explicitly bound to it.
- Detected, a container receives the vault secrets whose key is declared in its `.env.example`.
- All, a container receives EVERY vault secret (org + project scope), regardless of binding or `.env.example`.
Choosing the mode is reserved for paid plans. On the Free plan it is fixed: every container receives the whole vault.
"Managed by Pierrr" section
A read-only section lists the variables Pierrr injects into your containers automatically, no manual setup needed. They always win over the vault and can't be edited or deleted from it while the matching resource stays linked. If you try to manually create a secret with one of these names, Pierrr rejects it and spells out exactly how to unlock the variable: unlink the corresponding resource.
- Managed Postgres (linked by default to every project): `DATABASE_URL`, `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`, `DATABASE_PASSWORD`. These names are deliberately generic rather than `PG*`: libpq does not auto-recognize them, so a container hosting its own Postgres never has its tooling accidentally redirected at the managed instance. A "Use Pierrr's managed Postgres" toggle, right above this section in the Secrets tab, lets you unlink the project without destroying the database or its credentials. Once unlinked, these names become free and you can set them yourself in the vault, for example to point `DATABASE_URL` at an external database. Unlinking is a paid-plan feature: on the Free plan, the toggle opens an upgrade dialog.
- Redis add-on (once linked to this project from Billing, then Add-ons): `CACHE_URL`, `CACHE_HOST`, `CACHE_PORT`, `CACHE_PASSWORD`. Same reasoning as above: `CACHE_*` rather than `REDIS_*`, so redis-cli does not latch onto them by itself. Unlink it from its node in the Topology tab to free these names; no plan restriction applies.
- Object storage add-on, S3-compatible (once linked to the project, then each container connected individually): `STORAGE_ENDPOINT`, `STORAGE_PUBLIC_URL`, `STORAGE_REGION`, `STORAGE_BUCKET`, `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY`. `STORAGE_ENDPOINT` is the address internal to the project network; `STORAGE_PUBLIC_URL` is the origin a browser reaches, and it is the one your presigned URLs must carry, or their signature will not verify. The bucket created up front is named after the project slug: a convenient default, not a ceiling. Your application may create others.
- Postgres extensions: `pgvector`, `pg_trgm`, `pgcrypto`, `uuid-ossp`, `unaccent`, `citext` and `btree_gin` are enabled per project from the Topology tab. Pierrr runs the creation with the privileges required; your database role stays deliberately restricted, so a migration doing `CREATE EXTENSION IF NOT EXISTS` works once the extension is enabled, with no change on your side.
Prisma
Prisma needs a second variable alongside `DATABASE_URL`: `DIRECT_URL`, used only for migrations. It normally exists to bypass a connection pooler (PgBouncer, Supabase…) sitting in front of the database, which does not support some of the DDL commands `prisma migrate` runs. Pierrr's managed Postgres is already a direct connection, with no pooler in front of it, so you can simply point `DIRECT_URL` at the same value as `DATABASE_URL`.
In your `schema.prisma`, declare both variables on the datasource:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}Then, in your container's entrypoint (the Dockerfile CMD or a startup script in `package.json`), export `DIRECT_URL` from `DATABASE_URL` before running migrations:
export DIRECT_URL="$DATABASE_URL"
npx prisma migrate deployNo action is needed on Pierrr's side: `DIRECT_URL` is not a managed variable, it lives entirely in your application's code.
Bulk edit (paste a .env)
Instead of adding variables one at a time, bulk edit lets you paste a whole .env block at once. Pierrr diffs your paste against the project's existing variables and applies only the differences.
Step by step:
- Open your project, then the Secrets tab. At the top right of the card, click the Bulk edit button (next to Add variable).
- In the dialog that opens, paste your .env content into the text area: one variable per line, in KEY=value format. Values may be quoted.
- To delete an existing variable, add a line prefixed with an exclamation mark, e.g. !OLD_FLAG.
- Read the preview below the text area: it shows how many variables will be added, replaced and deleted, and highlights invalid lines in red (the Apply button stays disabled while any line is invalid).
- Click Apply. The variables are saved (new ones are masked by default) and the list updates immediately.
Example content to paste:
DATABASE_URL=postgres://user:pass@db:5432/app
API_KEY=sk_live_123
# remplace une clé existante, ajoute une nouvelle
NODE_ENV=production
# supprime une variable qui existait
!OLD_FLAGMerge rules applied to your paste:
- A key that already exists is replaced with the pasted value.
- A missing key is added (masked by default).
- A line prefixed with ! (e.g. !OLD_KEY) deletes the variable if it exists.
The live preview recaps, before you apply, how many additions, replacements and deletions there are, and flags invalid lines to fix.
Backups tab
Open Archive to create or restore a backup. Two types are available:
- Data backup, a single run for the whole project: every volume of every container, plus a dump of the linked managed database. Give it a name, or one is generated automatically; the history lists each run by that name. Restore overwrites the app's current state; it is destructive and requires confirmation.
- Pierrr project export, exports the project's structure only (apps, repo bindings, network groups and volume metadata), with no data. Validating an export against another organization checks that it has a matching GitHub App installation for every repository, re-creating the project there is a documented next step, not shipped yet.
A project export can be created, downloaded and validated against another organization. Importing it back is not available yet: the matching button is visible but inactive.
Data backups are not included on the Free plan: the tab offers an upgrade instead. On paid plans, how long archives are kept depends on the plan, and the oldest ones are purged automatically.
Scheduling backups
A Schedule accordion sets a recurring backup of the project: daily or weekly, with the day of the week and the run hour in UTC, and an option to get an email after each run. Scheduled backups appear in the same history as the ones you start by hand.
Restoring a backup rewrites the project's volumes and database with the contents of the archive, and restarts the project. The operation destroys the current data, so it asks for a confirmation.
A successful backup can be downloaded, to keep a copy outside Pierrr.
Import my data (SQL dump restore)
At the top of the Backups tab, in its own "Import my data" accordion, a button restores an existing SQL dump straight into the project's managed database, no SSH or `psql` needed.
- Format: plain-text `.sql` file only, 15 MB maximum. The binary `pg_dump --format=custom` format is not accepted.
- Only one import can run at a time per project; retry once the previous one has finished.
- All or nothing: the dump runs as a single transaction. If anything conflicts with what's already there (a table that already exists, for example), the entire import fails and nothing is applied, so it's safe to retry.
- A history list shows every import live (filename, size, status: pending, running, succeeded, failed) with the error message visible inline when one fails.
- The uploaded file's content is encrypted at rest and is never retained once the restore finishes, whether it succeeds or fails.
Network groups tab
Group the project's containers into isolated Docker networks. Every project starts with a protected internal group that cannot be renamed or removed. Create additional groups with Add, then use Manage members to choose which containers join each group. Available on plans whose network policy is choice; on Free, projects stay locked to the single internal group.
A project's actions menu, the "..." next to Start, Stop and Restart, includes Resync networks: it reattaches every container to its network configuration right now, without waiting for the next deploy. Useful for troubleshooting a container that is not reachable on the network you expect. Available on every plan.
As with secrets, creating extra groups is reserved for paid plans: on the Free plan the project stays on its single internal group.
Published TCP ports
Some protocols cannot be routed over HTTP. A Published TCP ports card, at the bottom of the Network groups tab, exposes a container port directly on the server's public address. Pick the container, the internal port, and optionally a different public port.
A contiguous range is possible, for a service that needs one, such as the passive mode of a file server.
Two caveats. Every open port is one more exposed surface: publish only what the service genuinely needs. And redeploying a container that publishes a port briefly cuts the service, since a port cannot be held by the old and the new instance at the same time.
How many ports a container may publish depends on the plan, from the Starter plan onwards. The Free plan publishes none.
Database panel
A project's action menu opens, where the instance provides one, an administration panel for the managed database in a new tab: enough to inspect your tables and run a query without installing a client on your machine. You arrive already signed in, and you only see this project's database.