Guides

Environment variables & secrets

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.

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.

What you need first

  • A project you've already created in Synq (if not, start with 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.

Add or edit variables in the dashboard

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.

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.

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.

Environment values are encrypted at rest

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.

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.

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.

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.
  • Discard throws the changes away.

Saving isn't the same as applying

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.

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

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:

{
  "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.

Docker build arguments are not secret storage

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.

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:

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:

APP_URL=$(PUBLIC_URL)/api
SENTRY_RELEASE=$(PROJECT_NAME)@$(GIT_SHORT_SHA)
NameValue
PROJECT_NAMEThe project's slug.
PROJECT_IDThe project's ID.
SERVICE_NAMEThe service's name in a Compose stack or monorepo. For a single app, the project's slug.
PRIMARY_DOMAINThe first custom domain of the app or service, otherwise its free subdomain. Empty if neither.
PUBLIC_URLhttps:// followed by PRIMARY_DOMAIN. Empty if there is no domain.
GIT_SHAThe full hash of the commit being deployed.
GIT_SHORT_SHAThe first 7 characters of GIT_SHA.
GIT_BRANCHThe branch being deployed.
DEPLOYMENT_IDThe 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 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.

# 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.

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

# 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

Secrets and service env

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.

Troubleshooting

I saved a variable but my app still doesn't see it

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.

A secret shows as dots and I can't read it back

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.

The build failed with a missing-variable 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 for more.

On this page