Deploying Go Applications on Bare Metal
Go compiles to a single static binary with no runtime dependencies. No JVM, no interpreter, no node_modules. That makes Go the ideal language for bare metal deployment — copy one file to your server, start it, and you are serving production traffic. Here is the complete deployment workflow.
Why Go on Bare Metal
Go applications already run fast. On bare metal, they run faster. Without a hypervisor stealing 5–15% of CPU cycles and without noisy neighbors competing for resources, a Go HTTP server on dedicated hardware handles significantly more requests per second at lower latency and more consistent latency.
The deployment model is also the simplest possible: one binary, one systemd service, one Nginx config. No Docker required, no container orchestration, no package managers on the server.
Step 1: Build a Static Binary
Cross-compile your Go application for Linux on any development machine:
# Build for Linux AMD64 (most servers)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o myapp ./cmd/server
# Check binary size
ls -lh myapp
# -rwxr-xr-x 1 user user 12M myapp
# Optional: compress with UPX for smaller transfers
upx --best myapp
# 12M -> 4.1MThe CGO_ENABLED=0 flag produces a fully static binary with no shared library dependencies. It runs on any Linux system without installing anything.
Step 2: Deploy to Your Server
# Deploy a RAW server (13 seconds)
npx rawhq deploy
# Copy binary to server
scp myapp root@your-server-ip:/usr/local/bin/
# SSH in and verify
ssh root@your-server-ip
chmod +x /usr/local/bin/myapp
/usr/local/bin/myapp --versionFor automated deployments, add this to your CI pipeline. GitHub Actions or GitLab CI can build the binary and SCP it to your server on every push to main.
Step 3: Create a Systemd Service
Systemd keeps your Go application running, restarts it on crashes, and starts it on boot:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Go Application
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/local/bin/myapp
Restart=always
RestartSec=3
Environment=PORT=8080
Environment=GIN_MODE=release
WorkingDirectory=/opt/myapp
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target# Enable and start
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
systemctl status myappStep 4: Nginx Reverse Proxy
Put Nginx in front of your Go app for SSL termination, static file serving, and request buffering:
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name myapp.com;
location / {
proxy_pass http://127.0.0.1:8080;
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 30s;
proxy_send_timeout 30s;
}
}# Enable site and reload
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
# Add SSL with Certbot
certbot --nginx -d myapp.comStep 5: Health Checks
Add a health check endpoint to your Go application so systemd and monitoring tools can verify it is running correctly:
// Add to your router
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})Then configure systemd to use it for readiness checking. Add to your service file under [Service]:
# Add health check to systemd
ExecStartPost=/bin/sh -c 'for i in 1 2 3 4 5; do curl -sf http://127.0.0.1:8080/healthz && exit 0; sleep 1; done; exit 1'Step 6: Graceful Restarts
Handle OS signals in your Go application so in-flight requests complete before shutdown:
srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with 10s timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)With graceful shutdown in place, systemctl restart myapp completes all in-flight requests before starting the new binary. Zero dropped connections during deploys.
Deploy Script
Combine everything into a one-command deploy script:
#!/bin/bash
set -e
SERVER="root@your-server-ip"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o myapp ./cmd/server
scp myapp $SERVER:/usr/local/bin/myapp-new
ssh $SERVER 'mv /usr/local/bin/myapp-new /usr/local/bin/myapp && systemctl restart myapp'
echo "Deployed successfully"The atomic mv ensures the binary is fully transferred before systemd restarts the service. No partial uploads, no corrupted binaries.
Performance: Go on Bare Metal
Go's garbage collector already produces sub-millisecond pauses. On bare metal without hypervisor jitter, those pauses stay consistently low even under heavy load.
Start Deploying
npx rawhq deploy7-day free trial. Deploy a bare metal server in 13 seconds, SCP your Go binary, and serve production traffic at the lowest possible latency.