← Home

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.