the avatar image of Benjamin Bouvier

Hetzner: cheap auction servers, and how to find them

Lately, I’ve had to find a hosting server to replace the server we rented for delire.party, our self-hosting collective of friends. Indeed, OVH decided, with no extra explanations, to decomission our Kimsufi server at the end of the year, and to have us pay a few euros more per month until then. This kind of practices irritates me, so I’ve decided to pull out the plug (metaphorically speaking — the OVH server will still be online for the time of the migration) and have our next server live at Hetzner.

Hetzner is a German company, they’ve been relying on renewable energy for a while, and they have this interesting system of server auctions, with prices lowering until a server has been rented out. For a regular human being, that would mean checking the website multiple times a day to find a cheap server with the right specification. I, for one, am a nerd, so I’ve looked for ways to automate this.

There are a few scripts for this on the Internet, but I made a relatively short one that was perfectly suited for my constraints. I wanted to only see servers with a price tag less than 50€ per month, and at least 2TB of NVME storage. I also wanted email notifications, so that I can stay in the comfort of my email client to check out the new deals. The script doesn’t account for deals it’s already seen, but using it for three days (running it once per day) was sufficient to find a satisfying deal, so I didn’t even have to bother implementing that.

The script is written in JavaScript (based on the one I’ve taken inspiration from), and it has two dependencies: request to run the HTTP requests, and nodemailer to send me the email.

var nodemailer = require("nodemailer");
var request = require("request");

var dataUrl = 'https://www.hetzner.com/_resources/app/jsondata/live_data_sb.json';

var config = {
    smtp_host: "mail.example.com",
    smtp_port: null,
    smtp_username: null,
    smtp_password: null,
    smtp_accept_unauthorized: true,
    email_from: "hetzner-deals@example.com",
    email_to: "your-email@example.com",
}

if (!config.smtp_host) {
    console.error("Please specity SMTP host");
    process.exit(1);
}

if (!config.email_to) {
    console.error("Please specify the email address to send notifications to");
    process.exit(1);
}

var transporter = nodemailer.createTransport({
    host: config.smtp_host,
    port: config.smtp_port,
    auth: {
        user: config.smtp_username,
        pass: config.smtp_password
    },
    tls: {
        rejectUnauthorized: !config.smtp_accept_unauthorized
    }
});

var jar = request.jar();
request = request.defaults({ jar: jar });

request.get(dataUrl, (error, resp, body) => {
    if (error || resp.statusCode !== 200) {
        console.error("Error: " + error);
        console.error("Response code: " + resp.statusCode);
        process.exit(2);
    }

    let json = JSON.parse(body);

    let results = [];
    for (let s of json.server) {
        // Here, `s` is the data representing a single server.
        // You can look at it and make up your own filtering
        // criteria based on its shape.
        // console.log(s);

        // This holds the total size of NVME storage.
        let sumNvme = s.serverDiskData.nvme.reduce((a, b) => a + b, 0);

        // Rough approximation of the monthly price, based on the hourly price.
        let monthlyPrice = s.hourly_price * 24 * 31;
        // renting an IPv4 costs around 2.02/month
        monthlyPrice += 2.02;

        // round to second decimal
        monthlyPrice = ((100 * monthlyPrice) | 0) / 100;

        if (sumNvme >= 2000 && monthlyPrice < 50) {
            results.push(`- ${s.id}:`);
            results.push(`    RAM=${s.ram_size}GB`);
            results.push(`    NVME=${sumNvme}GB`);
            results.push(`    PRICE=~${monthlyPrice}`);
            results.push(`    (${s.information})`);
            results.push('');
        }
    }

    var mailOptions = {
        from: config.email_from,
        to: config.email_to,
        subject: "Hetzner Server deal found",
        text: "Found deals:\n\n" + results.join('\n'),
    };

    transporter.sendMail(mailOptions, function(error, info) {
        if (error) {
            console.error("Error sending email notification: " + error);
            process.exit(3);
        } else {
            console.log('Email notification sent: ' + info.response);
        }
    });
});

Hope this may be useful to some of you too!