Azure vs Hetzner VM Performance

This is a simple comparison between two virtual machines specced identically with regards to number of cpus and memory: 4 vCPUs and 16 GB RAM.

The VM types are CCX23 (Hetzner) and Standard D4ds v4 (Azure)

Both VMs had Ubuntu 2022 LTS as OS. They were benchmarked using YABS.

VMSingle-Core scoremulti-core scoremonthly priceperformance/price ratio
D4ds v412822864$16417,5
CCX23 18974338$30144,6

The Hetzner VM clearly wins this benchmark test, even though it is a lot cheaper than the Azure one. This is further emphazised when calculating the performance/price ratio (based on multi-core)

Hetzner does arguably not offer as many different cloud products or ease of scalability compared to Azure, but if you are willing to invest some time into setting things up and doing maintenance yourself, there sure are money to be saved and a lot to be gained in performance.

The following is the links to the full Geekbench test reports for the VMs.

Hetzner VM

Azure VM

Roblox Voice Chat ID Verification – A slightly more data-privacy friendly way

If you are feeling a bit uneasy about the verification process for enabling voice chat in Roblox, you are not alone. Roblox Corporation should not require you to blindly scan one of your most important documents and upload it to them just to verify your age. However, since verifying your age is their objective, I figured that is must be OK if I partly mask some of the sensitive information present in a passport typically used for identity theft (such as the social security number). Turns out, it worked.

The last digits of the social security number and the passport document number were simply covered using the adhesive part of a post-it like this:

It still feels a bit overkill for them to require a scan of an identity document, but at least it seems to be OK with them to cover up some sensitive parts.

DinkToPdf in Docker

To generate PDFs from HTML using DinkToPdf in a linux based container, the library is required.

One way to do this is to simply include libwkhtmltox in your version control system, but it takes up a lot of space (43 MB) thereby increasing the time for initial checkouts considerably. A better way, if your app is containerized, is to let the Dockerfile define how to fetch libwkhtmltox when building the base image for your app.

The following examples are based on the lightweight Debian linux image, more specifically “aspnet:5.0-buster-slim”.

Due to its slimmed down nature, the “aspnet:5.0-buster-slim” image does not even have wget installed, so we will have to install that along with other dependencies needed by wkhtmltopdf. Add the following to the “base” stage in your Dockerfile:

RUN apt update
RUN apt install -y libgdiplus
RUN ln -s /usr/lib/ /lib/x86_64-linux-gnu/
RUN apt-get install -y --no-install-recommends zlib1g fontconfig libfreetype6 libx11-6 libxext6 libxrender1 wget gdebi
RUN wget
RUN gdebi --n wkhtmltox_0.12.5-1.stretch_amd64.deb
RUN ln -s /usr/local/lib/ /usr/lib/

Libgdiplus is a replacement for System.Drawing required by DinkToPdf to work in a linux environment.

It is important that symbolic links are made to and libwkhtmltopdf in /usr/lib. Many examples of getting DinkToPdf to work save the files in the /app directory but this will not work while debugging the dockerized app in Visual Studio. Downloading to the /app dir using wget seems to works without errors when monitoring the build output, but the file simply does not appear in the /app directory in the final image. The reason for this is, that in Visual Studio, the /app directory is a Volume mapped to the directory on the host machine containing the source code. A little gotcha that can take a few hours of debugging to track down 🙂

The following is a complete Dockerfile utilizing the lines from above to enable PDF generation inside a Debian linux based Docker container.

FROM AS base

RUN apt update
RUN apt install -y libgdiplus
RUN ln -s /usr/lib/ /lib/x86_64-linux-gnu/
RUN apt-get install -y –no-install-recommends zlib1g fontconfig libfreetype6 libx11-6 libxext6 libxrender1 wget gdebi
RUN wget
RUN gdebi –n wkhtmltox_0.12.5-1.stretch_amd64.deb
RUN ln -s /usr/local/lib/ /usr/lib/
FROM AS build
COPY [“BooksOnline.PdfService/BooksOnline.PdfService.csproj”, “BooksOnline.PdfService/”]
RUN dotnet restore “BooksOnline.PdfService/BooksOnline.PdfService.csproj”
COPY . .
WORKDIR “/src/BooksOnline.PdfService”
RUN dotnet build “BooksOnline.PdfService.csproj” -c Release -o /app/build

FROM build AS publish
RUN dotnet publish “BooksOnline.PdfService.csproj” -c Release -o /app/publish

FROM base AS final
COPY –from=publish /app/publish .
ENTRYPOINT [“dotnet”, “BooksOnline.PdfService.dll”]

HTTPS Redirect with .htaccess

Creating a simple HTTPS redirect rule for Apache can potentially be a long, tiresome journey of trial and error.
There are many ways to write the rule with subtle syntax differences and other factors such as the version of the Apache server also affecting the outcome.
The following snippet is what finally worked for me on Gigahost (a shared web hosting provider in Denmark).

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{ENV:HTTPS} !on
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Forcing Retrieval of Images in ASP.NET/MVC

Browsers cache images in order to speed up page load times. If an action on a controller generates dynamic images, this can be a problem, since the user will only see the first image fetched by the browser. One way to force the browser to by-pass its cache and actually retrieve the image is to append a random parameter to the URL for the action.
For example: A user uploads an image to the server and the server subsequently returns some HTML showing the image. The user then performs an action which triggers the server to alter the image in some way (for example cropping or resizing), and returns the HTML once again. However, the browser sees that the link to the image is the same, and therefore it simply shows the initial image which it has already cached.

So, instead of generating the image URL with razor syntax like this:

<img src="@Url.Action("GetPhoto")" />

Simply add some random parameter (or just anything the browser has not seen before) to force it to download the image again:

<img src="@Url.Action("GetPhoto", new {ticks = DateTime.UtcNow.Ticks})" />

Extracting Image Files from SQL Server

There are a number of different plugins and tools which can export image data in hex format from an MS SQL server to an actual file on the harddrive. If you have Visual Studio installed, however, the easiest way is to just write a small console program to select the data from the image column and save it to a file. For example:

static void Main(string[] args)
SqlConnection thisConnection = new SqlConnection(@"Server=,1433;database=MyDb;User id=sa;Password=JdjdUD78!;");
SqlCommand thisCommand = thisConnection.CreateCommand();
thisCommand.CommandText = "SELECT ImageData FROM [MyDatabase].[dbo].[Images] where ID = 1";
byte[] imgBytes = (byte[]) thisCommand.ExecuteScalar();
FileStream fs = new FileStream(@"C:\temp\image.jpg", FileMode.CreateNew, FileAccess.Write);
fs.Write(imgBytes, 0, imgBytes.Length);
catch (SqlException e)

Basic SQL Server Query in C#

If you need to access a database on an MSSQL server programmatically (in C#) just to do some quick and dirty data extraction and perhaps manipulation, the following code snippet will get you started. Replace the connection string, table and columns names with whatever is appropriate for your task.

static void Main(string[] args)
SqlConnection thisConnection = new SqlConnection(@"Server=,1433;database=NorthWind;User id=sa;Password=HIUHFuhf8;");
SqlCommand thisCommand = thisConnection.CreateCommand();
thisCommand.CommandText = "SELECT [ID], [Name] FROM Customers";
SqlDataReader thisReader = thisCommand.ExecuteReader();
while (thisReader.Read())
Console.WriteLine("\t{0}\t{1}", thisReader["ID"], thisReader["Name"]);
catch (SqlException e)

Subversion revision number in AssemblyInfo.cs

Pre-requisites: Tortoise SVN, Visual Studio
  • Exclude the existing “AssemblyInfo.cs” file from Subversion
  • Rename “AssemblyInfo.cs” to “AssemblyInfo.template
  • Add the Subversion revision keyword to the template file. For example: [assembly : AssemblyVersion(“1.0.0.$WCREV$“)]
  • Add SubWCRev.exe from the TortoiseSVN bin directory to the project (or add the Tortoise SVN bin folder to your path)
  • In the pre-build event for your project, add the following line (You may need to adjust it to your own project structure): "$(SolutionDir)\lib\SubWCRev\subwcrev.exe" "$(SolutionDir)." "$(ProjectDir)Properties\AssemblyInfo.template" "$(ProjectDir)Properties\AssemblyInfo.cs"
  • Done! The “AssemblyVersion.cs” file should now be auto-generated on every build and the $WCREV$ keyword is replaced with the current SVN revision number on the solution directory.

XBMC + TVHeadEnd + HDHomeRun


How to install and configure TVHeadend and HDHomerun on an OpenELEC machine.


HDHomerun Driver


Configure HDHomerun Driver

Change directory to /storage/.xbmc/addons/driver.dvb.hdhomerun/config
Make a copy of dvbhdhomerun.sample to dvbhdhomerun.conf

Open \\xbmc\Userdata\addon_data\driver.dvb.hdhomerun\adapters and copy the IDs of the two tuners to dvbhdhomerun and set their types (just follow the instructions in the config file.). Alternatively, run userhdhomerun to get the IDs.


Configure TVHeadend

Go to webpage of TVHeadend: http://:9981
Go to Configuration -> DVB Inputs -> TV Adapters

Select the first adapter.
Click “Add DVB Network by Location”
Choose your location
Click “Enabled”
Click “Save”
TVHeadend now starts scanning for channels. You can monitor its progress in the pane to the right. When “Muxes awaiting initial scan” is zero, it’s done.
Click “Map DVB Services to Channels”.
The mapping will also take some minutes to complete depending on how many channels are present.

In the web config in Configuration -> TV Adapters: After all channels have been configured, disable “Autodetect Muxes” and “Idle Scanning“. Having these enabled completely destroyed the stream from TVHeadEnd, making the image look garbled and stuttering. It looked like a low bandwidth connection or bad signal.