Deployment
Gastro applications compile to a single binary with embedded templates and static assets. Deploy by copying one file.
Building for Production
Generate the Go code and cross-compile for your target platform:
# Generate Go code from .gastro files
gastro generate
# Cross-compile for Linux
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dist/myapp .
# Deploy the single binary
scp dist/myapp server:/opt/myapp
Or use the shorthand:
# Or use gastro build for generate + compile
gastro build
./app
The resulting binary contains everything: your Go handlers, compiled templates, and static assets from static/. No runtime dependencies.
Docker
A multi-stage Dockerfile keeps the image small. The build stage compiles everything, and the runtime stage contains only the binary:
FROM golang:1.26-alpine AS build
WORKDIR /src
# Install the gastro CLI
COPY . /gastro-src
RUN cd /gastro-src && go build -o /usr/local/bin/gastro ./cmd/gastro/
# Copy project files
COPY examples/gastro/ .
# Generate and build
RUN gastro generate
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app .
FROM alpine:3
RUN adduser -D -u 1000 appuser
USER appuser
COPY --from=build /app /app
EXPOSE 4242
CMD ["/app"]
The runtime image uses Alpine Linux with a non-root user for security. The final image is typically under 20MB.
Environment Variables
The only configuration is the PORT environment variable:
# Set the port via environment variable
PORT=8080 ./myapp
# In Docker
docker run -p 8080:8080 -e PORT=8080 myapp
If PORT is not set, the server defaults to port 4242.
Platform Guides
The Docker image works with any container platform:
- Fly.io —
fly launchauto-detects the Dockerfile - Railway — connect your repo, Railway builds from the Dockerfile
- Google Cloud Run —
gcloud run deploy --source . - AWS ECS / Fargate — build the image and push to ECR
- Any VPS — copy the binary directly with
scp
Since Gastro builds a static binary with no runtime dependencies, you can also deploy without Docker by copying the binary to any Linux server.
CI: detecting stale .gastro/ (gastro check)
If your repository commits the generated .gastro/ directory (some teams do
for editor convenience; the default gastro new template gitignores it),
add a CI gate to catch contributors who edit a .gastro file without
regenerating:
gastro check
Exit codes:
| Code | Meaning |
|---|---|
0 |
.gastro/ matches what gastro generate would produce |
1 |
Drift detected. Stderr lists the differing files |
2 |
The check itself failed (no .gastro/ directory, generate error, etc.) |
A typical GitHub Actions step:
- name: Verify .gastro is up to date
run: gastro check
You can also wire it into go generate:
//go:generate gastro generate
…and run go generate ./... && git diff --exit-code in CI for the same effect
without the gastro binary needing a separate subcommand. Both approaches are
fine; gastro check is faster (no go invocation) and produces a friendlier
diff summary.
Trimming the committed tree
If you commit .gastro/, you can keep just the Go files and exclude the
transformed template artifacts. They get regenerated on every
gastro generate and contribute most of the diff noise on template-only
edits:
# .gitignore (when committing .gastro/)
.gastro/templates/
.gastro/templates/*.html are not copies of your .gastro source files
— they're build artifacts (frontmatter stripped, {{ wrap }}
rewritten to compiled component calls). External content referenced
by //gastro:embed is baked into the generated .gastro/*.go
handler files at codegen time, not into the templates. The
Go handler files in .gastro/*.go are still committed and remain the
reviewable surface; gastro check continues to catch drift between
source .gastro files and generated Go even with templates/
ignored, because it regenerates and byte-compares the full tree.
static/ is copied into .gastro/static/ for the same //go:embed
reason; it's safe to add .gastro/static/ to the same ignore block if
your static/ directory is large.