← Back to Blog
GoTutorial

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.1M

The 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 --version

For 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 myapp

Step 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.com

Step 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

MetricVPS (4 vCPU)Bare Metal (4 cores)
HTTP req/sec (net/http)85,000140,000+
p99 latency12ms (variable)3ms (stable)
GC pause impactAmplified by hypervisorMinimal
Cold start~50ms~20ms

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 deploy

7-day free trial. Deploy a bare metal server in 13 seconds, SCP your Go binary, and serve production traffic at the lowest possible latency.