Skip to content

Commit 6a68639

Browse files
committed
feat(flutter): add Flutter web provider
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 /.
1 parent be73961 commit 6a68639

29 files changed

Lines changed: 1248 additions & 0 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
{
2+
"deploy": {
3+
"base": {
4+
"image": "ghcr.io/railwayapp/railpack-runtime:mise-2026.7.18"
5+
},
6+
"inputs": [
7+
{
8+
"include": [
9+
"/railpack/caddy"
10+
],
11+
"step": "packages:caddy"
12+
},
13+
{
14+
"include": [
15+
"/Caddyfile"
16+
],
17+
"step": "caddy"
18+
},
19+
{
20+
"include": [
21+
"build/web"
22+
],
23+
"step": "build"
24+
}
25+
],
26+
"startCommand": "caddy run --config /Caddyfile --adapter caddyfile 2\u003e\u00261",
27+
"variables": {
28+
"RAILPACK_VERSION": "dev"
29+
}
30+
},
31+
"steps": [
32+
{
33+
"assets": {
34+
"generated-mise-toml": "[generated-mise-toml]"
35+
},
36+
"commands": [
37+
{
38+
"path": "/mise/shims"
39+
},
40+
{
41+
"customName": "create mise config",
42+
"name": "generated-mise-toml",
43+
"path": "/etc/mise/config.toml"
44+
},
45+
{
46+
"cmd": "mise install",
47+
"customName": "install mise packages: flutter"
48+
}
49+
],
50+
"inputs": [
51+
{
52+
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.7.18"
53+
}
54+
],
55+
"name": "packages:mise",
56+
"variables": {
57+
"MISE_CACHE_DIR": "/mise/cache",
58+
"MISE_CONFIG_DIR": "/mise",
59+
"MISE_DATA_DIR": "/mise",
60+
"MISE_INSTALLS_DIR": "/mise/installs",
61+
"MISE_SHIMS_DIR": "/mise/shims"
62+
}
63+
},
64+
{
65+
"commands": [
66+
{
67+
"cmd": "flutter precache --web"
68+
},
69+
{
70+
"cmd": "flutter pub get"
71+
}
72+
],
73+
"inputs": [
74+
{
75+
"step": "packages:mise"
76+
},
77+
{
78+
"include": [
79+
"."
80+
],
81+
"local": true
82+
}
83+
],
84+
"name": "install",
85+
"secrets": [
86+
"*"
87+
],
88+
"variables": {
89+
"FLUTTER_SUPPRESS_ANALYTICS": "true",
90+
"PUB_CACHE": "/app/.pub-cache"
91+
}
92+
},
93+
{
94+
"commands": [
95+
{
96+
"cmd": "flutter build web --release --no-web-resources-cdn"
97+
}
98+
],
99+
"inputs": [
100+
{
101+
"step": "install"
102+
},
103+
{
104+
"include": [
105+
"."
106+
],
107+
"local": true
108+
}
109+
],
110+
"name": "build",
111+
"secrets": [
112+
"*"
113+
],
114+
"variables": {
115+
"FLUTTER_SUPPRESS_ANALYTICS": "true",
116+
"PUB_CACHE": "/app/.pub-cache"
117+
}
118+
},
119+
{
120+
"commands": [
121+
{
122+
"cmd": "mise install-into caddy@2.11.4 /railpack/caddy"
123+
},
124+
{
125+
"path": "/railpack/caddy"
126+
},
127+
{
128+
"path": "/railpack/caddy/bin"
129+
}
130+
],
131+
"inputs": [
132+
{
133+
"image": "ghcr.io/railwayapp/railpack-builder:mise-2026.7.18"
134+
}
135+
],
136+
"name": "packages:caddy",
137+
"variables": {
138+
"MISE_PARANOID": "1"
139+
}
140+
},
141+
{
142+
"assets": {
143+
"Caddyfile": "# global options\n{\n\tadmin off\n\tpersist_config off\n\tauto_https off\n\n\tlog {\n\t\tformat json\n\t}\n\n\tservers {\n\t\ttrusted_proxies static private_ranges 100.0.0.0/8 # trust railway's proxy\n\t}\n}\n\n# site block, listens on the $PORT environment variable, automatically assigned by railway\n:{$PORT:80} {\n\tlog {\n\t\tformat json\n\t}\n\n\trespond /health 200\n\n\t# Security headers\n\theader {\n\t\t# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type\n\t\tX-Content-Type-Options \"nosniff\"\n\t\t# Remove Server header\n\t\t-Server\n\t}\n\n\t# `flutter build web` always writes to build/web\n\troot * /app/build/web\n\n\t# Compression with more formats\n\tencode {\n\t\tgzip\n\t\tzstd\n\t}\n\n\t# directives inside a route run in written order, unlike the rest of the Caddyfile.\n\t# The cache header has to be matched *after* try_files rewrites, otherwise a request\n\t# for \"/\" is still \"/\" when the matcher runs and never matches /index.html.\n\troute {\n\t\t# match against direct HTML file matches, folder-level index.html, and optionally fall back to\n\t\t# the root index.html so client-side routes (usePathUrlStrategy) resolve\n\t\ttry_files {path} {path}.html {path}/index.html /index.html\n\n\t\t# Flutter's bootstrap files have stable names and are versioned by the service worker's\n\t\t# resource map rather than by a content hash. Caching them strands users on a stale build.\n\t\t@bootstrap path /index.html /flutter_bootstrap.js /flutter_service_worker.js /main.dart.js /version.json\n\t\theader @bootstrap Cache-Control \"no-cache\"\n\n\t\t# Handle static files\n\t\tfile_server {\n\t\t\thide .git\n\t\t\thide .env*\n\t\t}\n\t}\n}\n"
144+
},
145+
"commands": [
146+
{
147+
"name": "Caddyfile",
148+
"path": "/Caddyfile"
149+
},
150+
{
151+
"cmd": "caddy fmt --overwrite /Caddyfile"
152+
}
153+
],
154+
"inputs": [
155+
{
156+
"step": "packages:caddy"
157+
}
158+
],
159+
"name": "caddy",
160+
"secrets": [
161+
"*"
162+
]
163+
}
164+
]
165+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# global options
2+
{
3+
admin off
4+
persist_config off
5+
auto_https off
6+
7+
log {
8+
format json
9+
}
10+
11+
servers {
12+
trusted_proxies static private_ranges 100.0.0.0/8 # trust railway's proxy
13+
}
14+
}
15+
16+
# site block, listens on the $PORT environment variable, automatically assigned by railway
17+
:{$PORT:80} {
18+
log {
19+
format json
20+
}
21+
22+
respond /health 200
23+
24+
# Security headers
25+
header {
26+
# Prevent some browsers from MIME-sniffing a response away from the declared Content-Type
27+
X-Content-Type-Options "nosniff"
28+
# Remove Server header
29+
-Server
30+
}
31+
32+
# `flutter build web` always writes to build/web
33+
root * {{.DistDir}}
34+
35+
# Compression with more formats
36+
encode {
37+
gzip
38+
zstd
39+
}
40+
41+
# directives inside a route run in written order, unlike the rest of the Caddyfile.
42+
# The cache header has to be matched *after* try_files rewrites, otherwise a request
43+
# for "/" is still "/" when the matcher runs and never matches /index.html.
44+
route {
45+
# match against direct HTML file matches, folder-level index.html, and optionally fall back to
46+
# the root index.html so client-side routes (usePathUrlStrategy) resolve
47+
try_files {path} {path}.html {path}/index.html{{if .IndexFallback}} /index.html{{end}}
48+
49+
# Flutter's bootstrap files have stable names and are versioned by the service worker's
50+
# resource map rather than by a content hash. Caching them strands users on a stale build.
51+
@bootstrap path /index.html /flutter_bootstrap.js /flutter_service_worker.js /main.dart.js /version.json
52+
header @bootstrap Cache-Control "no-cache"
53+
54+
# Handle static files
55+
file_server {
56+
hide .git
57+
hide .env*
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)