Skip to content

Config Files

Config files let you store, version, and sync configuration to your servers via SSH — from .env files and Nginx configs to SSL certificates and Docker Compose templates.

Create a config file and deploy it to a server in under a minute:

  1. Go to Configuration > Config Files in the sidebar.
  2. Click Add Config File.
  3. Enter a name (“App API .env”), filename (.env), and paste your content.
  4. Click Create.
  5. Go to the service that needs this file, click Attach File, select the config file, and enter the target path (e.g., /opt/app/.env).
  6. Click Sync Files on the service to write the file to the server.

Config files exist at the environment level and are deployed to servers through service attachments. The flow is: create, attach, sync.

flowchart LR
    CF[Config File<br/>'.env' template<br/>with secret placeholders] --> A1[Attach to<br/>Service A<br/>path: /opt/app-a/.env]
    CF --> A2[Attach to<br/>Service B<br/>path: /opt/app-b/.env]

    A1 --> Sync1[Sync via SSH<br/>Resolve secrets<br/>Write to server-1]
    A2 --> Sync2[Sync via SSH<br/>Resolve secrets<br/>Write to server-2]

Key concepts:

  • Environment-scoped. Config files belong to an environment and can be attached to any service within that environment.
  • Template-based. Text files support ${SECRET_KEY} placeholders that are resolved at sync time with actual secret values.
  • Version-tracked. Every content edit creates a history entry. You can restore any previous version.
  • Sync-aware. Each service-file attachment tracks when it was last synced. Editing a file after syncing shows a “pending” status so you know which servers need updating.

  1. Navigate to Configuration > Config Files.
  2. Click Add Config File.
  3. Fill in the form:
FieldRequiredDescription
NameYesDisplay name (e.g., “App API .env”). Must be unique per environment.
FilenameYesTarget filename on the server (e.g., .env, nginx.conf, docker-compose.yml)
ContentYesFile content. Use ${SECRET_KEY} for secret placeholders.
LanguageNoSyntax-highlighting hint for the editor. Auto-detected from Filename on save when omitted (e.g., *.ymlyaml, Dockerfiledockerfile, Caddyfilenginx). Override via the Language dropdown next to the editor. Stored on the row and reused for the read-only viewer and history diffs.
DescriptionNoOptional documentation
  1. Click Create.

The content editor uses CodeMirror with syntax highlighting. Supported language values: plaintext, yaml, json, env, toml, ini, conf, sh, dockerfile, nginx, sql. Unknown filenames fall back to plaintext.

POST /api/environments/:envId/config-files
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "App API .env",
"filename": ".env",
"content": "DATABASE_URL=${DATABASE_URL}\nREDIS_URL=${REDIS_URL}\nDEBUG=false",
"description": "Environment variables for the API service"
}

For certificates, compiled configs, and other binary files:

  1. Click Upload Asset on the Config Files page.
  2. Select the file from your computer.
  3. Enter a display name and filename.

Binary files are stored as base64 in the database and synced to servers via SFTP. They do not support secret placeholder substitution.

API (multipart upload):

POST /api/environments/:envId/asset-files/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data
name: "Cloudflare Origin Cert"
filename: "cloudflare-origin.pem"
file: <binary file data>

Binary files cannot be edited in the text editor. To replace the content of an existing binary file in place (keeping its service attachments, sync assignments, and history):

  1. Click Edit on the binary file.
  2. Choose a replacement file under Replace file.
  3. Click Save Changes. The previous content is kept in history for rollback.

API (multipart upload):

POST /api/config-files/:id/replace-asset
Authorization: Bearer <token>
Content-Type: multipart/form-data
file: <binary file data>

The file’s mimeType and fileSize are updated from the uploaded file. Requires the operator role.

PATCH /api/config-files/:id rejects an empty content value for binary files — since binary content is stripped from API responses, round-tripping a fetched file through PATCH would otherwise wipe the stored payload. Use the replace endpoint above to change binary content.


Config files are deployed through service attachments. One config file can be attached to multiple services across different servers, each with a different target path.

UI: Go to the service detail page > Config Files section > Attach File.

API:

POST /api/services/:serviceId/files
Authorization: Bearer <token>
Content-Type: application/json
{
"configFileId": "clxyz...",
"targetPath": "/opt/app/.env"
}

The targetPath is the absolute path on the server where the file will be written during sync. BRIDGEPORT creates parent directories automatically if they do not exist.

DELETE /api/services/:serviceId/files/:configFileId
Authorization: Bearer <token>

Detaching a file removes the link but does not delete the file from the server where it was previously synced.

PATCH /api/services/:serviceId/files/:configFileId
Authorization: Bearer <token>
Content-Type: application/json
{
"targetPath": "/opt/app/config/.env"
}

Syncing writes config files to their target paths on the server via SSH. There are three sync scopes:

Syncs all files attached to a single service:

UI: Service detail page > Sync Files button.

API:

POST /api/services/:serviceId/sync-files
Authorization: Bearer <token>

Syncs all config files for all services on a server in a single SSH connection:

UI: Server detail page > Sync All Files button.

API:

POST /api/servers/:serverId/sync-all-files
Authorization: Bearer <token>

Syncs a specific config file to every service it is attached to. BRIDGEPORT groups by server to minimize SSH connections:

UI: Config file detail page > Sync to All button.

API:

POST /api/config-files/:configFileId/sync-all
Authorization: Bearer <token>

All three sync endpoints accept ?dryRun=true (or X-Dry-Run: true) to preview what a real sync would write without touching the host file or updating lastSyncedAt. The dry-run opens a read-only SSH session, runs cat <hostPath> to capture the current contents, and returns a unified diff against the rendered (redacted) content per target.

POST /api/config-files/:id/sync-all?dryRun=true
Authorization: Bearer <token>

Response shape:

{
"dryRun": true,
"results": [
{
"serverName": "web-1",
"serviceName": "api",
"configFileName": "app.env",
"hostPath": "/etc/api/app.env",
"diff": "--- a/etc/api/app.env\n+++ b/etc/api/app.env\n@@ -1,2 +1,2 @@\n-OLD=value\n+NEW=value",
"exists": true,
"referencingServices": ["api", "worker"],
"warnings": []
}
]
}
  • diff is a unified diff string (empty when the rendered content matches the host file).
  • exists is false if the file does not yet exist on the host — diff then shows the full rendered content as additions.
  • Secret VALUES in the rendered content are replaced with ***. ${KEY} placeholders that resolve to a secret are substituted then redacted.
  • Binary files are not diffed; they report an empty diff with a warning.
  • When the live sync path would have refused this target (missing secrets, template errors), the response carries an "error" string and omits the diff. The live path returns success: false, error: '...' for the same conditions — the dry-run mirrors that so callers do not render a green preview for a sync that would be rejected.
  • The dry-run writes an audit-log entry with details.dryRun = true. The same flag works on POST /api/services/:id/sync-files.
sequenceDiagram
    participant BP as BRIDGEPORT
    participant SSH as Server (SSH)

    BP->>SSH: Connect via SSH
    loop For each attached file
        BP->>BP: Resolve ${KEY} placeholders<br/>with environment secrets
        alt Missing secrets
            BP-->>BP: Skip file, record error
        else All resolved
            BP->>SSH: mkdir -p /target/directory
            alt Text file
                BP->>SSH: Write resolved content<br/>via heredoc
            else Binary file
                BP->>SSH: Write via SFTP
            end
            BP->>BP: Update lastSyncedAt
        end
    end
    BP->>SSH: Disconnect

Sync result envelope:

All three sync endpoints return the same envelope. Branch on status, not success — the latter is a deprecated alias kept for one release.

{
"status": "ok",
"targetsAttempted": 2,
"targetsSucceeded": 2,
"targetsFailed": 0,
"results": [
{ "file": "App API .env", "targetPath": "/opt/app/.env", "success": true },
{ "file": "Nginx Config", "targetPath": "/etc/nginx/conf.d/app.conf", "success": true }
],
"success": true
}

status is one of:

StatusMeaning
okEvery target succeeded.
no_targetsZero targets — the config file isn’t attached to any service, or the server/service has nothing to sync. Returned as HTTP 200 with targetsAttempted: 0 so the UI can render a warning instead of a red error.
partialAt least one target succeeded and at least one failed.
failedEvery target failed.

When a single change touches multiple config files (a coordinated certificate rotation, an env-wide TLS switch, a redeploy that needs three compose files updated together), use the batch sync endpoint to apply them as a single transactional unit.

A batch is single-environment scope — every config file referenced by the batch must live in the same BRIDGEPORT environment. v1 supports config-file-sync operations only; other op types are rejected with VALIDATION_ERROR.

POST /api/sync/batch
Authorization: Bearer <token>
Idempotency-Key: <optional opaque string>
Content-Type: application/json
{
"operations": [
{ "type": "config-file-sync", "configFileId": "ck_abc1" },
{ "type": "config-file-sync", "configFileId": "ck_abc2" }
],
"rollbackOnFailure": true
}
{
"batchId": "ck_batch_xyz",
"status": "ok",
"operations": [
{ "index": 0, "status": "ok" },
{ "index": 1, "status": "ok" }
]
}

Branch on status, not on the per-op array length:

Batch statusMeaning
okEvery op succeeded.
partialAt least one op succeeded and at least one failed. With rollbackOnFailure: true, this means some rollbacks themselves failed and the environment may be inconsistent — investigate.
rolled_backAn op failed and all previously successful ops were successfully reverted. The environment is back where it started.
failedEvery attempted op failed (or rollbackOnFailure: true and the very first op failed).

Per-op status is one of: ok, failed, skipped (didn’t run because an earlier op failed in rollbackOnFailure: true mode), rolled_back (was reverted), rollback_failed (revert attempt itself failed — manual intervention may be required).

With rollbackOnFailure: true:

  1. BRIDGEPORT snapshots ConfigFile.content before each op runs.
  2. On the first op failure, the forward loop stops; remaining ops are marked skipped.
  3. Already-successful ops are walked back in reverse order: prior content is restored to the database, then re-synced to the same servers.
  4. If every revert succeeds, the batch ends as rolled_back. If any revert fails, the batch ends as partial and the failing op carries a rollback_failed status.

With rollbackOnFailure: false (best-effort):

  • Every op is attempted regardless of earlier failures.
  • The batch ends as ok (all succeeded), partial (mixed), or failed (all failed).

Pass an Idempotency-Key header on retries to make the call safe to repeat:

  • Same key + same canonicalized body → returns the original batch result without re-executing.
  • Same key + different body → returns HTTP 409 with code: "IDEMPOTENCY_KEY_REUSED".

The canonicalization sorts JSON object keys recursively before hashing, so whitespace and key ordering don’t affect the dedupe.

GET /api/sync/batch/:batchId
Authorization: Bearer <token>

Returns the same payload shape as the POST response. Audit-log entries written by the batch carry a details.batchId field so you can correlate individual file syncs back to their batch.


Text config files support ${KEY} placeholders that are resolved at sync time against both secrets and variables from the same environment. See Secrets and Variables > Using Placeholders in Config Files for the full details.

Quick summary:

  • Use ${KEY} syntax in your config file content.
  • Placeholders resolve against vars first, then secrets; if the same key exists as both, the secret wins.
  • If any referenced key is missing (not a secret and not a var), the sync fails for that file with an error listing the missing keys.
  • The stored config file always contains the placeholder, never the actual value.
  • A Config File Scanner can detect hardcoded values across your config files and offer to promote them to secrets or vars.
# Template stored in BRIDGEPORT:
DATABASE_URL=${DATABASE_URL}
SECRET_KEY=${DJANGO_SECRET_KEY}
DEBUG=false
# File written to server after sync:
DATABASE_URL=postgres://user:pass@db:5432/app
SECRET_KEY=django-insecure-abc123
DEBUG=false

In addition to ${KEY} substitution, config files can enumerate the servers in an environment using a Go-style range block. This is useful for generating reverse-proxy upstreams, cluster member lists, or any output where the body repeats once per matching server.

{{range servers <filter>="<value>" [<filter>="<value>"]...}}<body>{{end}}
  • <body> is rendered once per matching server.
  • Inside the body, {{.field}} interpolates a per-server attribute.
  • The empty set renders to an empty string — no error is raised.
  • Servers are emitted in a stable alphabetical order by name so the rendered output is deterministic (this matters because it feeds into SHA-256 checksums used for deployment-artifact change detection).
FilterDescription
tag="web"Server’s tags array contains the exact value web.
name="api-*"Glob match against server name. Supports * (any chars) and ? (single char).
environment="staging"Match servers in another environment by name or id. Defaults to the config file’s environment when omitted.

Filters combine with logical AND.

FieldSource
.nameServer name (e.g., api-1).
.hostnameServer hostname / private address.
.privateIpAlias of .hostname.
.publicIpReserved public IP, or empty string if not set.
.idInternal server id.
.tagsComma-joined tag list (e.g., web,api).

Referencing an unknown field (e.g., {{.bogus}}) emits empty and records a template error during sync.

api.example.com {
reverse_proxy {{range servers tag="web"}}{{.privateIp}}:8000 {{end}}
}

After sync (with two web-tagged servers api-1 and api-2):

api.example.com {
reverse_proxy 10.0.0.1:8000 10.0.0.2:8000
}
# Servers iterated, then ${CLUSTER_SECRET} substituted in stage 2.
peers:
{{range servers tag="cluster"}} - id: {{.id}}
addr: {{.privateIp}}:7000
name: {{.name}}
{{end}}cluster_secret: ${CLUSTER_SECRET}
  • No nesting. A {{range}} block cannot contain another {{range}} block; this is reported as a template error during sync.
  • {{range}} and {{end}} are the only interpreted directives. Any other {{...}} content passes through verbatim, so existing literal usages are unaffected.
  • Unclosed {{range}} blocks are reported as a template error and the unterminated content is left in place.

Long .env and config files often repeat — most lines (DB URL, Redis URL, log config, Sentry DSN, …) are identical across services, with only a few service-specific keys at the end. Fragments are named, reusable text blocks that live at the environment level and get concatenated into ConfigFiles at deploy / sync time.

  • Env-scoped. A fragment belongs to one environment. Names must be unique per env.
  • Flat. A fragment cannot include another fragment — by construction, there is no field to do so. No cycle detection needed.
  • Last-definition-wins. Fragment content is prepended; the ConfigFile’s own content is appended last. Duplicate keys naturally resolve to the last occurrence — so a service-specific LOG_LEVEL=debug in the ConfigFile overrides a shared LOG_LEVEL=info in a fragment without any parser changes.
  • Render hygiene. When the ConfigFile’s language uses # comments (env, yaml, toml, ini, sh, dockerfile, conf, …), BRIDGEPORT injects a # === fragment: <name> === header before each fragment and a # === service-specific === header before the ConfigFile’s own content. For formats that don’t use # (json, xml, html), headers are skipped and the sections are concatenated directly. ConfigFiles with no fragments render byte-for-byte unchanged from before fragments existed.

Navigate to Configuration > Fragments in the sidebar.

  • Create a fragment with a name, optional description, and content. Content can reference ${KEY} placeholders — they’re resolved against the same environment-level secrets and vars at sync time.
  • View (eye icon) opens a read-only modal showing the fragment’s name, description, content, and usage. An Edit button inside switches into the editable modal.
  • Edit a fragment to update its content. If the fragment is included by any ConfigFile with Auto Re-sync enabled, an auto-resync is triggered for those files (same pipeline as the secret/var auto-resync).
  • Used by in the list is a clickable count: expand it to drill into the ConfigFiles that include the fragment and the services those files are attached to, with links to each. Fragments with no usage show an unobtrusive .
  • Delete is blocked when a fragment is in use. The API returns a 409 with an inUseBy array listing the ConfigFiles (and their attached services) that still reference the fragment.

In the ConfigFile create / edit modal, the Included Fragments section lets you pick fragments to prepend. Use the up/down arrows to reorder — the position determines render order, and the last definition of a key wins.

Click Preview in the edit modal to see the rendered output: fragments concatenated in order, headers injected, and ${KEY} placeholders resolved.

The read-only ConfigFile view modal also lists the included fragments (in position order), so you can see what a file pulls in without entering edit mode.

POST /api/environments/:envId/config-fragments
GET /api/environments/:envId/config-fragments
GET /api/config-fragments/:id
PATCH /api/config-fragments/:id
DELETE /api/config-fragments/:id
POST /api/config-files/:id/preview # Render fragments + content + placeholders

Include fragments by passing fragmentIds: string[] on POST /api/environments/:envId/config-files or PATCH /api/config-files/:id. Array order = render order. Sending an empty array on PATCH clears all includes; omitting the field leaves them unchanged.


Every content edit to a config file creates a history entry with the previous content, who made the edit, and when.

UI: Config file detail page > History tab.

API:

GET /api/config-files/:id/history
Authorization: Bearer <token>

Returns up to 50 history entries, newest first:

{
"history": [
{
"id": "hist1",
"editedAt": "2026-02-25T09:00:00.000Z",
"editedBy": { "id": "usr1", "email": "admin@example.com", "name": "Admin" }
}
]
}
  1. Open the history for a config file.
  2. Select the version to restore.
  3. Click Restore.
POST /api/config-files/:id/restore/:historyId
Authorization: Bearer <token>

Before restoring, the current content is saved as a new history entry so you can always undo a restore. After restoring, the config file’s updatedAt changes, which means all synced services will show a “pending” sync status.


Each file-to-service attachment tracks its sync status based on timestamps:

StatusMeaning
SyncedThe file was synced after the last content edit — the server has the latest version
PendingThe file was edited after the last sync — the server has an outdated version
NeverThe file has never been synced to this service
Not AttachedThe config file is not attached to any service

The config files list page shows the aggregate sync status across all attachments:

  • If all attachments are synced: Synced
  • If any attachment is pending or never synced: Pending
  • If no attachments exist: Not Attached

Individual attachment statuses are visible on the config file detail page and on the server’s config file status view (GET /api/servers/:serverId/config-files-status).


When a secret or variable is updated (via PATCH /api/secrets/:id or PATCH /api/vars/:id), BRIDGEPORT can automatically re-sync any config file that references it via ${KEY}. This keeps deployed files in sync with their resolved values without a manual sync step.

How it works:

  1. After a secret/var PATCH succeeds and the value actually changes (metadata-only edits do not trigger it), BRIDGEPORT scans the environment for text config files where autoResync = true and content contains the literal ${KEY}.
  2. Each matching config file is synced to all its attached services — once per config file, regardless of how many times ${KEY} appears.
  3. Each triggered sync writes an audit log with details.autoTriggered = true and details.triggeredBy = "var:<KEY>:patch" (or secret:<KEY>:patch).
  4. The trigger is fire-and-forget: it runs in the background after the PATCH response is sent, and a single failing host does not abort the rest.

Scope and limits:

  • Only text files are considered (binary files don’t get placeholder substitution).
  • Only files with autoResync = true are considered (the default for new files).
  • Creating/deleting a secret or variable does not trigger auto-resync (only PATCH does, and only when the value changes).
  • The full file is re-synced; BRIDGEPORT does not do partial substitution.

Disabling auto re-sync per file:

Set autoResync to false in the create or update payload (or uncheck the toggle in the UI) for any file that you want to keep manually controlled.

PATCH /api/config-files/:id
{
"autoResync": false
}

The most common use case. Store environment variables with secret placeholders:

NODE_ENV=production
DATABASE_URL=${DATABASE_URL}
REDIS_URL=${REDIS_URL}
JWT_SECRET=${JWT_SECRET}
PORT=3000

Attach to your application service at /opt/app/.env.

server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

Attach to your Nginx service at /etc/nginx/conf.d/api.conf.

services:
app:
image: registry.example.com/app:${APP_TAG}
env_file: .env
ports:
- "3000:3000"
restart: unless-stopped

Attach to the service at /opt/app/docker-compose.yml.

Upload a certificate file (.pem, .crt, .key) as a binary asset. Attach it to services that need TLS termination, with target paths like /etc/ssl/certs/origin.pem.

Any text-based configuration file can be managed through BRIDGEPORT. Sync crontab files, systemd service units, or application-specific configs.


FieldTypeDefaultDescription
namestringDisplay name (unique per environment)
filenamestringTarget filename on server
contentstringFile content (base64-encoded for binary files)
descriptionstringnullOptional documentation
isBinarybooleanfalseWhether this is a binary/asset file
mimeTypestringnullMIME type for binary files
fileSizeintegernullSize in bytes (for binary files)
autoResyncbooleantrueAuto re-sync attached services when a referenced ${KEY} secret or var changes (see Auto Re-sync on Value Change)
languagestringplaintextSyntax-highlighting hint (yaml, json, env, toml, ini, conf, sh, dockerfile, nginx, sql, plaintext). Auto-detected from filename on create when omitted.
FieldTypeDescription
targetPathstringAbsolute path on the server where the file is written
lastSyncedAtdatetimeWhen the file was last successfully synced to this service
ActionMinimum Role
View config filesViewer
Create, edit, delete config filesOperator
Attach/detach files to servicesOperator
Sync files to serversOperator

“Missing secrets: KEY1, KEY2” during sync The config file references secrets that do not exist in this environment. Create the missing secrets at Configuration > Secrets, then retry the sync.

“Template errors: …” during sync The config file uses {{range servers ...}} syntax but the template is malformed (unknown filter, unknown field, nested range, unclosed range, etc.). The error message lists the specific issue(s). See Iterating Over Servers for the supported syntax.

“Failed to write file” during sync The SSH connection succeeded but writing to the target path failed. Common causes:

  • The SSH user does not have write permissions to the target directory.
  • The disk is full on the target server.
  • The target path contains invalid characters.

Check the error details in the sync results for the specific failure message.

“Connection failed” during sync BRIDGEPORT could not establish an SSH connection to the server. Verify:

  • The server’s hostname is reachable from BRIDGEPORT.
  • The environment’s SSH key is configured and valid.
  • The SSH user has access to the server.

Sync shows “success” but file content is wrong If ${KEY} placeholders appear as literal text in the synced file, the secret either does not exist or is named differently than expected. Check the exact key name — it is case-sensitive and must match ^[A-Z][A-Z0-9_]*$.

Config file edit does not update the server Editing a config file in BRIDGEPORT does not automatically sync to servers. After editing, check the sync status (it should show “Pending”) and trigger a sync manually.

“Config file with this name already exists” Config file names are unique per environment. Choose a different name, or find and edit the existing file.

Binary file content is empty in API response This is by design. Binary file content is stripped from API responses to keep payloads small. The file is still stored and will be synced correctly. Download binary content by restoring from history if needed.


  • Secrets — Managing ${KEY} placeholders used in config file templates
  • Services — Attaching and syncing config files per service
  • Servers — Syncing all config files for all services on a server
  • Environments — Config files are scoped per environment