Using your database during the build
A build can use your project’s database: run prisma migrate deploy from a
prebuild script, generate code from the live schema, prerender pages from real
rows. There is one requirement — the site must be
connected to the database — and then every build of that
site gets:
- a network path to exactly the databases the site is connected to — the ones its running app can reach, and no others. A Valkey is the exception: no build reaches it (see Valkey);
- their connection variables —
DATABASE_URLand the rest, the same values the running app receives — as build secrets: present while the build steps run, never written into the image, its layers or its history.
No public database host, and no build-time copy of your connection string.
This covers the builds Ghayma runs: ghayma deploy, and pushes to a linked
GitHub branch. An image deploy
is built on your own machine, where these variables do not exist — run its
migrations when the container starts.
Connect the site to the database
Check the site’s Connections tab in the dashboard (Project → the site → Connections), or ask the CLI:
ghayma db sites my-postgres # which apps are connected
ghayma db sites my-postgres --add main # connect the 'main' siteA new connection reaches builds the way it reaches the running app: the
database’s network policy converges within about two minutes, so deploy
after that. A site is connected only to what you choose: when you create the
database, or when ghayma deploy offers a database no site uses yet — one you
created before your first deploy, say. See How a service gets
connected.
What the build receives
The variables of every database the site is connected to, a Valkey excepted — the names the site’s Connections tab shows on its database rows:
| Engine | Variables |
|---|---|
| PostgreSQL | DATABASE_URL, plus DATABASE_URL_<NAME> for each connected database |
| MongoDB | MONGODB_URI and its alias MONGODB_URL, plus MONGODB_URI_<NAME> and MONGODB_URL_<NAME> for each connected database |
| Valkey | Nothing: a build never reaches a Valkey, and its variables are runtime only (see Valkey) |
They are the running app’s own values — the database’s internal host, and the same credential the app connects with. One set of variables per database says which database the flat names point at.
- Databases only. Bucket credentials, the auth app’s variables,
GHAYMA_API_KEYand cron secrets are not handed to the build. The auth app’s id is the one exception: it is not a secret, and a frontend bundle needs it — see The app id in a frontend build. - Only databases on Ghayma. A database hosted outside Ghayma is not a connection, so nothing is derived for it. Set its URL yourself as a build-time variable, with the caveats given there.
- Each site’s own. A build receives its own site’s connections. Like every
connection variable they are never inherited,
so a
devsite’s build uses the databasedevis connected to, not production’s. - The connection’s level applies. A site connected
read-onlybuilds with a credential that cannot write, so a migration in its build fails. Migrate from a site connected atconnect. - It is the real database. Whatever the build writes — a migration, a seed script — lands in the database your app uses.
Generated builds: nothing to do
When Ghayma writes the Dockerfile (Next.js, other Node apps, static sites) or
builds with Railpack (Python, Go, PHP, Ruby and the other Railpack
languages), every step that builds your app
sees the variables: the install, prisma generate, your build command and its
prebuild script. The stage that becomes the running image never does; the app
gets its variables at runtime, as always.
So a migration in prebuild just works:
{
"scripts": {
"prebuild": "prisma migrate deploy",
"build": "next build"
}
}The build log names what the build received — names only, never values:
Detected: nextjs (Node 22, install: npm ci) [build secrets: DATABASE_URL, DATABASE_URL_MAIN_DB]A Railpack build prints Build-time variables (every build step, as secrets): …
instead.
Custom Dockerfile: mount what a step needs
With your own Dockerfile, a secret is visible only
to a RUN step that mounts it — one --mount per variable, whose id is
the variable’s name:
RUN --mount=type=secret,id=DATABASE_URL,env=DATABASE_URL npx prisma migrate deployenv=DATABASE_URL hands it to that one step as an environment variable of the
same name. The next step no longer has it, and nothing of it lands in a layer.
A multi-stage build in which only the builder’s migration step touches the
database:
# Stage 1 — build. The only stage that sees the database.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# A hash of the database variables: the steps below re-run when they change
ARG GHAYMA_BUILD_SECRETS_HASH
RUN --mount=type=secret,id=DATABASE_URL,env=DATABASE_URL npx prisma migrate deploy
RUN npm run build
# Stage 2 — runtime. No secret, no credential in the image.
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
COPY --from=builder --chown=node:node /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]-
Mount it on every step that needs it. If
npm run buildreads the database too — pages prerendered from real rows — give that step the same--mount. A second database is a second mount:--mount=type=secret,id=DATABASE_URL_ANALYTICS,env=DATABASE_URL_ANALYTICS. -
Declare
ARG GHAYMA_BUILD_SECRETS_HASHbefore the step. BuildKit leaves secret values out of its layer cache, so without it an unchanged step is reused even after the database behind it changed — you connected another one, or its credential was re-issued. The platform passes a hash of the values, never the values, under that name; declaring it re-runs the steps after it whenever they change. -
The build log lists what you can mount, with a line to copy:
Build secrets available: DATABASE_URL, DATABASE_URL_MAIN_DB A RUN step sees one only if it mounts it, e.g.: RUN --mount=type=secret,id=DATABASE_URL,env=DATABASE_URL <command> Declare ARG GHAYMA_BUILD_SECRETS_HASH before that RUN so it re-runs when a value changes.
env= needs Dockerfile syntax 1.10 or later, which is what a Dockerfile with no
# syntax= line gets. If yours pins an older version, remove the pin — or leave
env= out and read the value from the file the mount creates,
/run/secrets/DATABASE_URL.
Never pass a credential as ARG or ENV
# Don't
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URLBoth can leave the password in what the build records — ENV in the image’s
configuration, ARG in its build history — where anyone able to pull the image
can read it. Mount the secret instead; it records nothing.
On a site connected to a PostgreSQL database those two lines do not even work
any more: a build-time DATABASE_URL you set yourself is no longer passed to
the build (see below), so
they give it an empty DATABASE_URL. If your Dockerfile has them from an
older setup, delete both and add the --mount.
The platform keeps the values out of the image; a build step can still put
one back. Writing a variable into a file, or a bundler setting that inlines it
(the env key of next.config.js, say), copies the password into your
output.
If you set a build-time DATABASE_URL yourself
Before builds could reach project databases, the workaround was a
build-time DATABASE_URL, often with a
build-time override pointing at the public …db.ghayma.tech host. It is no
longer needed, and no longer used: on a site connected to a PostgreSQL
database, the platform’s value wins over one you set under the same name — in
the build exactly as at runtime. Delete it anyway, so nobody mistakes it for
the value in use:
ghayma env delete DATABASE_URLor remove the row on the site’s Environment tab. Do this only on a site
connected to that database: on a site connected to no PostgreSQL database, a
DATABASE_URL of your own is the only one the app gets.
Only the names the platform derives are replaced. A build-time value under a
name of your own — DIRECT_URL, say — still reaches the build exactly as you
set it; if it was part of the old workaround, read DATABASE_URL (or
DATABASE_URL_<NAME>) instead and delete it. DATABASE_URL connects straight
to the database, with no pooler in between, so Prisma needs no separate direct
URL for its migrations.
Troubleshooting
| What the build log shows | Why | What to do |
|---|---|---|
P1001: Can't reach database server, or a connection that times out | The build has no path to that database. The site is not connected to it (the URL came from a value of your own), the connection is less than two minutes old, or the database is stopped | Connect the site — ghayma db sites <db> --add <site> — wait two minutes and deploy again. Start a stopped database with ghayma db start <db> |
P1011: Error opening a TLS connection: unexpected EOF | A value of your own pointing at the public …db.ghayma.tech host | Read DATABASE_URL instead and delete that value — see above |
Environment variable not found: DATABASE_URL, on a generated build | The site is not connected to a PostgreSQL database, so there is no DATABASE_URL to hand the build | Connect it, then deploy again |
DATABASE_URL missing or empty in a custom Dockerfile | The step does not mount it, or ARG/ENV DATABASE_URL lines from an older setup set it to an empty value | Add --mount=type=secret,id=DATABASE_URL,env=DATABASE_URL to that RUN and delete the ARG/ENV lines. No Build secrets available: line in the log means the site is connected to no database |
P1002 and advisory lock | Something else was migrating the same database at that moment — another build of a site on it, or a container migrating at start | Deploy again once it has finished. See Prisma and database migrations |
permission denied while migrating | The site is connected read-only | Change the connection’s level to connect, or migrate from a site connected at that level |
A custom Dockerfile’s migration step shows CACHED after the database changed | Secret values are not part of BuildKit’s cache | Declare ARG GHAYMA_BUILD_SECRETS_HASH before that step |
Could not read this site's database credentials; aborting deploy | The platform could not read the credentials to hand the build, and stopped rather than build without them | Deploy again. If it persists, get in touch |
A failed build leaves your previous build serving. A migration it had already applied stays applied.
Related
- Builds — the queue, the limits, and what a build gets
- Custom Dockerfile — bringing your own build
- Connections — which app may use which service
- Environment Variables — every variable the platform derives
- Next.js — where to run Prisma migrations