Server Build
Run Wails applications as HTTP servers without a native GUI window
Wails v3 supports server mode, allowing you to run your application as a pure HTTP server without creating native windows or requiring GUI dependencies. This enables deploying the same Wails application to servers, containers, and web browsers.
Server mode is useful for:
- Docker/Container deployments - Run without X11/Wayland dependencies
- Server-side applications - Deploy as a web server accessible via browser
- Web-only access - Share the same codebase between desktop and web
- CI/CD testing - Run integration tests without a display server
- Microservices - Use Wails bindings in headless backend services
Quick Start
Server mode is enabled via the server build tag. Your application code remains the same - you just build with the tag:
# Using Taskfile (recommended)
wails3 task build:server
wails3 task run:server
# Or build directly with Go
go build -tags server -o myapp-server .
Here’s a minimal example:
package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed frontend/dist
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "My App",
// Server options are used when built with -tags server
Server: application.ServerOptions{
Host: "localhost",
Port: 8080,
},
Services: []application.Service{
application.NewService(&MyService{}),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
})
log.Println("Starting application...")
if err := app.Run(); err != nil {
log.Fatal(err)
}
}The same code can be built for desktop (without the tag) or server mode (with -tags server).
Configuration
ServerOptions
Configure the HTTP server with ServerOptions:
Server: application.ServerOptions{
// Host to bind to. Default: "localhost"
// Use "0.0.0.0" to listen on all interfaces
Host: "localhost",
// Port to listen on. Default: 8080
Port: 8080,
// Request read timeout. Default: 30s
ReadTimeout: 30 * time.Second,
// Response write timeout. Default: 30s
WriteTimeout: 30 * time.Second,
// Idle connection timeout. Default: 120s
IdleTimeout: 120 * time.Second,
// Graceful shutdown timeout. Default: 30s
ShutdownTimeout: 30 * time.Second,
// Additional origins allowed to open WebSocket connections and call
// /wails/runtime. Same-origin requests are always allowed.
WebSocketOriginPatterns: []string{"app.example.com"},
// Disable WebSocket and runtime origin checks. Unsafe; default: false.
WebSocketAllowAllOrigins: false,
// TLS configuration (optional)
TLS: &application.TLSOptions{
CertFile: "/path/to/cert.pem",
KeyFile: "/path/to/key.pem",
},
},Features
Health Check Endpoint
A health check endpoint is automatically available at /health:
curl http://localhost:8080/health
# {"status":"ok"}
This is useful for:
- Kubernetes liveness/readiness probes
- Load balancer health checks
- Monitoring systems
Service Bindings
All service bindings work identically to desktop mode:
type GreetService struct{}
func (g *GreetService) Greet(name string) string {
return "Hello, " + name + "!"
}
// Register in options
Services: []application.Service{
application.NewService(&GreetService{}),
},The frontend can call these bindings using the standard Wails runtime:
const greeting = await wails.Call.ByName('main.GreetService.Greet', 'World');Events
Events work bidirectionally in server mode:
- Frontend to Backend: Events emitted from the browser are sent via HTTP and received by your Go event handlers
- Backend to Frontend: Events emitted from Go are broadcast to all connected browsers via WebSocket
Each browser tab is represented as a “window” with a unique name (browser-1, browser-2, etc.), accessible via event.Sender:
// Listen for events from browsers
app.Event.On("user-action", func(event *application.CustomEvent) {
log.Printf("Event from %s: %v", event.Sender, event.Data)
// event.Sender will be "browser-1", "browser-2", etc.
})
// Emit events to all connected browsers
app.Event.Emit("server-update", data)From the frontend:
// Emit event to server (and all other browsers)
await wails.Events.Emit('user-action', { action: 'click' });
// Listen for events from server
wails.Events.On('server-update', (event) => {
console.log('Update from server:', event.data);
});Graceful Shutdown
The server handles SIGINT and SIGTERM signals gracefully:
- Stops accepting new connections
- Waits for active requests to complete (up to
ShutdownTimeout) - Runs
OnShutdownhooks - Shuts down services in reverse order
Differences from Desktop Mode
| Feature | Desktop Mode | Server Mode |
|---|---|---|
| Native windows | Created | Browser windows (browser-N) |
| System tray | Available | Not available |
| Native dialogs | Available | Not available |
| Application menu | Available | Not available |
| Screen info | Available | Returns error |
| Service bindings | Works | Works |
| Events | Works | Works (via WebSocket) |
| Assets | Via webview | Via HTTP |
| CGO required | Yes | No |
Window API Behavior
In server mode, window-related APIs are safely handled:
app.Window.NewWithOptions()- Logs a warning, returns nilapp.Hide()/app.Show()- No-opapp.Screen.GetPrimary()- Returns error
This allows code that references windows to run without crashing, though window operations have no effect.
Supported Platforms
Server mode is pure Go and compiles for every GOOS Wails supports, including android and ios, without CGO. On mobile targets it builds a headless server only:
- The native mobile APIs,
application.Androidandapplication.IOS, are not available. Code that uses them must be excluded from server builds with a!serverbuild constraint. - The default logger is the desktop one, which writes to stderr in development builds, rather than logcat or the WebView console.
Building for Production
Using Task (Recommended)
Projects created with wails3 init include a build:server task:
# Build for server mode
wails3 task build:server
# Build and run
wails3 task run:server
Manual Build
# Build with server mode
go build -tags server -o myapp-server .
Docker
Wails projects include a ready-to-use Docker setup. To build and run your application in a container:
# Build the Docker image
wails3 task build:docker
# Run it
wails3 task run:docker
That’s it! Your application will be available at http://localhost:8080.
You can customise the build with a few options:
# Use a custom image tag
wails3 task build:docker TAG=myapp:v1.0.0
# Run on a different port
wails3 task run:docker PORT=3000
The generated Dockerfile.server creates a minimal image based on distroless. It handles the network binding automatically, so your application will be accessible from outside the container.
Docker Compose
For more complex deployments, here’s a Docker Compose configuration with health checks:
services:
app:
build: .
ports:
- "8080:8080"
environment:
- WAILS_SERVER_HOST=0.0.0.0
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3Custom Dockerfile
If you need more control, you can create your own Dockerfile. The key thing to remember is setting WAILS_SERVER_HOST=0.0.0.0 so the server accepts connections from outside the container:
# Build stage
FROM golang:alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY . .
RUN go mod tidy
RUN go build -tags server -ldflags="-s -w" -o server .
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
COPY --from=builder /app/frontend/dist /frontend/dist
EXPOSE 8080
ENV WAILS_SERVER_HOST=0.0.0.0
ENTRYPOINT ["/server"]Security Considerations
When deploying server mode applications:
- Bind to localhost by default - Only use
0.0.0.0when needed - Use TLS in production - Configure
ServerOptions.TLS - Place behind reverse proxy - Use nginx/traefik for additional security
- Keep WebSockets and runtime calls same-origin - Add only trusted origins with
WebSocketOriginPatterns; avoidWebSocketAllowAllOrigins - Validate all inputs - Same security practices as any web application
Example
A complete example is available at v3/examples/server/:
cd v3/examples/server
# Using Taskfile
task dev
# Or run directly
go run -tags server .
# Open http://localhost:8080 in browser
Environment Variables
For deployment scenarios where you need to override the server configuration without changing code, Wails recognises these environment variables:
| Variable | Description | Default |
|---|---|---|
WAILS_SERVER_HOST |
Network interface to bind to | localhost |
WAILS_SERVER_PORT |
Port to listen on | 8080 |
These take precedence over the ServerOptions in your code, which is why the Docker examples set WAILS_SERVER_HOST=0.0.0.0 - it allows the container to accept external connections without requiring any changes to your application.
See Also
- Custom Transport - For advanced IPC customization
- Services - Service binding documentation
- Events - Event system documentation
Runtime request size
Requests to /wails/runtime are limited to 64 MiB before JSON processing. An ordinary request above this limit receives HTTP 413, including requests without Content-Length. Chunked runtime uploads retain their separate 1 MiB per-chunk and 64 MiB assembled-payload limits. Use application middleware or a reverse proxy to impose a lower limit where appropriate.
Runtime request origin
/wails/runtime accepts only POST requests; other methods receive HTTP 405. A browser request whose Origin is neither the request host nor a match for WebSocketOriginPatterns receives HTTP 403, as does one without Origin whose Sec-Fetch-Site is cross-site or same-site. Requests carrying neither header, such as those from curl or other non-browser clients, are accepted. If a reverse proxy rewrites the Host header, add the public origin to WebSocketOriginPatterns.
WebSocketOriginPatterns exists for reverse-proxy deployments, where the public origin differs from the Host seen by the application. Frontends served from a different origin that call /wails/runtime directly are not supported: the endpoint emits no CORS headers and answers preflight OPTIONS requests with HTTP 405. Serve the frontend from the Wails server, or put both behind the same proxy origin.
Loopback Host check
When the server binds a loopback address (the default localhost, any 127.0.0.0/8 address, or ::1, whether set in ServerOptions.Host or WAILS_SERVER_HOST), every request must carry a Host header naming localhost, an IP literal, the bind host, or a host matching the host part of a WebSocketOriginPatterns entry; any other Host receives HTTP 403. This stops DNS rebinding, where a hostile web page points its own hostname at 127.0.0.1 so that its requests pass the same-origin checks above. The port is not compared, and spellings such as localhost. (trailing dot) or 127.1 are rejected. The check still applies when WebSocketAllowAllOrigins is set. If a local reverse proxy forwards its public Host, add that host to WebSocketOriginPatterns. Binds to other addresses, such as 0.0.0.0 in containers, are not affected.