Shipping a Go + Flutter app as a single binary
OpenDray is a Go backend + Flutter frontend. In production, I wanted to
deploy a single file — scp it to the server, run it, done. No
Docker. No Nginx. No separate static file directory to manage.
go:embed
Go 1.16 added //go:embed directives. You point it at a directory
and the compiler bakes the files into the binary as an embed.FS.
At runtime, you serve them with http.FileServer.
//go:embed all:build/web
var DistFS embed.FS
The all: prefix includes files starting with . and
_, which Flutter's web build sometimes generates. Without it,
the embedded filesystem silently misses files and you get blank pages.
The build pipeline
The Makefile runs Flutter first, then Go:
build-web:
cd app && flutter build web --release
release-linux: build-web
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags='-s -w' -trimpath \
-o bin/opendray-linux-amd64 ./cmd/opendray
Flutter builds to app/build/web/. The Go embed directive
references that path. The final binary is ~61 MB — Flutter's compiled
JS + fonts + the Go server.
-ldflags='-s -w' strips debug symbols. -trimpath
removes local paths from the binary. CGO_ENABLED=0 produces a
static binary that runs anywhere without glibc version headaches.
CI
The catch: Go's go vet and go test also try to
resolve the embed directive. If the Flutter web build doesn't exist yet,
they fail with "pattern all:build/web: no matching files found."
In CI, the Flutter job runs first and uploads the build/web
directory as an artifact. The Go job downloads it before running vet/test/build.
The jobs can't run in parallel because the embed directive is evaluated at
compile time, not runtime.
The result
One binary. scp it. Run it. It serves the Flutter web UI on the
same port as the API. No reverse proxy needed for local use. Migrations run
on startup. The only external dependency is PostgreSQL.
Deployment is make release-linux && scp bin/opendray-linux-amd64
server:/opt/opendray/ && ssh server 'systemctl restart opendray'.
Three commands. No container registry. No orchestrator. No drama.