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 adeploycomes back withstatus: "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 logs —
docker build/ buildpack output: dependency installs, compile errors, missing files, failedRUNsteps. - 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 anoterather than erroring — branch onconfigured. Fall back to the edge-probe signal fromdeploy/rollback(theedges/edgeNotefields) 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-stylenoteand empty entries — checkget_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
deploymentIdto read the most recent deployment — the usual case right after a failed deploy. (deploymentIdis ignored forsource: "runtime", which reads the live service.) - All the filters (
grep/level/since) andtailwork identically across both sources — same parser. tailbounds how many trailing lines are fetched (default200, max10000) before filtering. Build logs can be long; start small and raisetailif the error is scrolled off the top.- Get a specific
deploymentIdfromlist_deploymentswhen 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. SetgrepRegex: falseto 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) orwarn+/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,
"redacted": false
}
logsis the log text, after secret redaction (see below). 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 anoteflagging that.entriesis the parsed, filtered view: one object per non-blank line, with a best-efforttsandlevelwhen they can be detected, andmessage(the line minus any leading timestamp). Branch on these instead of regexinglogs.totalis how many entries were parsed from the fetched window;matchedis how many passed the filters (and equalsentries.length).truncatedistruewhen the fetched window was completely full (total >= tail), i.e. older lines almost certainly exist — raisetailto see them. (The REST surface gives no exact line total, so this is a heuristic.)redactedistruewhen the redaction pass replaced at least one secret-shaped span in this response (see Secret redaction below);falsewhen nothing matched. Whentrue, thenotealso says so.- If the app has no deployments yet,
deploymentIdisnull,entriesis[], and anotesays to deploy first.
For source: "runtime" the shape is the same logs/entries/total/matched/truncated/redacted, 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).
Secret redaction
Both sources scrub secret-shaped material out of logs and entries before returning — log output routinely contains the very credentials deploymill injected (DATABASE_URL, the S3 access keys, bound secrets) and the app's own end-user PII, and get_logs hands its result straight into an agent transcript. This keeps the same names-only posture as list_env_vars: managed secret values aren't supposed to cross back to the client.
Two passes run before the response is built:
- Known injected values — deploymill resolves the secret-shaped env values it set for this app (keys matching
*SECRET*,*TOKEN*,*PASSWORD*,*_KEY*,DATABASE_URL, DSNs, …) and replaces any literal occurrence with***. - Structural shapes — a best-effort scrub of common patterns regardless of the env: connection-string passwords (
postgres://user:***@host), AWS access-key ids (AKIA…/ASIA…),Bearer …tokens, andpassword=/token=/api_key=-style pairs.
When anything was replaced, redacted: true and the note flags it. Limits — this is best-effort, not a guarantee: it won't catch novel/encoded secrets, a custom credential format deploymill didn't inject, or arbitrary PII (names, emails, IP addresses) in free-form log text. Treat get_logs output as lower-risk, not secret-free — don't paste it somewhere it could be indexed without a glance. Short secret values (under 6 characters) are intentionally not literal-scrubbed to avoid shredding unrelated log text. The *** you see in logs is deploymill's redaction, not your app's output.
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
logsis empty right after triggering a deploy → the build hasn't written to the file yet. Wait fordeployto 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.