Skip to content

Services

A service in BRIDGEPORT is an environment-scoped template that describes what to run (image, tag, health checks, compose template, base env). Where it actually runs is captured by one or more service deployments, each pinning the template to a specific server with its own container name, env overrides, and runtime status.

The Services list showing running, healthy services with their image tags, deployments, and exposed ports

Get a service deployed in 3 steps:

  1. Discover or create a service on your server:

    • Server detail page > Discover to auto-import running containers, or
    • Server detail page > Create Service to set one up manually.
  2. Deploy a tag:

    • On the service detail page, select a tag from the dropdown and click Deploy.
  3. Check health:

    • Click the health check button to verify the container is running.

A service has two layers:

  1. Service template — environment-scoped. Holds the image, tag, compose template, health checks, base env, and deploy strategy (sequential or parallel). One template can have many deployments.
  2. Service deployment — one row per (template, server). Holds the container name, env overrides, runtime status (status, containerStatus, healthStatus, discoveryStatus), exposed ports, and last-checked / last-discovered timestamps.

Every template is linked to a ContainerImage. The container image is the central entity that tracks the image name, available tags from the registry, and deployment history. One container image can be linked to many templates.

flowchart LR
    CI[ContainerImage<br>myapp:latest] --> S[Service template: app-web<br>imageTag: v2.1.0]
    S --> D1[ServiceDeployment<br>Server: web-prod-1<br>container: app-web]
    S --> D2[ServiceDeployment<br>Server: web-prod-2<br>container: app-web]
    S --> D3[ServiceDeployment<br>Server: web-prod-3<br>container: app-web]
    REG[Registry] -->|tag updates| CI

    style CI fill:#4f46e5,color:#fff
    style S fill:#0ea5e9,color:#fff
    style REG fill:#374151,color:#fff

When you deploy a tag to a service template, BRIDGEPORT:

  1. Looks at the template’s deployStrategy (sequential or parallel) and the list of serviceDeployments.
  2. For each deployment (one per server), creates a Deployment record (status: pending).
  3. Generates deployment artifacts (compose file, config files) per deployment if applicable.
  4. Pulls the new image on each target server.
  5. Runs docker compose up (if compose path is set) or restarts the container, in sequence or in parallel depending on the strategy.
  6. Verifies each container is running.
  7. Records success or failure per deployment in deployment history and container image tag history.

Sequential vs parallel:

  • sequential (default) — one deployment at a time. Useful when downstream services depend on a hot single instance, or when you want to bail out early on the first failure.
  • parallel — all deployments at once. Faster for stateless replicas behind a load balancer.

There are two ways to create services: auto-discovery and manual creation.

Discovery scans a server’s Docker daemon and creates services for every running container. See Servers > Container Discovery for details.

Each discovered container gets:

  • A service record linked to the server
  • A ContainerImage record (created if no matching image exists)
  • Current tag and container name populated from the running container

Create a service template before deploying its container, or for containers that are not yet running.

Recommended (template-only) API:

POST /api/environments/:envId/services
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "app-api",
"containerImageId": "climg...",
"imageTag": "v2.1.0",
"composeTemplate": null,
"deployStrategy": "sequential"
}

This creates the template only — with zero deployments. Attach servers afterwards by creating ServiceDeployment rows via the deployment endpoints.

Legacy (create template + first deployment in one call):

POST /api/servers/:serverId/services
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "app-api",
"containerName": "app-api",
"containerImageId": "climg...",
"imageTag": "v2.1.0"
}

This endpoint is preserved for the CLI and pre-2.0 UI flows. It creates the env-scoped Service plus a single ServiceDeployment bound to the specified server.

Required template fields:

  • name — service display name (unique per environment). Free-form — rename anytime without breaking discovery.
  • containerImageId — ID of an existing ContainerImage in the same environment.

Required deployment fields (for legacy server-scoped create):

  • containerName — Docker container name (unique per server). Discovery matches deployments to running containers via this field, so it must match the container_name: value in your compose file.

Optional fields:

  • imageTag — defaults to latest.
  • serviceTypeId — links the service to a plugin-provided Service Type (managed under /admin/service-types). Drives the grouping/filter chips on the Services list page.
  • composeTemplate — template Compose file (the rendered file is uploaded per-deployment to each server’s compose path).
  • deployStrategysequential (default) or parallel; controls how multi-server deploys roll out.
  • baseEnv — JSON object of env vars applied to every deployment. Per-deployment env overrides take precedence.

UI: On the service detail page, select a tag from the dropdown and click Deploy.

API:

POST /api/services/:id/deploy
Authorization: Bearer <token>
Content-Type: application/json
{
"imageTag": "v2.2.0",
"generateArtifacts": true,
"pullImage": true
}

Parameters:

FieldTypeDefaultDescription
imageTagstringCurrent tagTag to deploy
generateArtifactsbooleantrueGenerate and upload compose/config files
pullImagebooleantruePull the image before deploying

What happens during deploy:

  1. A Deployment record is created with status pending, then updated to deploying.
  2. BRIDGEPORT connects to the server (SSH or socket mode).
  3. If generateArtifacts is true:
    • Creates the deploy directory (/opt/<service-name>/ or the compose path’s directory).
    • Generates and uploads the compose file (auto-generated or from custom template).
    • Uploads attached config files to their target paths.
    • Saves artifacts to the database.
  4. If pullImage is true, pulls the image (docker pull <image>:<tag>).
  5. Runs docker compose up (compose mode) or docker restart (direct mode).
  6. Verifies the container is running.
  7. Appends container output (docker logs <container>, last defaultLogLines lines with timestamps) to the deployment log. This runs on both the success and failure paths so a container that crashes immediately surfaces its internal error in the deployment plan view without needing SSH access. If the container does not exist (for example, docker compose up failed before creating it), a --- container logs unavailable: <reason> --- note is appended instead.
  8. Updates the deployment record to success or failed with logs and duration.

Response:

{
"deployment": {
"id": "cldep...",
"imageTag": "v2.2.0",
"previousTag": "v2.1.0",
"status": "success",
"logs": "[2026-02-25T10:00:00Z] Starting deployment...\n...",
"durationMs": 12340,
"startedAt": "2026-02-25T10:00:00.000Z",
"completedAt": "2026-02-25T10:00:12.340Z"
},
"logs": "...",
"previousTag": "v2.1.0"
}

Add ?dryRun=true to the query string (or send X-Dry-Run: true as a header) on POST /api/services/:id/deployments/:depId/deploy to preview what a real deploy would do without creating a Deployment row, pulling the image, opening an SSH write session, or running docker compose up. The same flag works on POST /api/services/:id/sync-files and the deployment-plan / config-file sync endpoints documented below.

POST /api/services/:id/deployments/:depId/deploy?dryRun=true
Authorization: Bearer <token>

Response shape:

{
"dryRun": true,
"serviceId": "csrv...",
"serviceDeploymentId": "csdp...",
"serverName": "web-1",
"imageTag": "v2.2.0",
"imageDigest": "sha256:abc123...",
"composeContent": "services:\n api:\n image: registry.example.com/api:v2.2.0\n ...",
"env": { "PORT": "3000", "DB_PASSWORD": "***" },
"containerAction": "cycle",
"warnings": []
}
  • imageDigest is resolved from the registry manifest (no docker pull). null when no registry connection exists or the manifest fetch failed — a warning is added explaining why.
  • containerAction is "start" (no container running), "cycle" (running, would be recreated by compose up), or "no-op".
  • Secret VALUES are replaced with *** in both composeContent and env. ${KEY} references in the template stay visible in the source compose template’s substitution path; once resolved, the substituted value is redacted.
  • The endpoint still writes an audit-log entry with details.dryRun = true so operators can see who probed which deployment.
  • The request body’s imageTag (if provided) is honored — the preview reflects the tag that the real deploy would have used. Plan dry-runs use the per-step targetTag the same way.
  • When the real deploy would have failed at artifact generation (missing secrets, template errors in a config file), the response carries "wouldSucceed": false and an "error" string describing the blocker. The preview is still returned (so operators can see what is broken) but callers should treat this as a hard block before running the real path.

For the service-wide POST /api/services/:id/sync-files endpoint, the dry-run response shape is:

{
"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"],
"warnings": []
}
]
}

Drift endpoints diff BRIDGEPORT’s stored view of a service against the actual state on the host. They are strictly read-only — they only run docker inspect and file reads (cat) and never pull images, write files, or touch containers. All three are viewer-accessible.

GET /api/services/:id/drift # one service, all its deployments
GET /api/servers/:id/drift # every deployment on a server
GET /api/environments/:envId/drift # environment-wide roll-up
Authorization: Bearer <token>

A service can be deployed to multiple servers (one ServiceDeployment per server), and runtime state lives on the deployment — so drift is reported per deployment, keyed by serverId.

Response shape (GET /api/services/:id/drift):

{
"serviceId": "csrv...",
"serviceName": "api",
"checkedAt": "2026-06-08T12:00:00.000Z",
"deployments": [
{
"serviceDeploymentId": "csdp...",
"serverId": "csvr...",
"serverName": "web-1",
"containerName": "api",
"drift": {
"composePath": { "expected": "/opt/api/docker-compose.yml", "actual": "/opt/api/docker-compose.yml", "match": true },
"composeContent": { "match": false, "reason": "Host compose file content differs from the regenerated compose." },
"imageDigest": { "expected": "sha256:abc...", "actual": "sha256:def...", "match": false, "reason": "Host image digest does not match the recorded deployed digest." },
"exposedPorts": { "expected": [{ "host": 80, "container": 80, "protocol": "tcp" }], "actual": [{ "host": null, "container": 80, "protocol": "tcp" }], "match": false, "reason": "Published ports differ from the stored mapping." },
"configFiles": [ { "targetPath": "/etc/api/app.env", "configFileName": "app.env", "match": true } ],
"envVars": { "missing": [], "unexpected": ["LOG_LEVEL"], "match": false }
},
"summary": "3 drift items detected",
"warnings": []
}
],
"summary": "3 drift items detected"
}

GET /api/servers/:id/drift returns the same per-deployment shape under deployments (one entry per deployment on the server). GET /api/environments/:envId/drift groups deployments under a services[] array ({ serviceId, serviceName, deployments[] }).

Per-field semantics:

  • match is true (in sync), false (drift detected), or null (could not be reliably compared — see reason). The summary counts only match: false fields; null is never counted as drift.
  • composePath — the stored deployment compose path is the source of truth. match: null when no path is set (auto-managed compose off, or never deployed via compose).
  • composeContent — compared by checksum only; the raw content is never returned because it can embed secrets. match: null with a reason when the compose file is shared and operator-maintained (BRIDGEPORT intentionally does not rewrite those, so content drift is expected).
  • imageDigestexpected is the manifest digest BRIDGEPORT recorded as deployed; actual is read from the host image’s RepoDigests. match: null (not a false mismatch) when the comparison can’t be resolved — e.g. the host image is locally built / never pulled by digest, or BRIDGEPORT has no recorded deployed digest.
  • exposedPorts — compared as a set of host:container/protocol mappings, derived from the same logic a real deploy uses to publish ports.
  • configFiles — one entry per attached config file, compared by checksum only over secret-redacted content. Binary files report match: null with a reason.
  • envVars — only keys BRIDGEPORT manages (baseEnv + per-deployment envOverrides) are compared; image-baked and Docker-injected vars (PATH, HOSTNAME, …) are ignored. missing = a managed key absent on the host container; unexpected = a managed key whose host value differs. Only key names are returned, never values — so secret values never leak.

Security: drift never returns decrypted secret values or raw secret-bearing content. Content comparisons hash secret-redacted text, and env comparisons report presence/mismatch by key name only. Hosts that are unreachable degrade each affected field to match: null with a reason/warnings entry rather than failing the whole request.

View deployment history:

GET /api/services/:id/deployments-history?limit=20
Authorization: Bearer <token>

Returns recent deployments ordered newest first, each with id, imageTag, previousTag, status, triggeredBy, startedAt, completedAt, durationMs, and serviceDeployment.server ({ id, name }, nullable for legacy rows whose per-server deployment is gone).

View a single deployment with logs:

GET /api/deployments/:id
Authorization: Bearer <token>

View deployment artifacts:

GET /api/deployments/:id/artifacts
Authorization: Bearer <token>

Returns the compose file, env files, and config files that were generated and uploaded during that deployment.

View service action history (all actions):

GET /api/services/:id/history?limit=50
Authorization: Bearer <token>

Returns audit log entries for this service (deploys, restarts, health checks, updates, creates) plus a separate deployments array.

BRIDGEPORT supports several container lifecycle actions:

Restart container:

POST /api/services/:id/restart
Authorization: Bearer <token>

For compose-managed services (those with a composePath), restart runs docker compose ... rm -f -s <service> followed by docker compose ... up -d --force-recreate <service>. This creates a new container so updated compose or config files are picked up. For services without a composePath, restart falls back to docker restart <container-name>. Restart does not regenerate compose artifacts — it always uses the current on-disk compose file. The action is logged in the audit trail.

View container logs:

GET /api/services/:id/logs?tail=100&before=2026-05-25T10:23:45Z
Authorization: Bearer <token>

Returns container logs with timestamps. Query params:

  • tail (optional): number of lines to return. When omitted, falls back to the defaultLogLines system setting.
  • before (optional): ISO-8601 timestamp. When set, the endpoint returns up to tail lines whose timestamps are at or before this value — used by the service detail logs viewer to page back (“Load older”).

Output always includes Docker timestamps (docker logs -t), so the client can extract the oldest line’s timestamp and request the next page with before=<that timestamp>.

Stream container logs (SSE):

GET /api/services/:id/logs/stream
Authorization: Bearer <token>

Opens a Server-Sent Events stream with real-time stdout and stderr events.

Run predefined commands:

POST /api/services/:id/run-command
Authorization: Bearer <token>
Content-Type: application/json
{
"commandName": "shell"
}

Runs a command defined by the service’s service type (e.g., Django shell, Node.js REPL). Returns the command string for the CLI to execute.

Check for image updates:

POST /api/services/:id/check-updates
Authorization: Bearer <token>

Queries the linked registry for newer tags and returns whether an update is available.

Each service has per-service health check settings used during deployment orchestration:

SettingAPI FieldDefaultDescription
Health WaithealthWaitMs30000 (30s)Wait after deploy before first check
Health RetrieshealthRetries3Number of check attempts
Health IntervalhealthIntervalMs5000 (5s)Time between retry attempts

Update health check config:

PATCH /api/services/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"healthWaitMs": 60000,
"healthRetries": 5,
"healthIntervalMs": 10000
}

Manual health check:

POST /api/services/:id/health
Authorization: Bearer <token>

This runs a comprehensive check:

  1. Connects to the server and inspects the container (state, health, ports, image).
  2. If healthCheckUrl is configured, makes an HTTP request to that URL.
  3. Updates the service’s status, containerStatus, and healthStatus fields.
  4. Checks the linked registry for available updates.
  5. Logs the result in the health check log.

Set a health check URL:

PATCH /api/services/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"healthCheckUrl": "http://localhost:8000/health"
}

For services that expose TCP ports or TLS endpoints, the monitoring agent can perform automated connectivity and certificate expiry checks.

Configure TCP checks:

PATCH /api/services/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"tcpChecks": "[{\"host\": \"localhost\", \"port\": 5432, \"name\": \"postgres\"}]"
}

Configure certificate checks:

PATCH /api/services/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"certChecks": "[{\"host\": \"api.example.com\", \"port\": 443, \"name\": \"api-cert\"}]"
}

The agent runs these checks periodically and stores results in agentTcpCheckResults and agentCertCheckResults on the service record.

Each service can be linked to a Service Type via serviceTypeId. Service Types are plugin-provided (and admin-editable under /admin/service-types); they carry a display name and an optional set of predefined commands. The Services list page groups and filters by the linked Service Type.

Set / change a service’s type:

PATCH /api/services/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"serviceTypeId": "<service-type-id>"
}

Pass null to clear it (the service becomes “untyped”).

List the service types in use in an environment (for the filter chips):

GET /api/environments/:envId/services/type-counts
Authorization: Bearer <token>

Response:

{
"types": [
{ "id": "ckxq...", "displayName": "Django", "count": 4 },
{ "id": "ckyr...", "displayName": "Postgres", "count": 2 }
]
}

Services with no serviceTypeId are excluded — the Services list page surfaces untyped services via a separate “No type” filter chip.

UI: the Services list page shows filter chips at the top (one per Service Type in use, plus “No type” when applicable). The selected chip persists in the URL via ?type=<serviceTypeId> (?type=__none__ for the “No type” chip). The ServiceDetail config modal exposes a “Service Type” dropdown.

Every service is linked to a ContainerImage — the central entity that tracks the Docker image name, deployed tags, and registry updates.

Change a service’s container image:

PATCH /api/services/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"containerImageId": "clnewimg..."
}

The container image must be in the same environment as the service’s server. One image can be linked to many services, enabling “deploy all” workflows from the Container Images page.

See Container Images for managing images, tag history, and auto-updates.

Dependencies define the order in which services should be deployed and health-checked during orchestrated deployments.

Two dependency types:

TypeMeaning
health_beforeThe dependency must be healthy before this service is deployed
deploy_afterThis service deploys after the dependency has been deployed

Dependencies are configured on the service detail page or via the Service Dependencies API. They are used by deployment plans to build the correct execution order.

See Deployment Plans for the full orchestration guide.

Config files (docker-compose overrides, nginx configs, .env files, certificates) can be attached to services and synced to the server during deployment.

Attach a config file to a service:

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

The targetPath is the absolute path on the server where the file will be written during deployment. During deploy, BRIDGEPORT:

  1. Resolves any {{SECRET_NAME}} placeholders in the file content with actual secret values.
  2. Uploads the file to the target path via SSH.
  3. Sets chmod 600 on .env files for security.

See Config Files for creating and managing config files.

BRIDGEPORT generates a Docker Compose file for each service during deployment. You can use the auto-generated default or provide a custom template.

If no custom template is set, BRIDGEPORT generates a minimal compose file:

services:
app-api:
image: "registry.example.com/app-api:v2.1.0"
container_name: app-api
restart: unless-stopped
volumes:
- "/opt/app-api/config/nginx.conf:/opt/app-api/config/nginx.conf:ro"
ports:
- "8080:80"

The auto-generated template includes the image, container name, restart policy, read-only volume mounts for any attached config files, and ports: entries derived from the service’s discovered exposedPorts.

Port-mapping behavior:

  • An explicit binding (e.g., 8080:80) round-trips as "8080:80".
  • A binding restricted to a specific host IP (e.g., 127.0.0.1:8080:80) preserves the IP, so loopback-only services are not silently widened to all interfaces on regenerate.
  • A port the container only EXPOSEs (no host binding) is published on the matching host port — EXPOSE 80 becomes "80:80". Without this, the regenerated compose would have no ports: section and the container would come up unreachable.

To opt out of a port binding entirely (e.g., a service that should only be reached from inside the docker network), switch to a custom template.

For complex setups (extra networks, sidecar containers, environment variables, volume mounts), create a custom compose template with variable substitution.

Set a custom template:

PUT /api/services/:id/compose/template
Authorization: Bearer <token>
Content-Type: application/json
{
"composeTemplate": "services:\n ${SERVICE_NAME}:\n image: ${FULL_IMAGE}\n container_name: ${CONTAINER_NAME}\n restart: unless-stopped\n networks:\n - traefik\n volumes:\n - ${CONFIG_FILE_0}:${CONFIG_FILE_0}:ro\n - app-data:/data\n\nvolumes:\n app-data:\n\nnetworks:\n traefik:\n external: true"
}

Custom templates support these variables:

VariableReplaced WithExample
${SERVICE_NAME}Service nameapp-api
${CONTAINER_NAME}Docker container nameapp-api
${IMAGE_NAME}Full image path without tagregistry.example.com/app-api
${IMAGE_TAG}Tag being deployedv2.1.0
${FULL_IMAGE}Image path with tagregistry.example.com/app-api:v2.1.0
${CONFIG_FILE_N}Mount path of Nth config file (0-indexed)/opt/app-api/config/nginx.conf
${CONFIG_FILE_N_NAME}Filename of Nth config file (0-indexed)nginx.conf

Preview what the generated artifacts will look like without actually deploying:

GET /api/services/:id/compose/preview
Authorization: Bearer <token>

Returns the compose file content, config file contents (with secret placeholders resolved), and checksums.

GET /api/deployments/:id/artifacts
Authorization: Bearer <token>

Returns all artifacts (compose, config, env files) that were generated and uploaded during a specific deployment.

Delete the custom template to go back to the auto-generated default:

DELETE /api/services/:id/compose/template
Authorization: Bearer <token>
ScenarioRecommendation
Simple single-container serviceAuto-generated is sufficient
Extra Docker networks (e.g., Traefik)Custom template
Named volumes or bind mountsCustom template
Sidecar containersCustom template
Environment variables in composeCustom template
Complex restart/healthcheck policiesCustom template

Service Template Settings (env-scoped, shared across deployments)

Section titled “Service Template Settings (env-scoped, shared across deployments)”
FieldTypeDefaultDescription
imageTagstring'latest'Tag deployed across all deployments of this template
composeTemplatestringnullCustom compose template (null = auto-generated)
baseEnvJSON stringnullEnv vars applied to every deployment (overrides win per-deployment)
deployStrategyenum'sequential'sequential or parallel — how multi-server deploys roll out
healthCheckUrlstringnullURL to check for HTTP health
healthWaitMsint30000Wait after deploy before checking health
healthRetriesint3Number of health check attempts
healthIntervalMsint5000Interval between health check retries
serviceTypeIdstringnullPlugin-provided Service Type (predefined commands). Also drives the grouping/filter chips on the Services list.
tcpChecksJSON stringnullTCP port checks (agent-required)
certChecksJSON stringnullTLS certificate checks (agent-required)
FieldTypeDefaultDescription
serverIdstringrequiredTarget server for this deployment
containerNamestringrequiredDocker container name on the server (unique per server)
composePathstringnullPath to compose file on the server
envOverridesJSON stringnullPer-deployment env vars (override the template baseEnv)
exposedPortsJSON stringnullDiscovered exposed ports (managed by discovery)
status / containerStatus / healthStatusstringderivedPer-deployment runtime status, updated by health checks and the agent
discoveryStatusstring'unknown'found / missing / unknown — last discovery result
lastCheckedAt / lastDiscoveredAt / lastDeployedAtdatetimenullPer-deployment timestamps

Environment-Level Settings (Affect All Services)

Section titled “Environment-Level Settings (Affect All Services)”
SettingModuleDefaultDescription
Service Health IntervalMonitoring60000msAutomated health check frequency
Update Check IntervalMonitoring1800000msHow often to check registries for new tags

Deploy fails with “Container is not running after deploy” The container exited after starting. Check container logs (GET /api/services/:id/logs) for error messages. Common causes: missing environment variables, port conflicts, or application errors.

“Service not found” on deploy Verify the service ID is correct. If the service was recently deleted and re-created, the ID will have changed.

“Container image must be in the same environment” When creating a service, the containerImageId must reference a container image in the same environment as the server. Create the image in the correct environment first.

Health check returns “unknown” status The container was not found on the server. Run container discovery to update the service’s status, or verify the container name matches what is actually running.

Config files not appearing on the server after deploy

  • Verify the config file is attached to the service with a target path.
  • Check that generateArtifacts is true (the default) in the deploy request.
  • Verify SSH connectivity to the server.

Custom compose template variables not substituted Variable syntax is ${VARIABLE_NAME} (with curly braces). Check for typos in variable names. Only the variables listed in the Variable Substitution table are supported.

“No registry connection configured” when checking for updates The service’s container image is not linked to a registry connection. Configure one on the Container Images page.