You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
S3-compatible backups (feat(backup): Add remote S3-compatible backup destinations #168, ui: Object stores, a base component library, and dark-mode fixes ui#79): backups now mirror to remote S3-compatible object storage (AWS, R2, B2, MinIO) on top of the always-local copy, best-effort so a remote outage never fails a backup. What/how/where is documented in docs/BACKUPS.md; object-storage secrets live in the credential manager, never the flat-file config. This also seeds the object store abstraction (external vs managed stores, a MinIO template so FlatRun can run its own S3 endpoint), specced in docs/OBJECT_STORES.md. Follow-up slices, still open: one-click managed deploy with auto-register, an object browser, deployment consumption, and store-to-store replication.
Dashboard & API performance (performance: Loading deployments and the dashboard is generally too slow #154): progressive / lazy loading for the deployments
list and dashboard instead of loading everything at once (parked: the backend is no
longer the bottleneck, so whether the UI still needs this is unmeasured); profile and
optimize the backend paths behind those views. This item is about how fast FlatRun's own
UI and API respond, not how fast the deployed apps serve their traffic (tracked
separately below).
Backend profiling (this cycle): deployment status was recomputed on every request by
shelling out to the docker compose CLI with nothing cached, so latency scaled with
the number of deployments and was dominated by process startup. Listing N deployments
spawned 2N to 4N docker processes; a single detail view spawned 4 to 8. The backend
status path is addressed in perf(deployments): Read deployment status from the Docker engine #171: status now comes from one Engine API
query covering every deployment at once, so listing is flat with the deployment count
instead of scaling with it. The query is deliberately restricted to live containers,
which is what keeps reported status identical to what the compose CLI reported.
Progressive / lazy loading on the UI side is still open. Concrete work:
Memoize compose-command detection so docker compose version runs once per process,
not once per runCompose.
Serve list/detail status from one Engine API ContainerList filtered by com.docker.compose.project labels, reusing the existing APIClient, instead of
per-deployment docker compose ps.
Collapse the detail path that runs the compose sequence twice into one status read.
Skip the status fan-out for endpoints that only need on-disk metadata (certificates,
proxy sync, traffic), reading via FindDeployments rather than ListDeployments.
Cluster is excluded: it marshals the full deployment list to clients with status
included, so it does need the status read.
Add a short-TTL cache for container status so request bursts don't each recompute
it. Parked: it was specced when a list cost ~486ms and spawned 2N to 4N processes.
A list is now ~22ms, of which ~20ms is the single Engine API call, so a cache would
save ~20ms in exchange for stale status right after a start/stop and the invalidation
needed to avoid it. The call scales with live container count, so revisit if a busy
host shows it in real numbers.
Record before/after list and detail timings at 1, 10, and 50 deployments: list 128ms
to 25ms at 1, 104ms to 22ms at 10, and 486ms to 26ms at 50; detail ~150ms to ~22ms.
Deployment serving performance: how fast a deployed app answers real end-user
requests through FlatRun's reverse proxy, distinct from dashboard/API speed above. The
proxy is the shared path every deployment's traffic crosses, so its defaults set the
floor for everyone. Establish a baseline and remove the obvious drags:
Measure request latency and throughput through the proxy to a deployment (p50/p95,
requests/sec) against hitting the container directly, so proxy overhead is a number. With
keepalive pooling in place the proxy adds ~0.05ms mean over a direct hit (p50 0.04 to
0.09ms, p95 0.07 to 0.17ms on loopback): negligible, not a bottleneck.
Audit the generated nginx/openresty vhost defaults: keepalive to upstreams, gzip,
HTTP/2, sensible proxy buffer sizes, and static-asset caching headers. Audited; gzip,
proxy buffers, and HTTP/1.1 to upstreams landed in fix: Auto-renew certificates by default, with honest renew reporting and proxy tuning #167; HTTP/2 and the TLS
session cache were already set on every generated vhost. The two remaining gaps are now
closed too:
Keepalive to upstreams (feat(nginx): Reuse upstream connections with pooled keepalive #175). Routing sent Connection: close on every
non-WebSocket request and proxied through a variable that cannot pool, so each ordinary
request opened and closed a fresh connection to the container. Now emits real upstream
blocks with keepalive plus server ... resolve, which keeps the 30s restart
rediscovery the variable proxy_pass provided. resolve is open source only since nginx
1.27.3, so support is probed once against the live proxy and the old per-request
behaviour is kept on older images. Measured 40 requests over 3 pooled connections instead
of 40.
Confirm no per-request work scales badly (excess logging, TLS session cache off,
DNS re-resolution of upstreams). Checked, and all three are fine: access logging is
buffered (buffer=32k flush=5s) and the ACME challenge locations log nothing, the TLS
session cache is on for a day on every vhost, and upstream DNS re-resolution is the
cheap 30s resolver that exists on purpose. The one real per-request drag this audit
turned up is the upstream keepalive above.
Document knobs an operator can turn per deployment (worker limits, cache toggles).
Integrations: deploy from a GitHub or git URL and from uploaded code, not only
from compose content or a template. Today a deployment must already have a compose
file; this adds source-based deployment with auth.
Builders: build code and then deploy it. Today only an existing compose build:
section runs. This adds build-from-source (Dockerfile detection, buildpacks / nixpacks)
with build config (args, cache, secrets).
Agents v0 (feat(ai): Add agents defined as flat markdown files #189, feat(ai): Add the Agents view ui#91): agents defined as flat markdown files
in .flatrun/agents/, executed by the runtime as AI sessions through the shared tool set,
so permission gates, protected mode, secret redaction, and the state-change approval pause
all carry over; each run records its agent, so run history is just sessions. Three creation
paths: describe it to the assistant (a validated, gated write tool), the panel's editor with
a starter template, or drop a file in the directory. Architecture for governance policy,
durable runs, triggers, and MCP-client tools is in docs/AGENTS.md; positioning shipped as
the self-operating server (flatrun/website#12). Next slices: policy frontmatter, run
lifecycle, MCP client, triggers.
Persist AI chat sessions (feat(ai): List saved assistant sessions so past chats can be resumed #181, feat(ui): List and resume past assistant conversations ui#88): sessions were already
persisted per user on the agent, but nothing could enumerate them. Added a listing of the
caller's saved sessions (all for an admin), most recent first, each with a title from its
first message; the assistant's history view now loads real conversations and reopening one
restores its transcript and scope, replacing the previous hardcoded placeholder.
Marketplace-sourced templates: the deployment flow's templates should come from the
marketplace (api.flatrun.dev) and be kept current, with flatrun/marketplace on GitHub
as the fallback source for public publications when the API is unreachable. Today the
deploy flow uses the templates embedded in the agent binary.
Firewall enforcement (feat(firewall): Enforce the host firewall policy through nftables #180): saving a policy now translates it to
nftables and applies it, re-applies at startup, and removes the rules when disabled. It
only touches FlatRun's own table (Docker untouched), always keeps loopback, established
connections, and the active SSH port open so a default-deny cannot lock the operator out,
and snapshots/restores on a failed load. Where nftables is unavailable the policy is saved
but reported as not enforced. Verified via unit tests and by loading the generated ruleset
into real nft in an isolated netns; not applied to a live host's firewall.
Security trace & false-block fix (fix: Security module blocking after 1 404 #153, fix(security): Stop blocking legitimate clients on their first request #176, feat(ui): Show the events behind a blocked IP ui#83): the block
after a single request was general-purpose HTTP clients (curl, wget, scripts, uptime
monitors, webhooks) being matched as attack scanners, which block on sight; scanner
matching is now limited to tools that name themselves, and ordinary clients are held to
the volumetric thresholds. Every auto-block records the rule, count, and paths that tripped
it instead of a fixed string, and the UI adds a per-IP event trace (from the blocked-IP
list and the events table) showing each event's path, status, user agent, and message.
Effective rebuild / pull / env (bug: Rebuild, pulls etc not effective with picking new env vars #149): expose force-recreate, no-cache rebuild,
and non-cached pull so updated env vars and images actually take effect, matching what
currently requires a manual docker compose from the terminal.
UI design refresh: improve the UI design and adopt Iconify for iconography. First pass shipped in feat(ui): Design refresh with dark mode, assistant, and global search ui#74 (dark mode, design tokens, Iconify, the assistant rework, global search). Per-screen polish and modal consolidation still to follow.
Seed bind mounts from image content (Seed bind mounts from image content when host path is empty #139): shipped in feat(deployments): Bring container content onto the host as flat files #172 and feat(deployments): Browse container files and bring them onto the host ui#81, and wider than the original proposal. Seeding an empty mount from the image
is there (opt-in per mount, from a template's seed: or per request, only when the target
is empty), but the image is the wrong source for a deployment that is already running: an
entrypoint that writes its config on first start leaves nothing in the image, so a service
would come back on content it never had. So a path is copied out of the running container
instead, then mounted back, with the service stopped for the copy so nothing written in
between is lost. Files browses a running service and brings any path onto the host, where
the existing editor already works, and mounted paths are tagged and can be unmounted again
(which recreates the service, since a container stays bound to the host until it does).
Warnings follow what happens: deleting a mounted path takes it from the container, while
unmounting first leaves the host copy harmless to delete.
User-set primary service (enh: Smarter primary service detection in multi-service compose files #111): the smarter fallback shipped (detection now prefers app/web before the first service with ports, instead of random map order). Letting the
user explicitly set a deployment's primary service is still open.
Compose validation working directory (fix(compose): Resolve relative env_file against the deployment directory #161): validation hardcodes the working
directory to ., so a relative env_file (e.g. ./.env) fails on the image-set /
update path even though the file exists in the deployment directory. Resolve relative
paths against the deployment directory, as the up path already does.
Certificate marked for auto renewal not auto-renewed (fix: Auto-renew certificates by default, with honest renew reporting and proxy tuning #167): auto-renewal
was off by default and, even when on, the global flag could veto a certificate the user had
marked for renewal. Auto-renewal now defaults on, the renewer runs whenever certbot is
enabled, and each certificate's own setting wins over the global default. Single Renew also
stops reporting success when nothing was reissued, with force as an explicit option. Serving
performance was tuned in the same PR (HTTP/1.1 to upstreams on all routes, buffered access
logging, wider gzip, larger proxy buffers).
Routing-only hostnames for externally-fronted proxies (feat(nginx): Support routing-only hostnames with externally-terminated TLS #166, feat(nginx): Route hostnames whose TLS is terminated upstream #177, feat(ui): Add routing-only hostnames to the domain form ui#84): a hostname fronted by an external proxy (e.g. dashboard.app.com on Cloudflare proxying to a deployment on api.app.com) arrived with an
unmatched Host and was rejected, and it could not be added as an alias because aliases force
certificate issuance that fails when the DNS points at the external proxy. A hostname can now
be marked routing-only: it routes to the deployment and shares the primary domain's
certificate over the proxy's SNI, is kept out of issuance and renewal, and counts as a known
domain. Entered separately from ordinary aliases in the domain form. Scope note: covers the
externally-terminated-TLS case where the proxy reaches the origin over HTTPS with the primary
domain's SNI; a direct plain-HTTP request to the origin for such a host still hits the port-80
redirect to HTTPS, left as a possible follow-up.
Observability: alerts, history and interop (feat(observ): Make the metrics reach someone, and outlive the process #174, feat(observ): Show what a deployment is doing, and let rules be written ui#82): metrics
were collected and drawn and nothing else. Nothing reached an operator who was not watching
the screen, nothing survived a restart, and nothing else could read them. Thresholds now
alert through the notification targets already configured, and self-healing says when it
gives up rather than going quiet at the one moment someone has to act. History is on disk,
folded into a point a minute once it ages, so the UI's 6h and 24h ranges mean what they say
instead of drawing whatever the hour of memory happened to hold. Metrics are exported over
OTLP and served for scraping, which the README had promised while nothing exported anything.
How a deployment serves its users is drawn from the proxy's own record of every request,
which was being kept for the security views alone: that answers what CPU cannot, since a
deployment can sit at 3% CPU while every request returns 502. Logs can be followed as they
are written. Dashboard storage exists without a builder, deliberately: the fixed views
answer the common questions and the metrics are scrapeable for anything else. Request times
are stored in a format SQLite cannot parse, which is why serving is bucketed in Go (fix(traffic): Store request times in a format SQLite can read #173).
Observability follow-ups (from feat: Add AI-native observability and self-recovery module #148 review, fix(observ): Address the observability review follow-ups #178): (a) the health watcher
held its lock across the docker restart call, briefly blocking health reads during a restart;
(b) notification target URLs, including SMTP passwords, were stored in cleartext and returned
by the settings endpoint to any client that could read settings; (c) the notification HTTP
handlers lacked boundary tests; (d) set_auto_restart is a host-wide toggle but was gated as
if it needed a deployment scope, so it failed in an unscoped assistant session. The restart
now runs without the read lock; target URLs are masked in API responses and their file is
owner-only, with a masked save preserving the stored secret; the handlers (including the
plugin-token gate on emit) have tests; and the global toggle is gated on settings-write.
Dashboards API has no caller (feat(ui): Build the dashboard view over container and serving metrics ui#87): wired up rather than dropped, since the
client was unfinished observability scaffolding, not dead code. A Dashboards screen now
lets an operator create, rename, and delete dashboards and arrange panels over container or
serving metrics, reading from the metric endpoints the observability screens already use.
Housekeeping
Document the release naming system: keep the convention below current and reuse
it when opening the next umbrella issue.
Release naming: umbrella releases are codenamed after sea creatures, advancing one
letter per release. Albacore (A) -> Barnacle (B) -> Cuttlefish (C) -> Dolphin (D) ...
Pick the next unused letter when opening each new group(enhancements): issue.
Albacore release. Theme: full deployment lifecycle and self-healing operations.
Umbrella for the next release; each item below ships this cycle.
Features
docs/BACKUPS.md; object-storage secrets live in the credential manager, never the flat-file config. This also seeds the object store abstraction (external vs managed stores, a MinIO template so FlatRun can run its own S3 endpoint), specced indocs/OBJECT_STORES.md. Follow-up slices, still open: one-click managed deploy with auto-register, an object browser, deployment consumption, and store-to-store replication.progressive / lazy loading for the deployments(parked: the backend is nolist and dashboard instead of loading everything at once
longer the bottleneck, so whether the UI still needs this is unmeasured); profile and
optimize the backend paths behind those views. This item is about how fast FlatRun's own
UI and API respond, not how fast the deployed apps serve their traffic (tracked
separately below).
Backend profiling (this cycle): deployment status was recomputed on every request by
shelling out to the
docker composeCLI with nothing cached, so latency scaled withthe number of deployments and was dominated by process startup. Listing N deployments
spawned 2N to 4N
dockerprocesses; a single detail view spawned 4 to 8. The backendstatus path is addressed in perf(deployments): Read deployment status from the Docker engine #171: status now comes from one Engine API
query covering every deployment at once, so listing is flat with the deployment count
instead of scaling with it. The query is deliberately restricted to live containers,
which is what keeps reported status identical to what the compose CLI reported.
Progressive / lazy loading on the UI side is still open. Concrete work:
docker compose versionruns once per process,not once per
runCompose.ContainerListfiltered bycom.docker.compose.projectlabels, reusing the existingAPIClient, instead ofper-deployment
docker compose ps.proxy sync, traffic), reading via
FindDeploymentsrather thanListDeployments.Cluster is excluded: it marshals the full deployment list to clients with status
included, so it does need the status read.
Add a short-TTL cache for container status so request bursts don't each recomputeParked: it was specced when a list cost ~486ms and spawned 2N to 4N processes.it.
A list is now ~22ms, of which ~20ms is the single Engine API call, so a cache would
save ~20ms in exchange for stale status right after a start/stop and the invalidation
needed to avoid it. The call scales with live container count, so revisit if a busy
host shows it in real numbers.
to 25ms at 1, 104ms to 22ms at 10, and 486ms to 26ms at 50; detail ~150ms to ~22ms.
requests through FlatRun's reverse proxy, distinct from dashboard/API speed above. The
proxy is the shared path every deployment's traffic crosses, so its defaults set the
floor for everyone. Establish a baseline and remove the obvious drags:
requests/sec) against hitting the container directly, so proxy overhead is a number. With
keepalive pooling in place the proxy adds ~0.05ms mean over a direct hit (p50 0.04 to
0.09ms, p95 0.07 to 0.17ms on loopback): negligible, not a bottleneck.
HTTP/2, sensible proxy buffer sizes, and static-asset caching headers. Audited; gzip,
proxy buffers, and HTTP/1.1 to upstreams landed in fix: Auto-renew certificates by default, with honest renew reporting and proxy tuning #167; HTTP/2 and the TLS
session cache were already set on every generated vhost. The two remaining gaps are now
closed too:
Connection: closeon everynon-WebSocket request and proxied through a variable that cannot pool, so each ordinary
request opened and closed a fresh connection to the container. Now emits real
upstreamblocks with
keepaliveplusserver ... resolve, which keeps the 30s restartrediscovery the variable
proxy_passprovided.resolveis open source only since nginx1.27.3, so support is probed once against the live proxy and the old per-request
behaviour is kept on older images. Measured 40 requests over 3 pooled connections instead
of 40.
toggle sets a long browser cache on static assets, keyed off the request path's
extension so only static files are affected and dynamic responses keep the app's own
cache headers.
DNS re-resolution of upstreams). Checked, and all three are fine: access logging is
buffered (
buffer=32k flush=5s) and the ACME challenge locations log nothing, the TLSsession cache is on for a day on every vhost, and upstream DNS re-resolution is the
cheap 30s resolver that exists on purpose. The one real per-request drag this audit
turned up is the upstream keepalive above.
visible next to its CPU and memory, closing the loop with feat: Add AI-native observability and self-recovery module #148. Shipped in
feat(observ): Make the metrics reach someone, and outlive the process #174 and feat(observ): Show what a deployment is doing, and let rules be written ui#82: rate, errors, average and p95 are bucketed from the
proxy's own record of every request and served per deployment.
from compose content or a template. Today a deployment must already have a compose
file; this adds source-based deployment with auth.
build:section runs. This adds build-from-source (Dockerfile detection, buildpacks / nixpacks)
with build config (args, cache, secrets).
summarization, and related tools (UI side: feat(ai): Add AI workflow to file viewers/editors accross the application ui#71). First agent-side set shipped in
feat(ai): Let the assistant edit files, run actions, and control deployments #185: write a deployment file, run a quick action, start/stop/restart a
deployment, and list a deployment's security events to summarize. Every state-changing tool
requires deployment write access and honours protected mode (failing closed if that check
errors). The shared tool set is exposed over an MCP server (feat(ai): Expose FlatRun's tools as an MCP server #184, feat: Expose the assistant tool set over an MCP server #186;
settings tab feat: Add MCP server settings tab ui#89), the assistant's loop runs on the reusable
whilesmartgo/agentsrunner with per-call tool approval (refactor(ai): Adopt whilesmartgo/agents for the assistant core #182, refactor(ai): Adopt whilesmartgo/agents for the model engine #183 andrefactor(ai): Drive the assistant loop through the shared agents runner #187), and the website and docs present the MCP server and the agent-runtimes
direction (flatrun/website#11). Editor integration closed the loop (feat(ai): Scope file-editor assistant to the deployment and cover env vars ui#90,
fix(ai): Redact session input and gate state-changing tools on approval #188): file-editor sessions are deployment-scoped, the env tab shares keys
only, seeded session input is redacted server side, a state-changing tool always pauses for
per-call approval even in auto-run sessions, and the open editor reloads after an approved
write instead of clobbering it. Future tools land as needs surface.
in
.flatrun/agents/, executed by the runtime as AI sessions through the shared tool set,so permission gates, protected mode, secret redaction, and the state-change approval pause
all carry over; each run records its agent, so run history is just sessions. Three creation
paths: describe it to the assistant (a validated, gated write tool), the panel's editor with
a starter template, or drop a file in the directory. Architecture for governance policy,
durable runs, triggers, and MCP-client tools is in
docs/AGENTS.md; positioning shipped asthe self-operating server (flatrun/website#12). Next slices: policy frontmatter, run
lifecycle, MCP client, triggers.
persisted per user on the agent, but nothing could enumerate them. Added a listing of the
caller's saved sessions (all for an admin), most recent first, each with a title from its
first message; the assistant's history view now loads real conversations and reopening one
restores its transcript and scope, replacing the previous hardcoded placeholder.
marketplace (api.flatrun.dev) and be kept current, with
flatrun/marketplaceon GitHubas the fallback source for public publications when the API is unreachable. Today the
deploy flow uses the templates embedded in the agent binary.
nftables and applies it, re-applies at startup, and removes the rules when disabled. It
only touches FlatRun's own table (Docker untouched), always keeps loopback, established
connections, and the active SSH port open so a default-deny cannot lock the operator out,
and snapshots/restores on a failed load. Where nftables is unavailable the policy is saved
but reported as not enforced. Verified via unit tests and by loading the generated ruleset
into real nft in an isolated netns; not applied to a live host's firewall.
Improvements
nginx vhost generation (nginx vhost generation: WebSocket timeouts, unconditional upgrade header, ssl_stapling noise, missing target validation #156): configurable / long WebSocket proxy timeouts;
conditional
Connection: upgradevia amapinstead of unconditional; skipssl_staplingwhen the cert has no OCSP responder; validate the target service/portactually listens before saving a vhost.
Streamed deployment actions (feat: Stream start/stop progress and run deployment actions as jobs #150): run start/stop/restart as background jobs that
return a job id; job status and buffered output survive a page reload; stream compose
output over the WebSocket keyed by job id with a poll fallback; serialize concurrent
actions per deployment. Shipped in feat(agent): Run deployment and service actions as streamed jobs #163 and feat(ui): Stream deployment and service action progress ui#76. (Jobs are
in-memory with ~15-minute retention: reload survives, an agent restart does not.)
Security trace & false-block fix (fix: Security module blocking after 1 404 #153, fix(security): Stop blocking legitimate clients on their first request #176, feat(ui): Show the events behind a blocked IP ui#83): the block
after a single request was general-purpose HTTP clients (curl, wget, scripts, uptime
monitors, webhooks) being matched as attack scanners, which block on sight; scanner
matching is now limited to tools that name themselves, and ordinary clients are held to
the volumetric thresholds. Every auto-block records the rule, count, and paths that tripped
it instead of a fixed string, and the UI adds a per-IP event trace (from the blocked-IP
list and the events table) showing each event's path, status, user agent, and message.
Effective rebuild / pull / env (bug: Rebuild, pulls etc not effective with picking new env vars #149): expose force-recreate, no-cache rebuild,
and non-cached pull so updated env vars and images actually take effect, matching what
currently requires a manual
docker composefrom the terminal.UI design refresh: improve the UI design and adopt Iconify for iconography. First pass shipped in feat(ui): Design refresh with dark mode, assistant, and global search ui#74 (dark mode, design tokens, Iconify, the assistant rework, global search). Per-screen polish and modal consolidation still to follow.
Seed bind mounts from image content (Seed bind mounts from image content when host path is empty #139): shipped in feat(deployments): Bring container content onto the host as flat files #172 and
feat(deployments): Browse container files and bring them onto the host ui#81, and wider than the original proposal. Seeding an empty mount from the image
is there (opt-in per mount, from a template's
seed:or per request, only when the targetis empty), but the image is the wrong source for a deployment that is already running: an
entrypoint that writes its config on first start leaves nothing in the image, so a service
would come back on content it never had. So a path is copied out of the running container
instead, then mounted back, with the service stopped for the copy so nothing written in
between is lost. Files browses a running service and brings any path onto the host, where
the existing editor already works, and mounted paths are tagged and can be unmounted again
(which recreates the service, since a container stays bound to the host until it does).
Warnings follow what happens: deleting a mounted path takes it from the container, while
unmounting first leaves the host copy harmless to delete.
nginx additional-domain upstream collision (fix(nginx): Multi-domain upstream collides across deployments; server_names_hash not configurable #155): the extra-domain path proxies by
bare service name (e.g.
app), which is not unique across deployments, so a customdomain on a shared host intermittently serves another deployment's container. Route by
the unique deployment name like the primary-domain path.
nginx security base image (bug: Use flatrun/openresty for security base #106, check: Ensure base nginx image is configured to flatrun/openresty #39): use
flatrun/openrestyas the nginx baseinstead of upstream
openresty/openresty, which is incompatible with the FlatRunsecurity configs.
User-set primary service (enh: Smarter primary service detection in multi-service compose files #111): the smarter fallback shipped (detection now prefers
app/webbefore the first service with ports, instead of random map order). Letting theuser explicitly set a deployment's primary service is still open.
Propagate metadata-save errors (enh: Propagate SaveMetadata errors during compose updates #110): surface
SaveMetadatafailures during composeupdates instead of discarding them, so compose and
service.ymlcan't silently diverge.Suppress ACME challenge log noise (enh: Suppress logging for ACME challenge requests in nginx configs #112): set
access_log off/log_not_found offon the well-known ACME challenge locations so cleaned-up challenge 404s stop burying real
errors. Complements the fix: Security module blocking after 1 404 #153 security trace.
--helplists subcommands (enh:--helpcommand flag omits subcommands (update, setup, version) #130): top-level help should listupdate,setup, andversion, andhelpshould print usage instead of falling through to start the server.Compose validation working directory (fix(compose): Resolve relative env_file against the deployment directory #161): validation hardcodes the working
directory to
., so a relativeenv_file(e.g../.env) fails on the image-set /update path even though the file exists in the deployment directory. Resolve relative
paths against the deployment directory, as the
uppath already does.Certificate marked for auto renewal not auto-renewed (fix: Auto-renew certificates by default, with honest renew reporting and proxy tuning #167): auto-renewal
was off by default and, even when on, the global flag could veto a certificate the user had
marked for renewal. Auto-renewal now defaults on, the renewer runs whenever certbot is
enabled, and each certificate's own setting wins over the global default. Single Renew also
stops reporting success when nothing was reissued, with force as an explicit option. Serving
performance was tuned in the same PR (HTTP/1.1 to upstreams on all routes, buffered access
logging, wider gzip, larger proxy buffers).
Routing-only hostnames for externally-fronted proxies (feat(nginx): Support routing-only hostnames with externally-terminated TLS #166, feat(nginx): Route hostnames whose TLS is terminated upstream #177,
feat(ui): Add routing-only hostnames to the domain form ui#84): a hostname fronted by an external proxy (e.g.
dashboard.app.comon Cloudflare proxying to a deployment onapi.app.com) arrived with anunmatched Host and was rejected, and it could not be added as an alias because aliases force
certificate issuance that fails when the DNS points at the external proxy. A hostname can now
be marked routing-only: it routes to the deployment and shares the primary domain's
certificate over the proxy's SNI, is kept out of issuance and renewal, and counts as a known
domain. Entered separately from ordinary aliases in the domain form. Scope note: covers the
externally-terminated-TLS case where the proxy reaches the origin over HTTPS with the primary
domain's SNI; a direct plain-HTTP request to the origin for such a host still hits the port-80
redirect to HTTPS, left as a possible follow-up.
Observability: alerts, history and interop (feat(observ): Make the metrics reach someone, and outlive the process #174, feat(observ): Show what a deployment is doing, and let rules be written ui#82): metrics
were collected and drawn and nothing else. Nothing reached an operator who was not watching
the screen, nothing survived a restart, and nothing else could read them. Thresholds now
alert through the notification targets already configured, and self-healing says when it
gives up rather than going quiet at the one moment someone has to act. History is on disk,
folded into a point a minute once it ages, so the UI's 6h and 24h ranges mean what they say
instead of drawing whatever the hour of memory happened to hold. Metrics are exported over
OTLP and served for scraping, which the README had promised while nothing exported anything.
How a deployment serves its users is drawn from the proxy's own record of every request,
which was being kept for the security views alone: that answers what CPU cannot, since a
deployment can sit at 3% CPU while every request returns 502. Logs can be followed as they
are written. Dashboard storage exists without a builder, deliberately: the fixed views
answer the common questions and the metrics are scrapeable for anything else. Request times
are stored in a format SQLite cannot parse, which is why serving is bucketed in Go (fix(traffic): Store request times in a format SQLite can read #173).
Observability follow-ups (from feat: Add AI-native observability and self-recovery module #148 review, fix(observ): Address the observability review follow-ups #178): (a) the health watcher
held its lock across the docker restart call, briefly blocking health reads during a restart;
(b) notification target URLs, including SMTP passwords, were stored in cleartext and returned
by the settings endpoint to any client that could read settings; (c) the notification HTTP
handlers lacked boundary tests; (d)
set_auto_restartis a host-wide toggle but was gated asif it needed a deployment scope, so it failed in an unscoped assistant session. The restart
now runs without the read lock; target URLs are masked in API responses and their file is
owner-only, with a masked save preserving the stored secret; the handlers (including the
plugin-token gate on emit) have tests; and the global toggle is gated on settings-write.
Dashboards API has no caller (feat(ui): Build the dashboard view over container and serving metrics ui#87): wired up rather than dropped, since the
client was unfinished observability scaffolding, not dead code. A Dashboards screen now
lets an operator create, rename, and delete dashboards and arrange panels over container or
serving metrics, reading from the metric endpoints the observability screens already use.
Housekeeping
it when opening the next umbrella issue.
Release naming: umbrella releases are codenamed after sea creatures, advancing one
letter per release. Albacore (A) -> Barnacle (B) -> Cuttlefish (C) -> Dolphin (D) ...
Pick the next unused letter when opening each new
group(enhancements):issue.