Skip to Content
Framework GuidesDatabase at Build Time

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_URL and 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' site

A 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:

EngineVariables
PostgreSQLDATABASE_URL, plus DATABASE_URL_<NAME> for each connected database
MongoDBMONGODB_URI and its alias MONGODB_URL, plus MONGODB_URI_<NAME> and MONGODB_URL_<NAME> for each connected database
ValkeyNothing: 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_KEY and 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 dev site’s build uses the database dev is connected to, not production’s.
  • The connection’s level applies. A site connected read-only builds with a credential that cannot write, so a migration in its build fails. Migrate from a site connected at connect.
  • 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:

package.json
{ "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 deploy

env=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:

Dockerfile
# 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 build reads 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_HASH before 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_URL

Both 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_URL

or 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 showsWhyWhat to do
P1001: Can't reach database server, or a connection that times outThe 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 stoppedConnect 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 EOFA value of your own pointing at the public …db.ghayma.tech hostRead DATABASE_URL instead and delete that value — see above
Environment variable not found: DATABASE_URL, on a generated buildThe site is not connected to a PostgreSQL database, so there is no DATABASE_URL to hand the buildConnect it, then deploy again
DATABASE_URL missing or empty in a custom DockerfileThe step does not mount it, or ARG/ENV DATABASE_URL lines from an older setup set it to an empty valueAdd --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 lockSomething else was migrating the same database at that moment — another build of a site on it, or a container migrating at startDeploy again once it has finished. See Prisma and database migrations
permission denied while migratingThe site is connected read-onlyChange 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 changedSecret values are not part of BuildKit’s cacheDeclare ARG GHAYMA_BUILD_SECRETS_HASH before that step
Could not read this site's database credentials; aborting deployThe platform could not read the credentials to hand the build, and stopped rather than build without themDeploy again. If it persists, get in touch

A failed build leaves your previous build serving. A migration it had already applied stays applied.