How to Build and Deploy a Go Web Application on RHEL 7
Go’s standard library includes a production-capable HTTP server in the net/http package. Unlike Java or Python, you do not need an external application server or framework to serve web traffic — the Go runtime handles it natively with impressive performance. This guide covers writing a basic Go web server, compiling it to a self-contained binary, running it as a systemd service on RHEL 7, placing Nginx in front as a reverse proxy, cross-compiling for different architectures, embedding static assets into the binary, and implementing graceful shutdown so in-flight requests complete before the process exits.
Prerequisites
- RHEL 7 with root or
sudoaccess - Go 1.22 or later installed (see the Go installation guide)
- Nginx available via
yum - Basic familiarity with Go syntax and systemd
Step 1: Write a Basic net/http Web Server
Create a project directory and initialise a Go module:
mkdir -p ~/projects/webdemo
cd ~/projects/webdemo
go mod init github.com/yourusername/webdemo
Create the main application file with a JSON API endpoint and an HTML homepage. This example demonstrates routing, JSON responses, and structured logging using only the standard library:
cat > ~/projects/webdemo/main.go <<'EOF'
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type StatusResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Version string `json:"version"`
}
func statusHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := StatusResponse{
Status: "ok",
Message: "Web demo is running",
Version: "1.0.0",
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, "encoding error", http.StatusInternalServerError)
return
}
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(`<!DOCTYPE html><html>
<head><title>Go Web Demo</title></head>
<body><h1>Go Web Demo</h1><p>Running on RHEL 7</p></body>
</html>`))
}
func main() {
logger := log.New(os.Stdout, "[webdemo] ", log.LstdFlags)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/api/status", statusHandler)
srv := &http.Server{
Addr: ":" + port,
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in a goroutine
go func() {
logger.Printf("Listening on port %s", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("ListenAndServe: %v", err)
}
}()
// Graceful shutdown on SIGTERM or SIGINT
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
logger.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatalf("Forced shutdown: %v", err)
}
logger.Println("Server stopped cleanly.")
}
EOF
Test the server locally:
go run main.go &
curl http://localhost:8080/
curl http://localhost:8080/api/status
kill %1
Step 2: Build the Production Binary
Compile the application with optimised flags. The -ldflags="-s -w" flag strips the symbol table and DWARF debug information, significantly reducing binary size:
go build -ldflags="-s -w" -o webdemo main.go
ls -lh webdemo
Verify the binary runs correctly:
./webdemo &
curl -s http://localhost:8080/api/status | python -m json.tool
kill %1
Step 3: Cross-Compile for a Different Architecture
One of Go’s most powerful features is effortless cross-compilation. You can build a Linux ARM64 binary from an x86-64 RHEL 7 machine — useful for deploying to ARM servers or Raspberry Pi systems — by setting two environment variables:
# Build for Linux ARM64
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o webdemo-arm64 main.go
# Build for Linux 386 (32-bit)
GOOS=linux GOARCH=386 go build -ldflags="-s -w" -o webdemo-386 main.go
# Build for Windows AMD64 (cross-platform)
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o webdemo.exe main.go
# Verify the target architecture
file webdemo webdemo-arm64
No special toolchain is required — Go’s compiler handles all of this natively.
Step 4: Embed Static Assets with go:embed
Go 1.16 introduced the //go:embed directive, which bundles static files (HTML templates, CSS, images, etc.) directly into the compiled binary at build time. This eliminates the need to distribute static assets separately.
Create a static assets directory:
mkdir -p ~/projects/webdemo/static
cat > ~/projects/webdemo/static/style.css <<'EOF'
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; }
h1 { color: #333; }
EOF
Update main.go to embed and serve the directory. Add the following to your imports and main function:
import (
"embed"
"io/fs"
// ... other imports
)
//go:embed static
var staticFiles embed.FS
// In main(), add to the mux:
staticFS, _ := fs.Sub(staticFiles, "static")
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
Rebuild. The compiled binary now contains all files from the static/ directory and serves them from memory — no filesystem access required at runtime.
Step 5: Deploy the Binary and Create a System User
Deploy the application binary to a standard location and run it as a dedicated non-privileged user:
sudo useradd -r -s /sbin/nologin goapp
sudo mkdir -p /opt/webdemo
sudo cp ~/projects/webdemo/webdemo /opt/webdemo/webdemo
sudo chown goapp:goapp /opt/webdemo/webdemo
sudo chmod 750 /opt/webdemo/webdemo
# Create log directory
sudo mkdir -p /var/log/webdemo
sudo chown goapp:goapp /var/log/webdemo
Step 6: Create the systemd Service Unit
Write a systemd service unit that starts the Go application on boot, restarts it on unexpected exit, and passes configuration through environment variables:
sudo tee /etc/systemd/system/webdemo.service <<'EOF'
[Unit]
Description=Go Web Demo Application
Documentation=https://example.com
After=network.target
[Service]
Type=simple
User=goapp
Group=goapp
WorkingDirectory=/opt/webdemo
Environment=PORT=8080
Environment=GIN_MODE=release
ExecStart=/opt/webdemo/webdemo
# Go handles SIGTERM gracefully; exit 0 and 143 are both clean
SuccessExitStatus=0 143
Restart=on-failure
RestartSec=5s
StartLimitInterval=60s
StartLimitBurst=3
# Security hardening
NoNewPrivileges=yes
PrivateTmp=yes
StandardOutput=journal
StandardError=journal
SyslogIdentifier=webdemo
[Install]
WantedBy=multi-user.target
EOF
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable webdemo
sudo systemctl start webdemo
sudo systemctl status webdemo
Monitor the logs:
sudo journalctl -u webdemo -f
Step 7: Configure Nginx as a Reverse Proxy
Install Nginx and configure it to proxy requests to the Go application. This setup handles load balancing, SSL termination, and serves as the public-facing entry point:
sudo yum install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Create the Nginx server block:
sudo tee /etc/nginx/conf.d/webdemo.conf <<'EOF'
upstream go_backend {
server 127.0.0.1:8080;
keepalive 64;
}
server {
listen 80;
server_name webdemo.example.com;
access_log /var/log/nginx/webdemo.access.log combined;
error_log /var/log/nginx/webdemo.error.log;
# Increase buffer sizes for Go's response headers
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
location / {
proxy_pass http://go_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_connect_timeout 10s;
}
location /static/ {
proxy_pass http://go_backend;
expires 1d;
add_header Cache-Control "public, immutable";
}
}
EOF
sudo nginx -t
sudo systemctl reload nginx
Step 8: Graceful Shutdown with Signal Handling
The graceful shutdown code in main.go (Step 1) uses Go’s os/signal package to catch SIGTERM and SIGINT. When systemctl stop webdemo is executed, systemd sends SIGTERM to the process. The application then:
- Stops accepting new connections
- Waits up to 30 seconds for in-flight requests to complete
- Exits with code 0 once all active handlers have returned
You can test this behaviour manually:
# Send a request that takes a few seconds (simulated with a slow endpoint)
curl http://localhost:8080/ &
# Immediately stop the service
sudo systemctl stop webdemo
# The curl request should complete before the process exits
# Check journal to confirm clean shutdown
sudo journalctl -u webdemo --no-pager | tail -5
The final log line should read Server stopped cleanly. — confirming no requests were dropped. This is critical for zero-downtime deployments where you roll over to a new binary while traffic is live.
Conclusion
Building and deploying a Go web application on RHEL 7 demonstrates several of the language’s production strengths: a self-contained binary with no runtime dependencies, built-in HTTP handling in the standard library, trivially easy cross-compilation for different targets, and clean signal-based graceful shutdown. By running the binary as a systemd service under a non-privileged account and placing Nginx in front, you achieve a secure, maintainable, and highly performant deployment architecture suitable for production workloads. The entire deployment artifact is a single binary file, making updates as simple as copying the new binary, restarting the service, and confirming the health check passes.