Hosting a Go application on a 4$ VPS - Part 1

Hosting a Go application on a 4$ VPS - Part 1

There was a wave of posts few months ago on X/Twitter about running your applications on a cheap VPS vs managed platforms/services like Fly.io, Render, Heroku, GCP etc. Although there were a lot of comments about how “easy” it is to run something on your own VPS and how everyone should know it, but there were hardly any backing examples of people actually showing how to do it. Hence, I decided to give it a try myself and see how it turns out.

💡
This is part 1 of the overall piece, as my end goal was to host a simple website I will cover part 2 in a separate blog, which will talk about using Cloudflare pages to host a react app and link it to our Go app/api. You can access the site at - https://noyap.foo

Platform chosen - Digital Ocean. Why? - I just wanted to get started with something easy and convenient, upon searching Reddit and Google Digital Ocean seemed like a decent option. The UI/UX is pretty simple to get up and running as well.

Droplet - 512 MB Memory / 10 GB Disk / SGP1 - Ubuntu24.04 (LTS) x64, 500GB outbound transfer , SGP datacenter, costs about $4/month.

Additional options - Enable IPV6 and droplet metrics agent, both are free.

💡
This was the first time I was going to set up a server with a public facing application, hence I wanted to double check on aspects like - security/accessibility, alerts, pricing (don’t want to pay a random bill of hundreds of dollars), spam etc. It’s probably good to think of these things upfront and figure out ways to implement these before leaving a running application open without any checks in places.

Based on the above here was my initial plan -

  1. Set up the system first - ssh, required packages, non-root user to run things, check default network accessibility.

  2. Run a simple Go app with a GET endpoint localhost:8080/api/trivia . Test it out locally and also over internet.

  3. Set up a domain name, serve the api over it and configure a firewall to restrict access to the droplet.

  4. Set up resource limits and alerts.

Initial setup - this is to test our api over the internet with minimal steps

User setup and installations

  1. Set up SSH access for the root user during droplet creation process. SSH with root and set up a sudo user with SSH access. This should be the primary user for accessing the server from here-on and running the application. Ref - user setup

  2. SSH with the created user and try out some commands.

  3. Install go and create a simple application with a GET endpoint.

Serving the API over internet

Install nginx to access the endpoint over internet. Currently we will access it directly via the droplet’s public (Don’t share the public IP with anyone). Later on once we have a domain registered we will access the api against it.

sudo apt install nginx -y

Create a nginx configuration for serving your api - /etc/nginx/sites-available/goapp.conf , there should already be a default file located inside this folder.

server {
    listen 80;  # This makes Nginx listen on port 80
    server_name <your_droplet_ip>;  # Use your droplet's public IP address

    location / {
        proxy_pass http://localhost:8080/api/trivia;  # Proxy requests to your Go app running on port 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;
    }
}

Setup symlink for nginx to read this config - sudo ln -s /etc/nginx/sites-available/goapp.conf /etc/nginx/sites-enabled/

Verify the config - sudo nginx -t and reload nginx sudo systemctl reload nginx

Logging utility options -

Go to /etc/nginx/nginx.conf and update the default logging setting in http block

  1.      ##
         # Logging Settings
         ##
    
             log_format custom_headers_log '[$time_local] "$request" '
                                       'Host: "$host", '
                                       'Origin: "$http_origin", '
                                       'Referer: "$http_referer", '
                                       'User-Agent: "$http_user_agent", ';
    
         access_log /var/log/nginx/access.log custom_headers_log;
    

You can check the access and error logs via sudo tail -f /var/log/nginx/access.log and sudo tail -f /var/log/nginx/error.log

Firewall setup

We’ll use ufw to restrict network access to our droplet. By default ufw denies all incoming traffic and allows all outgoing traffic. Presently, we need to allow HTTP traffic over port 80 so nginx can serve our api over the internet.

  1. sudo ufw allow OpenSSH - Important - to keep SSH open through firewall on default port 22.

  2. sudo ufw allow in 80

  3. sudo ufw enable

  4. sudo ufw status verbose - check detailed rules.


With this setup we should be able to access our api at - http://<your_droplet_public_ip>/api/trivia

💡
NOTE - this was just for testing that things work, don’t share your ip with anyone. Now we’ll move to a more practical setup where we don’t have to share our server’s ip with users.

Making things practical

Ideally we would want to host and access our application/api over some domain name. We would also want to enforce stricter access policies to the server and use a secure protocol like https for internet communication.

Domain -

I went ahead and bought a domain noyap.foo via Cloudflare. As I intended on using Cloudflare pages anyway to host the FE piece and wanted to keep things at a single place and simple. This basically makes Cloudflare both my DNS registrar and resolver, which is termed as a “full setup” by Cloudflare. It turned out to be a good decision as having a “full setup” unlocks lot of additional features in cloudflare - proxying requests, caching, secure tunnel setup etc.

The setup might look slightly different if you have purchased your domain from some other provider.

Once you have the domain we need to link it to our application/server so that Cloudflare can route requests to it over the internet.

In the DNS Records section of Cloudflare create 2 entries for the same -

  1. ‘A’ record for your root domain -

    | Type | Name | Content | TTL | Proxy status | | --- | --- | --- | --- | --- | | A | @ | <your_droplet_public_ip> | Auto | Proxied |

  2. CNAME record for the "www" subdomain -

TypeNameContentTTLProxy status
CNAMEwww<your_domain>, ex - noyap.fooAutoProxied

Once you save these records and they are propagated in a while you can check on the dns resolution with commands like dig or nslookup

💡
Make sure the records are proxied via Cloudflare (orange coloured) in the dashboard.

Networking setup

Once we have the domain, we would want to also use a secure connection protocol to relay the traffic between the user to our app. The interesting piece is with Cloudflare in the middle acting like a proxy for the user to route traffic to our origin server, so it allows us to define different ssl modes. It might be okay to connect over HTTP between Cloudflare to your origin server in some cases. I have chosen Full (Strict) mode for learning purposes. You can check out more here - https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/#custom-ssltls

As Cloudflare provides free Origin CA certificates, I went ahead with it finally, to keep things simple. You can create one in the SSL/TSL section of your Cloudflare dashboard. Ref - https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/, You have to copy and store the certificates in your server’s /etc/ssl folder.

💡
I also explored other options beforehand for setting up a origin certficate for my server. Initially I tried using certbot + letsencrypt for managing certificates automatically on my server. This needed some additional setup on the server side which is manageable. The problem however was with allowing traffic on port 80 for certbot to automatically manage/renew certificates. This would require me to relax my ufw and Digital Ocean network firewall rules.
💡
I also tried setting up a Cloudflare secure tunnel to my server, which doesn’t require any outbound traffic relaxation in your server. This is a bit advanced and needs running a cloudflare daemon in the server. However, I faced issues with the daemon as it was using 100% CPU for some reason and I didn’t want to deal with it.

Nginx configuration to only accept HTTPS traffic -

server {
    server_name <domain> www.<domain>;

    location / {
        proxy_pass http://localhost:8080/api/trivia;  # Your Go app's address
        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;
    }

    listen 443 ssl; 
    ssl_certificate /etc/ssl/certs/cloudflare.pem; 
    ssl_certificate_key /etc/ssl/private/cloudflare.key;
}

Also, we would want to block any access to our application via direct IP and reroute HTTP calls to HTTPS, for which we can add the below pieces.

server {
    listen 80;
    listen 443 ssl;
    server_name <your_droplet_ip> # Public ip

    ssl_certificate /etc/ssl/certs/cloudflare.pem;
    ssl_certificate_key /etc/ssl/private/cloudflare.key;

    return 444;  # No response for direct IP access
}

server {
    if ($host = www.<domain>) {
        return 301 https://$host$request_uri;
    }

    if ($host = <domain>) {
        return 301 https://$host$request_uri;
    }

    listen 80;
    server_name <domain> www.<domain>;
    return 444;
}

Run sudo nginx -t and sudo systemctl restart nginx for these to take effect.

Also, we will need to update our ufw config to deny incoming traffic on port 80 HTTP now and allow on 443 for HTTPS. For this we can first run sudo ufw status numbered followed by sudo ufw delete <rule_number_for_allow_in_port_80> to delete the incoming HTTP traffic rule.

💡
Additionally, I also wanted to only allow any incoming calls to my server only from Cloudflare servers and not from any other place. To achieve this we can enhance our ufw rules to only allow HTTPS traffic from selected IPs/IP ranges which belong to Cloudflare. Check here -https://www.cloudflare.com/ips/
#!/bin/bash

CLOUDFLARE_IPV4=(
    "173.245.48.0/20"
    "103.21.244.0/22"
    "103.22.200.0/22"
    "103.31.4.0/22"
    "141.101.64.0/18"
    "108.162.192.0/18"
    "190.93.240.0/20"
    "188.114.96.0/20"
    "197.234.240.0/22"
    "198.41.128.0/17"
    "162.158.0.0/15"
    "104.16.0.0/13"
    "104.24.0.0/14"
    "172.64.0.0/13"
    "131.0.72.0/22"
)
CLOUDFLARE_IPV6=(
    "2400:cb00::/32"
    "2606:4700::/32"
    "2803:f800::/32"
    "2405:b500::/32"
    "2405:8100::/32"
    "2a06:98c0::/29"
    "2c0f:f248::/32"
)

# Allow HTTPS (port 443) only from Cloudflare IPs
for ip in "${CLOUDFLARE_IPV4[@]}"; do
    sudo ufw allow from $ip to any port 443
done

for ip6 in "${CLOUDFLARE_IPV6[@]}"; do
    sudo ufw allow from $ip6 to any port 443
done

sudo ufw reload
💡
On top of this I have also configured a network firewall for my droplet in Digital Ocean dashboard with the same rules, to only allow SSH and incoming HTTPS traffic from Cloudflare IP ranges. The added advantage is that anything not permitted will get dropped by Digital Ocean first and won’t even reach my droplet/server, this saves me overhead of unnecessary resource consumption in case of spam or direct IP accesses.

This should set up access to our application over HTTPS, which is routed via Cloudflare securely to our Digital Ocean droplet and served via the NGINX server running on it. You can test it by hitting - https://<your_domain> or www.<your_domain> on the browser or by making a curl request.


Additional - setting up resource alerts

It’s probably also a good idea to set up resource usage alerts for your droplet. It’s pretty easy to set one up in the Monitoring section on the Digital Ocean dashboard. Another option is to also set Uptime checks for your application, which is also available. Usages and charges can be tracked in the Billing section which also shows Outbound data transfer amount. For a $4 droplet the allocated amount is 500GB/month beyond which it is charged at a defined rate, hence something to keep an eye on.

That’s it for this post. If you have read till here then consider reading the 2nd part of it (will be published later) which is to deploy a react application with Cloudflare pages and tie it together to the api hosted in the droplet. You can check out the final output here - noyap.foo. Why the name? Because it’s tiring to come across meaningless yapping on public forums. Good choice? Idk