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).
Documented in chore: Fold small Albacore follow-ups (backup pruning + docs) #193: per-domain proxy timeout and static-asset cache,
container CPU/memory limits, and that app worker counts stay the app's own setting.
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. Git-source deploy shipped in feat(deployments): Deploy from a git source behind a provider interface #195 behind a provider interface with a registry, so a source is a
registered implementation rather than a branch in the deploy flow: a repo is fetched
to a temp dir, required to contain a compose file before anything is created, and
private repos authenticate with a token from the credential manager. Still open on the
same interface: upload and webhook providers, and the "Deploy from source" UI.
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). Slices shipped since: governance policy
with step budgets and dry runs (feat(ai): Enforce agent governance policy, step budgets, and dry runs #191), and cron-scheduled unattended runs
under a permission grant plus per-agent run history (feat: Albacore follow-ups — host metrics, agent scheduling, run history #196). Remaining:
durable runs and MCP-client tools.
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.
Agent: an opt-in prerelease channel so update can see betas, reading
the releases list instead of only /releases/latest, with proper semver
ordering (the current check is a string comparison, so it can't tell a
prerelease from its final and can't reason about a downgrade).
Shipped in feat(updater): Make updates channel-aware with semver ordering #194: the CLI takes --prerelease, reads the releases
list, and orders by semver, and the same logic is exposed over /agent/update.
UI: an update/management view showing the current version, the versions
available to install (stable by default, prereleases behind an opt-in),
and each version's changelog, with a one-click update that streams
progress and reports the restart.
Shipped in feat(updates): Add an agent update management view ui#94 as Administration > Updates. Two gaps remain: the
install runs synchronously and reports the restart rather than streaming live
progress, and only the latest release installs (older versions are listed with
changelogs but are display-only, no per-version install yet).
Gate a UI-triggered update like any other host-level action
(settings-write, protected mode), so updating from the dashboard is
authorized, not a bypass.
Held to the settings read/write permissions. Host-level protected mode does
not exist (protected mode is per-deployment), so it does not apply to a host
action.
Scope note: fielded v0.3.0 agents predate the channel, so this first beta is
still a manual install; the work makes every beta after it reachable from the
CLI and the UI.
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.
Compose backups accumulate (discovery.go): every compose rewrite drops a
timestamped docker-compose.yml.bak.<ts> next to the file and nothing removes it, so a
frequently-updated deployment fills its directory with backups that clutter the file
manager and are tedious to clear one at a time.
Prune on a successful rewrite: keep the most recent backup (or a small bounded number)
and delete the rest, so a good update cleans up after itself while one rollback copy
still exists if a later edit goes wrong. Shipped in chore: Fold small Albacore follow-ups (backup pruning + docs) #193: a rewrite keeps
the five most recent backups and prunes the rest.
Bulk actions in the file manager: multi-select with delete (and move), so many files
clear in one action instead of one by one. Useful beyond backups.
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
Observability & self-recovery (feat: Add AI-native observability and self-recovery module #148): shipped in feat(observ): Add observability engine, plugin tool bridge, notifications #165 and feat(observ): Add observability UI, plugin slots, notifications settings ui#78. OTel-semconv metrics with a native time-series UI, health self-heal scoped to FlatRun-managed deployments only (capped retries with cooldown, so an external container can't trigger a restart loop), a core notification system wite email/webhook targets and test-send, and a plugin framework that lets a plugin inject UI sections, contribute settings, and expose tools the AI assistant can call. Follow-ups tracked under Improvements; none blocked the release.
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 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.Dashboard & API performance (performance: Loading deployments and the dashboard is generally too slow #154):
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.
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:
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.
Documented in chore: Fold small Albacore follow-ups (backup pruning + docs) #193: per-domain proxy timeout and static-asset cache,
container CPU/memory limits, and that app worker counts stay the app's own setting.
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. Git-source deploy shipped in
feat(deployments): Deploy from a git source behind a provider interface #195 behind a provider interface with a registry, so a source is a
registered implementation rather than a branch in the deploy flow: a repo is fetched
to a temp dir, required to contain a compose file before anything is created, and
private repos authenticate with a token from the credential manager. Still open on the
same interface: upload and webhook providers, and the "Deploy from source" UI.
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).
More AI tools (feat(ai): Add more tools for quick actions, file editing, summarization etc #147): expand the AI assistant with quick actions, file editing,
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.
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 asthe self-operating server (flatrun/website#12). Slices shipped since: governance policy
with step budgets and dry runs (feat(ai): Enforce agent governance policy, step budgets, and dry runs #191), and cron-scheduled unattended runs
under a permission grant plus per-agent run history (feat: Albacore follow-ups — host metrics, agent scheduling, run history #196). Remaining:
durable runs and MCP-client tools.
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/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.
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.
Update management (feat(updater): Make updates channel-aware with semver ordering #194, feat(updates): Add an agent update management view ui#94): today
flatrun-agent updatepulls GitHub's/releases/latest, which by definition skips prereleases, so a beta isunreachable from the CLI and there is no update surface in the UI at all.
Make updates channel-aware and drive them from the dashboard.
updatecan see betas, readingthe releases list instead of only
/releases/latest, with proper semverordering (the current check is a string comparison, so it can't tell a
prerelease from its final and can't reason about a downgrade).
Shipped in feat(updater): Make updates channel-aware with semver ordering #194: the CLI takes
--prerelease, reads the releaseslist, and orders by semver, and the same logic is exposed over
/agent/update.available to install (stable by default, prereleases behind an opt-in),
and each version's changelog, with a one-click update that streams
progress and reports the restart.
Shipped in feat(updates): Add an agent update management view ui#94 as Administration > Updates. Two gaps remain: the
install runs synchronously and reports the restart rather than streaming live
progress, and only the latest release installs (older versions are listed with
changelogs but are display-only, no per-version install yet).
(settings-write, protected mode), so updating from the dashboard is
authorized, not a bypass.
Held to the settings read/write permissions. Host-level protected mode does
not exist (protected mode is per-deployment), so it does not apply to a host
action.
Scope note: fielded v0.3.0 agents predate the channel, so this first beta is
still a manual install; the work makes every beta after it reachable from the
CLI and the UI.
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.
Compose backups accumulate (
discovery.go): every compose rewrite drops atimestamped
docker-compose.yml.bak.<ts>next to the file and nothing removes it, so afrequently-updated deployment fills its directory with backups that clutter the file
manager and are tedious to clear one at a time.
and delete the rest, so a good update cleans up after itself while one rollback copy
still exists if a later edit goes wrong. Shipped in chore: Fold small Albacore follow-ups (backup pruning + docs) #193: a rewrite keeps
the five most recent backups and prunes the rest.
clear in one action instead of one by one. Useful beyond backups.
Housekeeping
below now lives durably in the repo, with its history, to reuse 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.