A hands-on guide to deploying Go apps without Docker, CI/CD, or cloud automation. Build binaries, transfer over SSH, set up systemd, configure NGINX, and generate SSL, all manually, on a real server. Full control. Zero abstraction.
Set Up the Server and Configure systemd
SSH into your VPS server, create the necessary directory for your app.
mkdir /var/lib/goapp
And then configure a systemd service to manage it. Navigate to /etc/systemd/system
and create a new service file named goapp.service
[Unit]
Description=goapp
[Service]
Type=simple
Restart=always
RestartSec=5s
ExecStart=/var/lib/goapp/latest
WorkingDirectory=/var/lib/goapp
[Install]
WantedBy=multi-user.target
Creating Golang App
Build a simple Go application that handles an HTTP server.
package main
import "net/http"
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
}))
http.ListenAndServe(":8080", mux)
}
Build the App
Compile the Go app into a uniquely named binary using a timestamp. Assume your VPS server is running Linux on amd64
architecture.
GOOS=linux GOARCH=amd64 go build -o binary-$(date +%s) main.go
The output binary will be named like binary-1685612345
, where the number is the current Unix timestamp.
Send the Binary to the Server
Transfer the compiled binary from your local machine to the VPS server using scp
. Assuming your VPS server’s IP is 123.123.123.123
.
scp binary-1685612345 [email protected]:/var/lib/goapp
Manage Binary Versions with a Symlink
Once the new binary is on your VPS server at /var/lib/goapp/
, create or update a symbolic link called latest that points to the current binary version. This lets your systemd service always run the latest binary without changing the service file every time.
ln -sfn /var/lib/goapp/binary-1685612345 /var/lib/goapp/latest
Finally, restart the goapp
service to pick up the new binary:
sudo systemctl restart goapp.service
Your app is now accessible at http://123.123.123.123:8080
.
Configure NGINX with Domain and SSL Certificates
Get a free SSL certificate from gethttpsforfree. Follow their steps to generate your domain private key (domain.key) and Signed Certificate Chain (chained.pem).
Move the private key to the server’s app directory
mv domain.key /var/lib/goapp
Create the file directly on your VPS and paste the contents of the chained.pem
certificate from your browser output and save the file
nano /var/lib/goapp/chained.pem
Install NGINX on your VPS server if you haven’t already
sudo apt update
sudo apt install nginx
Create a new NGINX site configuration file at /etc/nginx/sites-available/goapp
. Assuming your domain is goapp.com
server {
listen 80;
server_name goapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name goapp.com;
ssl_certificate /var/lib/goapp/chained.pem;
ssl_certificate_key /var/lib/goapp/domain.key;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
}
}
Enable the site and reload NGINX
sudo ln -s /etc/nginx/sites-available/goapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Your app is now accessible at https://goapp.com
.
Secure Your Server with UFW (Firewall)
Right now, your server is exposed, all ports are open by default. We’ll use UFW to lock it down and allow only essential traffic: SSH and HTTP/HTTPS. First, install UFW
sudo apt update
sudo apt install ufw
Then allow only the necessary services
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
Now enable the firewall
sudo ufw enable
Deployment with Makefile
To streamline future deployments, you can create a Makefile
script. This avoids repeating manual build, upload, and symlink steps every time.
.PHONY: deploy
deploy:
[email protected] && \
DEPLOY_PATH=/var/lib/goapp && \
TIMESTAMP=$$(date +%s) && \
BINARY=binary-$$TIMESTAMP && \
GOOS=linux GOARCH=amd64 go build -o $$BINARY main.go && \
scp $$BINARY $$REMOTE:$$DEPLOY_PATH/ && \
ssh $$REMOTE 'ln -sfn '$$DEPLOY_PATH'/'$$BINARY' '$$DEPLOY_PATH'/latest && systemctl restart goapp' && \
rm $$BINARY