feat(flutter): add Flutter web provider - #679
Open
krushiraj wants to merge 3 commits into
Open
Conversation
Builds the Flutter web target and serves build/web with Caddy. Detection requires pubspec.yaml, web/index.html, and a Flutter SDK dependency, so mobile-only repos fall through instead of failing inside the Flutter toolchain. Version resolution follows the node.go ordering: latest, the environment > flutter constraint, an FVM pin, mise version files, then RAILPACK_FLUTTER_VERSION. PUB_CACHE lives at /app/.pub-cache rather than a BuildKit cache mount. dart2js resolves imports through absolute paths in package_config.json that point into the pub cache, so a cache mount drops the packages between the install and build steps. Pass --no-web-resources-cdn so CanvasKit is served from the app itself. Flutter otherwise loads it from gstatic.com at runtime and renders a blank page, still returning HTTP 200, when that host is unreachable. The SDK bundles canvaskit/ regardless, so image size is unchanged. The Caddyfile uses a route block because Caddy runs header before try_files rewrites, so a path matcher for /index.html never matches a request for /.
CanvasKit ships ~7.6MB of *.symbols files across canvaskit/ and canvaskit/chromium/. They only symbolicate engine stack traces in a debugger and are never requested at runtime, so they are dead weight in the runtime image. Dropping them takes build/web from 40MB to 32MB. Only the symbol maps are excluded, not the renderer payloads. skwasm and wimp go unfetched under the default CanvasKit renderer, but a --wasm build through RAILPACK_BUILD_CMD needs skwasm.
GHA arm runners emulate amd64 through QEMU, and the Flutter web build does not finish inside the 20m integration test timeout. Skip on arm64 the same way node-puppeteer does; native amd64 CI still covers it. Also regenerates the plan snapshot for the mise 2026.8.0 bump on main.
krushiraj
force-pushed
the
feat/flutter-web-provider
branch
from
August 7, 2026 18:34
92dd516 to
8dd006f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Flutter web apps match no provider today, so building one fails with "no
provider detected" and the only workaround is a hand-written Dockerfile. There
is an open request for Flutter web support on Nixpacks/Coolify, and the same gap
exists here.
Description
Adds a
flutterprovider that builds the web target and serves it with Caddy,reusing the existing static-serving pattern.
Detection requires all three:
pubspec.yaml,web/index.html, and a FlutterSDK dependency (
dependencies > flutter > sdk: flutter, or a top-levelflutter:section). Theweb/index.htmlrequirement is deliberate — amobile-only Flutter repo fails deep inside the Flutter toolchain with "This
application is not configured to build on the web", so falling through to the
next provider gives a clearer error.
Version resolution, lowest to highest precedence:
latest, theenvironment > flutterconstraint inpubspec.yaml, an FVM pin (.fvmrcor.fvm/fvm_config.json), mise/asdf version files, thenRAILPACK_FLUTTER_VERSION. This follows the ordering innode.go. FVM pinsnaming a channel (
stable,beta) are skipped, since a channel is notresolvable to a release.
No custom SDK download logic — mise ships
registry/flutter.tomlwith anhttp:backend and per-platform checksums, andgit/xz-utilsare already inthe builder image.
Three decisions worth calling out, all of which came out of running the build
rather than reading it:
PUB_CACHElives at/app/.pub-cache, not a BuildKit cache mount.Dart does not vendor packages the way
node_modulesdoes —dart2jsresolves imports through absolute paths in
.dart_tool/package_config.jsonthat point into the pub cache. A cachemount is not part of the layer, so the packages vanish between the install
and build steps and the build fails with
Error when reading '.../vector_math_64.dart'. This mirrors why the node provider caches/app/node_modules/.cacherather thannode_modulesitself.--no-web-resources-cdnis passed by default. Without it the built appfetches CanvasKit from
https://www.gstatic.comat runtime and renders ablank page with no error when that host is unreachable — while still
returning HTTP 200. The SDK bundles
canvaskit/intobuild/webregardless, so serving it locally costs no image size (66.1 MB either way).
It moves ~2.3 MB gzipped per cold load onto the deployer's bandwidth, which
is the real trade-off; HTTP cache partitioning means the shared CDN copy is
rarely reused across origins anyway. This follows the precedent of defaults
changed for the deployment context (
staticfile.goindex fallback,NEXT_TELEMETRY_DISABLED,DOTNET_CLI_TELEMETRY_OPTOUT) rather than theopt-in pattern used for Playwright browsers, which exists because that
install costs hundreds of MB.
RAILPACK_BUILD_CMDrestores the CDNbehaviour.
The Caddyfile uses a
routeblock. Caddy runsheaderbeforetry_filesrewrites, so apath /index.htmlmatcher never matches arequest for
/. Inside aroute, directives run in written order, so theCache-Control: no-cacheon Flutter's bootstrap files (index.html,flutter_bootstrap.js,flutter_service_worker.js,main.dart.js,version.json) actually applies. Those files have stable names and areversioned by the service worker's resource map rather than a content hash,
so caching them strands users on a stale build.
assets/andcanvaskit/are deliberately left on normal caching — they are not content-hashed
either, so
immutablewould be wrong.Index fallback defaults to
truesousePathUrlStrategy()routes resolve,overridable through the existing
Staticfileindex_fallbackkey.Test
examples/flutter-webis a minimal Flutter web app with twohttpCheckcases:/and a client-side route, both expecting 200, the latter proving indexfallback. Both are pinned to
linux/amd64because Flutter publishes Linux SDKarchives for x64 only —
releases_linux.jsoncarries no arm64 entry and mise'sregistry defines no
linux-arm64platform.Unit tests cover detection (web app, app with no assets section, mobile-only,
plain Dart package, node app, static site), pubspec parsing, and FVM version
precedence including the legacy config format and channel pins.
Verified manually beyond the automated checks: the built container was loaded in
a real browser, confirming the Dart app initialises (
flutter-view,flt-glass-panepresent,flt-renderer="canvaskit") and renders. Withwww.gstatic.comblocked at DNS, the app still renders and CanvasKit is servedfrom
/canvaskit/chromium/*; before--no-web-resources-cdnthe same conditionproduced a blank page while the HTTP check still passed.
Links