Docs / Reading logs reference
Guidedeploymill://guides/logs

Reading logs reference (deploymill)

get_logs returns an app's logs in two flavors, selected with source: build/deploy logs (source: "build", the default) and runtime container logs (source: "runtime"). Both come back as raw logs text plus parsed, filterable entries.

  • source: "build" is the first thing to reach for when a deploy comes back with status: "error": that bare status tells you the build failed, and the build log tells you why.
  • source: "runtime" is what you want when the deploy succeeded but the app misbehaves once it's live (500s, crashes on a request) — it's the running container's own stdout/stderr.

What source: "build" covers

  • Build logsdocker build / buildpack output: dependency installs, compile errors, missing files, failed RUN steps.
  • Deploy logs — the platform pushing the image and starting the service: the steps Dokploy runs around your container.

These are stored as files on the host; the build source tails the file and returns the text. Under the hood it resolves a deployment, then reads its log content via Dokploy's REST surface.

What source: "runtime" covers

  • Runtime / container logs — your app's own stdout/stderr once it's running (request logs, console.log, stack traces from a live request), aggregated across all replicas of the service and timestamped.

Dokploy has no REST surface for these (they're WebSocket-only), so deploymill reads them through a small host-side log-reader sidecar that talks to the Docker Swarm API (see sidecar/README.md). This is an opt-in piece of infrastructure:

  • When the sidecar is configured (the server has RUNTIME_LOGS_URL + RUNTIME_LOGS_TOKEN), source: "runtime" returns parsed runtime entries just like the build source.
  • When it is not configured, the call returns { configured: false } with a note rather than erroring — branch on configured. Fall back to the edge-probe signal from deploy/rollback (the edges / edgeNote fields) and a readiness route (probePath) to narrow down the runtime issue.
  • If the app isn't running (stopped / never deployed), you get a service_not_found-style note and empty entries — check get_app_health, then deploy or start it.

Usage

get_logs({ applicationId })                            → latest deployment, last 200 lines, parsed
get_logs({ applicationId, tail: 1000 })                → last 1000 lines of the latest deployment
get_logs({ applicationId, deploymentId, tail: 500 })   → a specific historical deployment
get_logs({ applicationId, level: "error+" })           → only error/fatal entries
get_logs({ applicationId, grep: "npm ERR|ENOENT" })    → lines matching a regex (case-insensitive)
get_logs({ applicationId, grep: "out of memory", grepRegex: false }) → literal substring
get_logs({ applicationId, since: "2026-05-30T23:00:00Z" })          → entries at/after an instant
get_logs({ applicationId, source: "runtime" })         → running container stdout/stderr, last 200 lines
get_logs({ applicationId, source: "runtime", level: "error+", tail: 1000 }) → runtime errors only
  • Omit deploymentId to read the most recent deployment — the usual case right after a failed deploy. (deploymentId is ignored for source: "runtime", which reads the live service.)
  • All the filters (grep/level/since) and tail work identically across both sources — same parser.
  • tail bounds how many trailing lines are fetched (default 200, max 10000) before filtering. Build logs can be long; start small and raise tail if the error is scrolled off the top.
  • Get a specific deploymentId from list_deployments when you want an older build (e.g. comparing the last good deploy against the broken one).

Filtering

All three filters are best-effort and applied server-side over the fetched window:

  • grep — matched against the raw line, case-insensitive. A regex by default; an invalid pattern silently falls back to a substring match. Set grepRegex: false to force a literal substring.
  • level — keep only entries whose level was parsed off the line. Accepts a single level (trace/debug/info/warn/error/fatal) or warn+/error+ for "that level or worse". Lines with no detectable level are dropped when this is set.
  • since — keep entries timestamped at/after an ISO instant. Lines with no parseable timestamp are kept (we can't prove they're older).

Return shape

{
  "deploymentId": "…",
  "status": "error",
  "title": "…",
  "tail": 200,
  "logs": "…raw build output…",
  "entries": [
    { "line": "2026-05-30T23:12:18Z ERROR npm ERR! missing script: build",
      "ts": "2026-05-30T23:12:18.000Z", "level": "error",
      "message": "ERROR npm ERR! missing script: build" }
  ],
  "total": 200,
  "matched": 1,
  "truncated": true
}
  • logs is the raw log text (unchanged contract). An empty string means the log file is missing or not yet written (the deploy may still be starting, or the log rotated) — the response adds a note flagging that.
  • entries is the parsed, filtered view: one object per non-blank line, with a best-effort ts and level when they can be detected, and message (the line minus any leading timestamp). Branch on these instead of regexing logs.
  • total is how many entries were parsed from the fetched window; matched is how many passed the filters (and equals entries.length).
  • truncated is true when the fetched window was completely full (total >= tail), i.e. older lines almost certainly exist — raise tail to see them. (The REST surface gives no exact line total, so this is a heuristic.)
  • If the app has no deployments yet, deploymentId is null, entries is [], and a note says to deploy first.

For source: "runtime" the shape is the same logs/entries/total/matched/truncated, but instead of deploymentId/status/title you get source: "runtime", serviceName (the Swarm service read), and configured (a boolean — false means no log-reader sidecar is set up, with a note explaining the fallback).

The failed-deploy loop

1. deploy({ applicationId })            → status: "error" (+ failNote pointing here)
2. get_logs({ applicationId })          → read the build output, find the failing step
3. fix the cause (Dockerfile, deps, source) and push
4. deploy({ applicationId }) again      → repeat until status: "done"

start_project and deploy both surface this pointer on failure, so an agent that hits an error status knows to call get_logs next instead of guessing.

Troubleshooting

  • logs is empty right after triggering a deploy → the build hasn't written to the file yet. Wait for deploy to reach a terminal status (done/error), then read.
  • Error is cut off at the top → raise tail.
  • Deploy succeeded but the app 502s / throws on requests → that's a runtime problem, not a build one. Read get_logs({ applicationId, source: "runtime" }) for the container's own stdout/stderr. If that comes back { configured: false }, the server has no log-reader sidecar — fall back to the edge probe and your app's readiness route.