Saving VPS Costs with Bash and Cloudflare Workers

Dec 14, 2023

What is a VPS?

Well, it expands to Virtual Private Server1, which is often a virtual machine sold as a service by an Internet hosting service. In our case, it is a tiny Linux box running a WireGuard2 VPN (Virtual Private Network) service, set up in minutes with Algo3.

How did you do it?

Before we get into the how, let’s start with the why :) Running virtual machines in the cloud incur costs, also, some of these are running inside datacenters not powered by 100% renewable energy. So, turning off your resources when not in use where possible is a win-win situation.

Now, I have this Linux box in the cloud running a VPN service as mentioned. However, I do not want to have it running all the time, which is why I saved below script4 in /opt/scritps/poweroff-if-no-ssh-connection.sh on the server.

if last | grep "still logged in"; then
  exit 0
fi

LAST_ACCESS="$(stat -c'%X' /var/log/wtmp)"
CURRENT_TIME="$(date +%s)"
DIFF="$((CURRENT_TIME-LAST_ACCESS))"
if [ $DIFF -ge 300 ]; then
  sudo poweroff
fi

What the above script does is poweroff the VM instance if no users are currently logged in via ssh and it’s been 300 seconds since the last ssh user logged out.

Then I added below inside the server’s cron configuration.

# save the planet!
* * * * * /opt/scritps/poweroff-if-no-ssh-connection.sh

Cool, now our VM instance will turn itself off when above criteria is met. But how do we turn it on again? it’s now that Cloudflare Workers5 enter the chat. What we’ll be doing is deploy a Cloudflare Worker that listens on a specific endpoint, and turns on the VM instance for us by calling the cloud provider’s API.

Below is the code I have in the Worker deployment.

		const apiUrl = '<cloud-provider-api-url>';
		const accessToken = '<cloud-provider-api-token>;

		const requestHeaders = {
			'Content-Type': 'application/json',
			'Authorization': `Bearer ${accessToken}`
		};

		const requestBody = {
			type: 'power_on'
		};

		/**
		 * gatherResponse awaits and returns a response body as a string.
		 * Use await gatherResponse(..) in an async function to get the response body
		 * @param {Response} response
		 */
		async function gatherResponse(response) {
			const { headers } = response;
			const contentType = headers.get("content-type") || "";
			if (contentType.includes("application/json")) {
				return JSON.stringify(await response.json());
			} else {
				return response.text();
			}
		}

		const init = {
			body: JSON.stringify(requestBody),
			method: "POST",
			headers: requestHeaders,
		};

		const response = await fetch(apiUrl, init);
		const results = await gatherResponse(response);
		return new Response(response.status.toString());

Now, what we have to do is just call the specific CF Worker endpoint to turn on the VM—an HTTP GET in my case.

What else?

Sure, I could’ve just created a bookmarklet6 with JavaScript sending an HTTP POST directly to the cloud provider’s endpoint, but I wanted to check out Workers as well, so ended up with this. The second point being deploying a CF Worker may add a Carbon footprint which we are trying to reduce by turning off the VM instance in the first place. However, my guess is that the Workers are deployed in CF’s already running software-defined network, and the footprint delta is still positive. Further, a third point being why we are not destroying the instance and recreating it when necessary (GitOps FTW!), which I would look into—based on the application and persistance requirements, maybe with a different instance.


  1. https://en.wikipedia.org/wiki/Virtual_private_server ↩︎

  2. https://wiki.archlinux.org/title/WireGuard ↩︎

  3. https://github.com/trailofbits/algo ↩︎

  4. Shoutout to Abin Simon for https://blog.meain.io/2020/auto-shutdown-no-ssh/ ↩︎

  5. https://developers.cloudflare.com/workers/ ↩︎

  6. https://en.wikipedia.org/wiki/Bookmarklet ↩︎

LinuxBashCloudflareNetworking

Writing your own Static Site Generator

Upgrading Portable Visual Studio Code