# Environment variables & secrets
URL: https://synquic.tech/docs/guides/environment-variables.md

Give your app its settings and secrets. See and edit them inline on the Environment page or with the CLI, keep the sensitive ones masked, and apply changes without a full rebuild.

import { Step, Steps } from "fumadocs-ui/components/steps";

Most apps need a few pieces of information to run: a database address, an API key, a "am I in production?"
flag. Those are **environment variables** — little named settings your app reads when it starts. Think of
them as sticky notes you hand your app: `DATABASE_URL = ...`, `STRIPE_KEY = ...`. Synq keeps those notes
for you, hands them to your app every time it builds and runs, and hides the sensitive ones so nobody can
read them over your shoulder.

<Callout title="What you need first">

- A project you've already created in Synq (if not, start with [Deploy from GitHub](/docs/guides/deploy-from-github)).
- The names and values you want to set — for example `DATABASE_URL` and its connection string.
- Nothing else. Synq encrypts and stores the values for you.

</Callout>

## Add or edit variables in the dashboard

<Steps>

<Step>

### Open the Environment page

Go to your project and choose **Environment**. The **Project variables** table is right there, with no
panel to open: every variable's name, its value, and a badge for the secret ones. Empty is fine; a
brand-new app often has none. The same table also appears on the **Configuration** tab.

</Step>

<Step>

### Add or change a variable

Press **Add variable**. A new row opens with two boxes: the **KEY** on the left (the name, like
`DATABASE_URL`) and the **value** on the right. Type both.

To change an existing row, press its **pencil icon**, edit the name or value, then press the check mark
when you're done. To remove a row, press its **trash icon**. Each row also has a **copy icon** that
copies its value to your clipboard.

</Step>

<Step>

### Mark the secret ones

Some values are sensitive: passwords, API keys, tokens. While a row is open for editing, press its
**key icon** to **mark it as secret**. A name that looks sensitive (`API_KEY`, `DB_PASSWORD`,
`STRIPE_SECRET`) starts out marked for you. A saved secret shows as dots (`••••••••`) instead of plain
text until someone reveals it.

<Callout title="Environment values are encrypted at rest" type="info">
  Whether or not you mark a variable secret, Synq **encrypts every value before saving it**.
  Marking a value secret adds the on-screen masking on top. It controls what people see in the
  dashboard, not whether the value is protected on disk.
</Callout>

Inline service environment values and build arguments are also encrypted, including their copies in
saved deployment snapshots and Compose drift baselines. Existing plaintext service configuration is
converted when the API or native engine starts after upgrading. Keep your `BETTER_AUTH_SECRET` with
your database backup: it is needed to read these values. To downgrade to a version from before this
protection, restore its matching database backup as well.

When moving to an installation with a different key, use **Settings → Data Transfer** with secrets
included. It re-encrypts the configuration for the destination. Transfers or recovery manifests that
omit secrets also omit inline environment, build arguments and protected service configuration; set
those values again before deploying.

</Step>

<Step>

### Or edit everything as a .env file

Press **Edit as .env** to see the whole table as one `KEY=value` document, handy for pasting a block
from another tool. Saved secrets appear as `••••••••`: leave those lines as they are to keep the saved
values, or type a new value to replace one. Press **Edit as table** to go back.

The editor refuses text it would otherwise drop without a word, and says why:

- a line that is not a `KEY=value` assignment is named by its **line number only**, so a pasted token
  or key never appears in the message;
- a mask with a dot added or removed by accident is refused, so it can't replace your secret with dots;
- a key written twice, or a new key holding the mask, asks you for a real value.

While the text has an error, **Save** and **Save and apply** stay disabled.

</Step>

<Step>

### Save, or save and apply

When you have unsaved changes, a bar appears under the table:

- **Save** stores the changes. Synq only writes the rows you actually added, changed, or deleted; every
  other variable is left exactly as it was. A masked secret you didn't touch is never overwritten,
  because it is never re-sent.
- **Save and apply** (shown once the project is deployed) saves, then restarts the running app with the
  new values, with no rebuild. See [Apply without a full rebuild](#apply-without-a-full-rebuild).
- **Discard** throws the changes away.

</Step>

</Steps>

<Callout title="Saving isn't the same as applying" type="warn">
  A saved change is stored right away, but the **running app keeps its old values until the next
  deploy**. After a plain **Save** on a live project, an **Apply without a rebuild** notice stays on the
  page until the change is applied or another deployment goes live. Otherwise the new values take
  effect the next time you deploy.
</Callout>

### Service variables

For a Compose stack or monorepo, the **Service variables** section below the project table shows one card
per service with everything that service receives at runtime. Each row carries a label for where its
value comes from:

- **Service override**: set on this service only;
- **Compose**: from the service's Compose file or template;
- **Shared project variable**: inherited from the project table above;
- **Service default**: generated by Synq (a port, say);
- **Linked**: written by a connection to another app, such as a database URL.

Editing an inherited value creates an override for that service; the other inherited values stay as they
are. Inherited values can only be removed where they are set, so the .env editor keeps them and tells you
which ones stayed.

Each card compares its values with the running container. A **Not applied** badge means the service still
runs older values: press **Apply environment changes** on the card to restart just that service with them.
Apply waits while a deployment started from this page is running, and the cards check again when it
finishes. Once every service is in sync, the project's apply notice clears on its own. **Open service**
takes you to the service's own Environment tab, which also recovers values that exist only in the running
container.

### Who can see and change what

Revealing or copying a secret needs permission to **edit** the project. Each reveal fetches only the
value you asked for and is recorded in the audit log by variable name, never by value. Members with
read-only access see names, plain values and masked secrets, with no reveal, copy of secrets, or
editing.

## When your app sees these values

Project-level variables are available at **two moments**:

- **During the build** — so a value baked in at build time (for example a `NEXT_PUBLIC_...` setting in a
  frontend) is there when the build runs.
- **At runtime** — so your running server can read them the normal way (like `process.env.DATABASE_URL`).

### Build-time values never enter the image

When Synq writes the Dockerfile for you (every framework build without a Dockerfile of its own), the
build runs with Docker BuildKit and your variables reach each build step as a **BuildKit secret** named
`synq-env`: a file mounted only while that step runs. No value is written into the Dockerfile, a layer or
the image history (`docker history --no-trunc`), and the generated Dockerfile itself is never copied into
the image. A short hash of the values is part of the step, so changing a variable still rebuilds.

If the build server has no BuildKit (the `docker buildx` plugin is missing, or the Docker engine is older
than 20.10), Synq falls back to Docker's legacy builder, which cannot mount secrets. The build log then
warns once that the variables are visible in that image's history; install `docker-buildx-plugin` on the
server to fix it.

### Build cache

Builds keep per-project dependency caches on the build server (npm, Yarn, pnpm, Bun, pip, uv, Poetry,
Bundler, Composer, Go, Cargo, Maven, Gradle, and the Next.js `.next/cache`), so a rebuild downloads only
what changed. Each cache belongs to one project; no other project can read or write it. For a single-app
Node project with a lockfile and no install scripts (or a Python project with a plain
`requirements.txt`), Synq also installs dependencies in their own layer before copying your code, so a
code-only push reuses the installed dependencies entirely. The build log says which applies and why.

**Clear build cache** in a project's advanced settings removes all unused build cache on that project's
server, including these dependency caches (Docker cannot remove one project's caches alone). A nightly
job keeps each server's build cache within about 10 GB.

For a Dockerfile build, declare every value you consume with `ARG` (and copy it into `ENV` only when the
running image also needs it):

```dockerfile
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
```

The project environment is the shared build/runtime layer. A multi-service project's `services[].env`
is different: it belongs only to that service at runtime and is never leaked into an image build. In
`synq.json`, add `services[].buildArgs` when an image needs an explicit build-only value or when you
want to document which shared key it inherits:

```json
{
  "env": { "NEXT_PUBLIC_API_URL": "https://api.example.com" },
  "services": [
    {
      "name": "web",
      "build": ".",
      "buildArgs": { "NEXT_PUBLIC_API_URL": null, "BUILD_FLAVOR": "production" }
    }
  ]
}
```

Here `null` means “inherit the same-named project variable”; a string is a per-image override. Values
declared in top-level `synq.json.env` become active project defaults automatically. A generic `.env`
file is only used after you choose **Import .env**.

<Callout title="Docker build arguments are not secret storage" type="warn">
  Build arguments can remain visible in image metadata or build cache. Prefer runtime environment
  variables for credentials. If a build genuinely needs a secret, use a dedicated BuildKit secret
  flow rather than committing it to `synq.json` or baking it into the image.
</Callout>

Your own Dockerfile can read the same variables as a secret instead. When the build uses BuildKit, Synq
also passes `--secret id=synq-env`, a file of `KEY='value'` lines you can load in any step:

```dockerfile
RUN --mount=type=secret,id=synq-env,required=false \
    set -a && . /run/secrets/synq-env && set +a && npm run build
```

Nothing about `ARG` changes; the secret is an additional, opt-in way in. A `RUN --mount` line makes
Synq build that Dockerfile with BuildKit.

## Built-in variables

Synq knows a few facts about every deployment. Write `$(NAME)` inside a variable's value and Synq fills
it in when it deploys, for the build and for the running app alike:

```bash
APP_URL=$(PUBLIC_URL)/api
SENTRY_RELEASE=$(PROJECT_NAME)@$(GIT_SHORT_SHA)
```

| Name             | Value                                                                                          |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `PROJECT_NAME`   | The project's slug.                                                                            |
| `PROJECT_ID`     | The project's ID.                                                                              |
| `SERVICE_NAME`   | The service's name in a Compose stack or monorepo. For a single app, the project's slug.       |
| `PRIMARY_DOMAIN` | The first custom domain of the app or service, otherwise its free subdomain. Empty if neither. |
| `PUBLIC_URL`     | `https://` followed by `PRIMARY_DOMAIN`. Empty if there is no domain.                          |
| `GIT_SHA`        | The full hash of the commit being deployed.                                                    |
| `GIT_SHORT_SHA`  | The first 7 characters of `GIT_SHA`.                                                           |
| `GIT_BRANCH`     | The branch being deployed.                                                                     |
| `DEPLOYMENT_ID`  | The ID of this deployment.                                                                     |

- Each service in a stack resolves its own `SERVICE_NAME`, `PRIMARY_DOMAIN` and `PUBLIC_URL`.
- A built-in with no value yet becomes an empty string: `GIT_SHA` when you deploy a prebuilt image, say.
- Any other `$(...)`, such as `$(date)`, is left exactly as written.
- Write `$$(NAME)` to keep the literal text `$(NAME)`.
- Values are filled in once. A value a built-in brings in is never scanned again.
- The Environment page always shows what you wrote; only the build and the container see the result.

Synq also sets some of these as variables of their own, but only where you haven't set the same name
yourself. Your value always wins.

- **During the build**: `GIT_SHA`, `GIT_BRANCH` and `DEPLOYMENT_ID`. They travel with the build
  variables (the `synq-env` secret, and build arguments for your own Dockerfile, so `ARG GIT_SHA` works).
  They never invalidate the build cache on their own, so a step Docker reuses from cache keeps the values
  of the build that first ran it. A generated Dockerfile built by the legacy-builder fallback, and the
  Nixpacks, Railpack and Buildpacks builders, don't receive them.
- **At runtime**: `SYNQ_PROJECT_NAME`, `SYNQ_PROJECT_ID`, `SYNQ_SERVICE_NAME`, `SYNQ_PRIMARY_DOMAIN`,
  `SYNQ_PUBLIC_URL`, `SYNQ_GIT_SHA`, `SYNQ_GIT_SHORT_SHA`, `SYNQ_GIT_BRANCH` and `SYNQ_DEPLOYMENT_ID`,
  each one only when it has a value.

## Service overrides and credential rotation

Open **Services → select a service → Environment** to see its effective saved
variables. The list includes Compose values, shared project variables and explicit
service overrides, with a source label on each row. Synq stores these values;
the list remains available when the deployment server is offline.

Editing an inherited value creates an override for that service. Saving changes
only the rows you edited; it does not copy the other inherited values into
overrides. Removing an override restores the inherited value, if one exists.
Remove inherited keys at their Compose or project source. Conflicting edits from
another session are rejected before anything is saved.

On a normal service deployment, the highest-priority value wins in this order:

1. A service-scoped variable from its environment editor.
2. A literal in that service's Compose `environment` or `synq.json` `env` map.
3. The project's environment, captured when the deployment starts.

Compose expressions such as `TOKEN: ${TOKEN}` resolve against the configured values;
they do not pin a previous scan's value. A rollback instead replays its captured
project values, including their precedence over present-day service overrides.

`synq project env set` warns when a changed or deleted key still has a service
override. The API and SDK return the same messages in `warnings`, and the same findings
as data in `overrides` (each service's `serviceName` and the `keys` it keeps). Deployment
logs name keys whose service values win. These messages contain names only. Saving the project
value succeeds, but it cannot replace a service-owned credential. Update the service
value, or remove its override to inherit the project value, then deploy. Deleting a
project variable does not delete a service's copy.

For example, if `web` pins `TOKEN=old`, setting the project's `TOKEN=new` will warn
about `web`; a rebuild alone still uses the pinned value. In the service editor,
remove that override before relying on the project's rotated token.

## Reveal, hide, and replace a secret

Each masked value has an **eye icon** to show or hide it, and **Show values** reveals every secret in the
table at once. Hiding a value again drops it from the page. Copying a secret fetches it just for the
clipboard and leaves the row masked.

While a saved secret's row is open for editing, its value box reads "Saved secret. Type a new value to
replace it". To change it, type the new value. To keep it, leave the box empty: Synq keeps the stored
value untouched.

## Production and preview values

Synq keeps variables **per environment**, so your test setup and your live setup can differ (a `preview`
build can point at a test database while `production` points at the real one). The three environments are
`production`, `preview`, and `development`.

The project editor manages **production** values. A service's Environment tab uses
its active deployment's environment, or production before the first deployment.
The CLI also supports `preview` and `development` through `--environment`.

These are variable sets within a project. Runtime isolation comes from selecting a separate
non-production project environment and deploying its project ID. Passing `--env preview` against
a production project is rejected; it cannot promote that preview onto the production runtime.
See [preview deployment](/docs/cli/deploy) for the complete flow.

## Download environment variables

In a service's **Environment** tab, choose **Download .env** beside **Upload .env**.
The file contains the listed variables and their real saved secret values. Downloading
does not reveal those secrets in the editor, save edits, or restart the service.
Unsaved edits shown in the editor are included in the download.

The file uses Compose-compatible `.env` quoting. **Upload .env** restores its literal
values, including dollar signs, quotes, and multiline values, without expanding variable references.

## Apply without a full rebuild

Rebuilding an app just to change a setting is slow: nothing about your _code_ changed. So once the project
is deployed, the project table offers **Save and apply**, and after a plain **Save** it shows an
**Apply (restart, no rebuild)** button.

Press either and Synq recreates the running containers from the images it already built, with the new
values injected. No git clone, no build, just a quick restart. It's the fastest way to roll out a changed
key or a rotated secret. The notice shows its progress until the restart finishes.

From the terminal, `synq deploy --refresh` (shown below) applies environment
changes through a deployment without rebuilding.

For one service, open its **Environment** tab, press **Save**, then
**Apply environment changes** in the toolbar above the variables. It refreshes only
that service using the image already running on its target. The editor stays open
and shows completion after the service starts. This is a dedicated service action
(`POST /api/projects/:id/services/:serviceId/apply-env`); it creates no deployment or
build session. Docker gets a graceful stop, and the replacement retains the existing
ports, project network address, volumes, commands, and resource limits. If startup
fails, Synq restores the previous container and leaves the changes pending.
The toolbar offers Apply only when a comparison with the deployed container finds
saved changes, including removed variables. **Check running service** refreshes
that comparison. An unavailable server is reported as an unknown state, while the
saved variables remain editable.

For an older or migrated service, Docker may still hold variables that were never
saved in Synq. **Recover from running service** reads those missing values
from that service's container, excluding image defaults, and adds them to the
editor for review. Press **Save** to store them encrypted in Synq. Recovery
never overwrites an already saved value. Before applying, recover these values or
explicitly choose to remove them from the running service.

This action supports Docker services on a server or an Synq Cloud Docker
workspace. Legacy cloud workspaces and adopted containers on Docker's default bridge
require **Redeploy**. Namespace-sharing services must be refreshed together through
a deployment.

A normal **Restart** keeps the container's existing variables. When saved changes are
pending, Synq opens the service's **Environment** panel so you can apply them there.
Variables used during a build still require a rebuild.

## CLI

Everything above works from the CLI. Secret values always come back **masked** when you list them.

<Tabs items={['Whole project', 'One service in a stack']}>

<Tab value="Whole project">

```bash
# List a project's variables (secrets show masked)
synq project env get <project-id>

# Set one or more values (repeat --set for each)
synq project env set <project-id> --set DATABASE_URL=postgres://... --set LOG_LEVEL=info

# Mark the values in this command as secret
synq project env set <project-id> --set STRIPE_KEY=sk_live_... --secret

# Remove a variable
synq project env set <project-id> --unset OLD_FLAG

# Target a different environment (defaults to production)
synq project env set <project-id> --environment preview --set API_URL=https://staging.example.com

# Push the current env to the running app — no git pull, no rebuild
synq deploy --refresh
```

`env set` **merges**: it only touches the keys you name with `--set` and `--unset`, leaving everything else
in place.

</Tab>

<Tab value="One service in a stack">

For a multi-service (compose) project, each service has its own variables. Target the stack with
`-p` and the service by name:

```bash
# List a service's variables (secrets masked)
synq service env get web -p my-stack

# Set values for a service (KEY=VALUE pairs)
synq service env set web -p my-stack DATABASE_URL=postgres://... --secret

# Pick the environment (defaults to production)
synq service env set web -p my-stack -e preview API_URL=https://staging.example.com
```

<Callout title="Secrets and service env" type="warn">
  The service command writes the whole environment at once, so it first reads the current values and
  merges yours in. Because saved secret values can't be read back, if the service already has a
  secret you didn't re-type, the command stops and asks you to re-specify it — or pass `--replace`
  to drop every value that isn't in your command.
</Callout>

</Tab>

</Tabs>

## Troubleshooting

<Callout title="I saved a variable but my app still doesn't see it" type="warn">
  Saving stores the value; the **running app** only picks it up on a deploy. Use **Save and apply**,
  **Apply (restart, no rebuild)** or a service card's **Apply environment changes** (or
  `synq deploy --refresh`) to push it out now, or just redeploy.
</Callout>

<Callout title="A secret shows as dots and I can't read it back" type="info">
  Secrets stay masked until you reveal them. On the **Environment** page, press the eye or copy icon
  on its row; that needs permission to edit the project, and each reveal is recorded in the audit log.
  The CLI always lists secrets masked. If you've lost the original value, set a fresh one.
</Callout>

<Callout title="The build failed with a missing-variable error" type="error">
  Your app expected a variable that isn't set yet. Add it on the **Environment** page (or with
  `synq project env set`), make sure the name matches exactly (env names are case-sensitive),
  then deploy again. See [Troubleshooting → Deployments](/docs/troubleshooting/deployments) for
  more.
</Callout>

## Related guides

<Cards>
  <Card
    title="Deploy from GitHub"
    href="/docs/guides/deploy-from-github"
    description="Connect a repo and put your app online."
  />
  <Card
    title="Add a custom domain"
    href="/docs/guides/custom-domains"
    description="Put your app on your own address with automatic HTTPS."
  />
  <Card
    title="Multi-service apps"
    href="/docs/guides/compose-multi-service"
    description="Give each service in a docker-compose stack its own variables."
  />
</Cards>
