Restarting a Cloud Server

May 1, 2025 Ā· Tyler Yeager Ā· 7 min reading time

Featured Image of blog post

Many of the applications I write for myself are small. In some way, I’ll need something done and I won’t have any convenient way of doing it. For example, restarting a server I am using through service Y.

What I will do first is go through their web app and do it manually. This will generally be the last mile of the automation. I might have automation for alerting me that I need to restart a server, but I won’t actually be able to restart it. Then, I’ll probably look to see if they have an API. If they do (awesome), then the API is fully featured and I can make use of it. Unfortunately, this can also make me feel overwhelmed. How can I implement this? There’s way too many things I would have to incorporate.

So, This is the pattern I like to call ā€œOne Trick Appsā€. It has a minimal backend, a minimal frontend, and some kind of authentication between the two. They are generally not shared.

Overview

  1. The Frontend does an HTTP request to the Backend, with parameters and a unique authorization token.
  2. The Backend parses the request and makes an HTTP request to the API of the Service with a unique scoped token.
  3. The Backend gets a response, parses it and returns this response to the frontend.
  4. The Frontend gets a response, parses it and displays this response to the user.

An Example

I need to do something specific, so for this example, I will go with restarting a server from Linode.

First, I need to get an API token. If possible, I’ll scope it to the areas I need, like ā€œLinodesā€ in this example. Since I will be issuing an update (or write), I’ll need Read/Write Access for that.

Great. Now, I can open this up in an API development app like Postman or Insomnia. There is a big field of apps in this space, For CLI you can try httpie or the absolute original: curl. Curl is the defacto app for nearly all testing and probably will be for a very long time.

With this, I can now view the API page. Knowing exactly what you want can be a little confusing. I definitely recommend searching around with keywords like ā€œserviceā€ ā€œintentā€ and ā€œapiā€ to get an idea of what you need to use. In this case, we’re going to use Reboot a Linode.

Many API guides will also cover Authentication. You don’t want just anyone to be able to restart your server, right? So when making your requests, it will follow a structure of:

  • HTTP Method (GET POST & Friends)
  • Authentication (Bearer in this case)
  • Url, such as /linode/instances/(linodeId)/reboot

The next part I’ll run into is understanding what I need past this. For example, what is a (linodeId)? If you go into the web app and click on your linode and look in the browser, you will often find a unique way to reference the page you’re off. This is probably your id.

Let’s say our linode id is server932 and our personal token is 5fdd613b60ff.

Terminal window
curl --request POST \
--url https://api.linode.com/v4/linode/instances/server932/reboot \
--header 'Accept: application/json' \
--header 'Content-type: application/json' \
--header 'Authorization: Bearer 5fdd613b60ff'

Submitting this will effectively restart your server. So beware. Once you’ve confirmed it’s working then the core piece:::: of your project is done.

Build the backend

Next, we’re going to build out the backend, stuff our token in it and create an endpoint for our client app to use. If you want to host your own server, that’s fine. Spin up a Node Express app, or web server in the language of your choice. There are multiple cloud services available that do this as well. However, they are generally in Javascript.

We’re going to use val.town, a free / paid service for running tiny javascript endpoints. The real advantage here is we’ll quickly have something publicly available. Otherwise, this will be identical for other hosting methods.

With a new script, we’ll use Hono, a very minimalistic web server. This is the basic setup.

app.js
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono!'))
export default app

Let’s shift things a bit for our purpose. We’ll need to do the following:

  1. Accept a query parameter (eg ?linode=server932).
  2. Verify it is our app, using a unique authentication token.
  3. Make a request to the Linode API.
  4. Return a response.

We’re also not going to use any built in integration, so you can clearly see what is happening.

In the first part of the below code, we are gathering the Authorization header from the request. Since the request will look like Bearer token, we split it a and take the second value. If it doesn’t match, we return status code 401. Note that doing a return r ends the request. Further code will not be processed.

Next, we take the query parameter server. If it is missing s , we return a status code of 400, along with a short description of the issue.

Finally, we return a successful response. We’re also using a format called jsend. This works well for general API’s like this one.

app.js
import { Hono } from "npm:hono";
const app = new Hono();
app.get("/", c => {
return c.text("Hello World");
});
app.post("/server", c => {
const authHeader = c.req.header("Authorization");
console.log({ authHeader });
if (!authHeader || authHeader.split(" ")[1] !== "5fdd613b60ff") {
c.status(401);
return c.json({
status: "failure",
message: "Not Authorized",
});
}
const server = c.req.query("server");
if (!server) {
c.status(400);
return c.json({
status: "failure",
message: "Missing server query parameter",
});
}
return c.json({
status: "success",
message: `restarted server ${server}`,
});
});
export default app.fetch;

While this is functional and doesn’t do what we want yet, this is a great starter and covers everything the client will need to do to operate it. We’ll revisit the backend at the end, pulling it all together.

Build the frontend

We’re going to write this in golang. It will accept a single argument, the server id. When you run it, it will contact our backend, which will restart the server for you. Because we wrote it with responses using the jsend format, it should be easy to parse.

We need to do a few things to be successful at this.

  1. Have a main function where everything is run.
  2. Collect information from the user (what’s the server id?).
  3. Make a network request with that information.
  4. Display the response to the user.

Here’s the code in full. Yes, compiling the code with the auth token t is bad. Instead, you can use a settings file, or define it each time with a flag. We’ll include this in a moment. Since we expect our response to always have a status and message, we can define r that type.

It is critical that we get the server id from the user. So we verify o that there is one argument provided (os.Args[0] is always the app name).

There’s a lot of boilerplate for making a http request. There are shorter ways of doing it, but this is very flexible. Effectively, we create a new client, set the query params q , set the headers and then tell it to ā€œdoā€ the request (client.Do(req)).

We want to make sure we close reading the body stream b . So we defer closing it. If there is an error, there won’t be a body, it might be nil, so we check for that.

Assuming the response code is 200, we create a ResponseBody with the contents and p print out the message.

main.go
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const url = "https://tyler71-restart-cloud-server-post.val.run/server"
const authToken = "5fdd613b60ff"
type ResponseBody struct {
Status string `json:"status"`
Message string `json:"message"`
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Provide a server id, example ./restart_linode_server server955")
return
}
server := os.Args[1]
req, err := http.NewRequest("POST", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
q := req.URL.Query()
q.Add("server", server)
req.URL.RawQuery = q.Encode()
req.Header = http.Header{
"Content-Type": {"application/json"},
"Authorization": {"Bearer " + authToken},
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer func(Body io.ReadCloser) {
7 collapsed lines
if Body != nil {
err = Body.Close()
}
if err != nil {
fmt.Println("Error closing body", err)
}
}(req.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
if resp.StatusCode == http.StatusOK {
var response ResponseBody
err = json.Unmarshal(body, &response)
fmt.Println(response.Message)
return
} else {
fmt.Println(string(body))
return
}
}

Let’s build it and run it.

Terminal window
go build -o restart_linode_server main.go
./restart_linode_server server932
restarted server server932

This completes the boilerplate. Our golang cli app contacts the server, the server does ā€œthingsā€ and returns the response to us, which we display to the user.

Pulling It All Together

Let’s make it functional. All we need to do is go into the server, call the right API and return the actual response. If you recall, here’s the query we needed to actually restart the server:

Terminal window
curl --request POST \
--url https://api.linode.com/v4/linode/instances/server932/reboot \
--header 'Accept: application/json' \
--header 'Content-type: application/json' \
--header 'Authorization: Bearer 5fdd613b60ff'

Let’s convert this into an async TypeScript function.

const apiUrl = "https://api.linode.com/v4"
async function restartLinodeServer(authorization: string, serverId: string): Promise<boolean> {
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${authorization}`,
}
const res = await fetch(`${apiUrl}/linode/instances/${serverId}/reboot`)
return res.ok
}

Finally, we can drop this in and use it. Because we have the potential to fail, we also check for this when r returning our response.

main.ts
import { Hono } from "npm:hono";
const app = new Hono();
const apiToken = "450331c3"
const apiUrl = "https://api.linode.com/v4"
async function restartLinodeServer(authorization: string, serverId: string): Promise<boolean> {
7 collapsed lines
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${authorization}`,
}
const res = await fetch(`${apiUrl}/linode/instances/${serverId}/reboot`)
return res.ok
}
22 collapsed lines
app.get("/", c => {
return c.text("Hello World");
});
app.post("/server", async c => {
const authHeader = c.req.header("Authorization");
console.log({ authHeader });
if (!authHeader || authHeader.split(" ")[1] !== "5fdd613b60ff") {
c.status(401);
return c.json({
status: "failure",
message: "Not Authorized",
});
}
const server = c.req.query("server");
if (!server) {
c.status(400);
return c.json({
status: "failure",
message: "Missing server query parameter",
});
}
const serverRestarted = await restartLinodeServer(apiToken, server)
if(serverRestarted) {
return c.json({
status: "success",
message: `restarted server ${server}`,
});
} else {
return c.json({
status: "failure",
message: `failed to restart server ${server}`,
});
}
});
export default app.fetch;

Saving and running all of this.. promptly fails. The tokens are fake and the server id’s are fake. So, that’s to be expected. But with tweaks to make it live, you may now speedily restart any linode servers in one command.

I’ll create a new post at some point (or update this one) with a simple skeleton for a backend / frontend to freely copy for use.

Did you find this interesting?

Consider subscribing 😊

No AI-generated content or SEO garbage.

Unsubscribe anytime