A while ago I bought myself a PINE A64 Single Board Computer.
Just like a Raspberry Pi it is a small Single Board Computer with low power consumption.
Perfectly suited as a small home server for playing around with!
Initial Setup
Setting up the OS was pretty easy. Find the right image, flash it on a micro SD card, put it into the board and start it.
The OS I choose is already set up to allow initial setup over SSH.
I didn't even have to connect the board to anything except Ethernet.
I wanted to set up my server, so I could easily deploy new applications and update running ones.
My first thought was Kubernetes.
Getting a Kubernetes distribution running on a ARM64 system is not too difficult.
I tried a couple of different distributions and settled on k0s.
Deploying the applications was easy on paper.
Build a docker image, push it to a registry and declare the kubernetes resource for the application.
In practice, it was much harder.
First Deployment
I didn't want to release my half-finished applications to a public registry just to deploy it on my private server.
A private registry on a site like Docker Hub or GitHub Packages seemed like a better idea.
That didn't really work either because Docker Hub only allows one private package and GitHub locks all private registries behind a paywall.
Paying extra just to use my home server? Big nope.
Last straw was setting up my own private registry on the server. Defining the registry as a Kubernetes resource was easy.
Doing anything with the registry was impossible.
I couldn't push images to the registry, I couldn't get my Kubernetes to pull images through the registry.
The registry was running, but I could not get it to work.
Because the private registry didn't work I was limited to the one private package on Docker Hub.
Only being able to run one custom application kinda sucked.
At least I got one application to run though. A small website for tracking my progress on imabi.org.
Nothing too fancy, just a small website with oauth2 login, a postgresql database and a very simple data structure consisting of users, chapters and lessons.
That application was running for a year or so until I broke the whole server when I tried again to get the private registries to work. Frustrated by the fact that I seemed to be unable to get something as simple as a docker registry to work I turned off the server and let it collect dust.
Reviving the Server
A couple of days ago I remembered that I still have my server and I decided to give it another shot.
This time going for a more reasonable setup for deploying my application.
The first thing I had to do was flash a new OS because I somehow managed to break the server so hard it wouldn't boot anymore.
Luckily that was just as easy as it was the first time.
Next I had to install deno. I decided to use deno for my applications for a couple of reasons.
- TypeScript is a very good language
- Deno is much easier to install than a lot of other runtimes
- No need to cross-compile to arm64 or install a whole tool stack for compiling on the server
- The deno community is great
As a first test I wrote a small hono based web server. Currently, the server does basically nothing, but it is enough to see if its working. Deploying the service works by copying the source files to the server, bundling them together and restarting the systemd service that is already configured for the application. All of that works via SSH.
I wrote a small deno script that automates this process for me:
import { exit } from "https://deno.land/std@0.132.0/node/process.ts";
const HOST = "";
const USER = "" ;
const APP = "" ;
const REMOTE_DIR = `/home/${USER}/${APP}`;
const FILES_TO_COPY = [
`deno.json`,
`deno.lock`,
`bundle.ts`,
`src`,
];
function run(command: string, ...args: string[]) {
const output = new Deno.Command(command, {
args,
stdout: "piped",
stderr: "piped",
}).outputSync();
if (output.success) {
console.log(new TextDecoder().decode(output.stdout));
} else {
console.error(new TextDecoder().decode(output.stderr));
exit(output.code);
}
}
function runRemoteCommand(...args: string[]) {
run("ssh", `${USER}@${HOST}`, ...args);
}
function copyFilesToRemote(...files: string[]) {
run(
"scp",
"-r",
...files.map((f) => `${Deno.cwd()}/${f}`),
`${USER}@${HOST}:${REMOTE_DIR}/`,
);
}
console.log("Stopping running instance");
runRemoteCommand(`systemctl --user stop ${APP}.service`);
console.log("Copying files");
copyFilesToRemote(...FILES_TO_COPY);
console.log("Bundle app");
runRemoteCommand(`cd ${REMOTE_DIR} && deno task build`);
console.log("Start new instance");
runRemoteCommand(`systemctl --user start ${APP}.service`);
So whenever I make a change on my application I can simply run deno task deploy
. If I want to create a new application it is as easy as copying and adjusting the systemd service and the deployment script once.
I have to say I am pretty happy with the way that it just works.
Noob out 👋