blog.iankulin.com

Possibly-Useful

Express router for better code organisation

In this beginner-level walkthrough, I tidy up a Node/Express app whose server.js has grown unwieldy by moving routes into separate files with Express Router. I show how to mount routers as middleware with app.use, note that a routes folder is a common convention, and highlight the gotcha that the mounted path prefix gets stripped from the request URL. A consistent naming convention for route groups makes the split easier, and sample code is available on GitHub...

A bit of web-scraping with Cheerio

The ‘show all’ button on The Rest Is History podcast’s episodes page is broken, and there’s no JSON endpoint behind the player, so the episode list has to be scraped from the page’s HTML. Using Cheerio, a jQuery-like library for the server, I show how to select the playlist’s list items and extract each episode’s title and link, ending with a complete working script...

Command chaining with NTFY for long running commands

Using ntfy.sh, shell commands can trigger a push notification to a phone or watch via curl when they finish. I demonstrate chaining long-running commands like rsync with && and || to send success or failure messages, running everything in the background with nohup, and redirecting the curl output to a log file...

Share files securely with Enclosed

I walk through self-hosting Enclosed on a VPS as a secure alternative to emailing password-protected zip files. I cover the Docker Compose setup behind Nginx Proxy Manager, along with configuring authenticated logins, a step the official docs leave under-explained, so only I can upload files while recipients download them without an account...

Moving a Docker image as a file

When my home internet connection can’t reach DockerHub, I work around it by pulling the Jellyfin image I need on a laptop tethered to a phone hotspot, then moving it to the server as a file using docker save and docker load. The post also includes a short explanation of how container images are built from layers described by a manifest...

Perils of Benchmarking

After choosing the tiny BusyBox httpd server for containerised websites, I encountered third-party benchmarks suggesting NGINX was dramatically faster. Running my own A/B tests with Apache’s ab tool initially gave misleading results, which turned out to be NGINX Proxy Manager serving cached responses. With cache-busting requests, BusyBox and NGINX performed similarly over the network, so I am sticking with the 1.35MB BusyBox containers over the roughly 49MB NGINX-alpine images...

Moving a domain from Wordpress

A step-by-step guide to moving my domain registration from WordPress.com to Porkbun, motivated by my unease over recent WordPress drama. I cover the transfer process—including a required 60-day wait, disabling the transfer lock, and a gotcha where private registration had to be turned off before the authorization code would work—plus copying DNS records and switching nameservers. In a closing update, I note that WordPress later blocked external domain pointing, pushing me toward self-hosting...

Updating a deployment on fly.io

Hosting UptimeKuma on Fly.io’s free tier, I find that updating an out-of-date container image requires nothing more than running fly deploy with an existing fly.toml file. After the update the app starts crashing from memory exhaustion, and switching to a lighter Alpine-based image appears to keep usage within the free instance’s 256MB limit...

Controlling Docker container startup order

How I make one Docker Compose service wait for another to be ready, using a healthcheck on the depended-on service and a depends_on condition with service_healthy on the dependent one. The worked example adds InfluxDB to my homelab monitoring stack so a Go metrics collector starts only after the database is up, with sample compose YAML included...

Fixing TLS for wget in BusyBox

A BusyBox container serving a static site can’t fetch content over HTTPS because BusyBox ships without root CA certificates and has no package manager to install them. Rather than switch to a larger Alpine image, I settle on bind-mounting the host’s certificate directory into the container read-only, with example configurations for both docker-compose and docker run...

Fancier Website in a Docker Container

A follow-up on bundling static sites into BusyBox Docker containers, covering how to handle minor dynamic tasks such as periodically downloading an image into the directory of hosted static files. Rather than using cron, which behaves unexpectedly on BusyBox, my solution is a shell script with a download-and-sleep loop that runs in the background alongside the httpd server. I also note that BusyBox’s wget has TLS problems, a topic I defer to a later post...

Website in a Docker Container

Serving static websites from small Docker containers built on BusyBox httpd, coming in at around 4MB each. I walk through the Dockerfile setup, cross-platform builds from an M1 Mac to linux/amd64, pushing to the GitHub Container Registry, and deploying via docker-compose behind Nginx Proxy Manager, including DNS and SSL configuration...

rsync between Synology NAS

After abandoning an overly complicated LXD-based file transfer setup, I walk through using command-line rsync to sync files directly between two Synology NASes, including over Tailscale. I cover setting up passwordless SSH without ssh-copy-id, working around DSM 7’s restriction on Tailscale outbound connections and its lack of Magic DNS, enabling rsync in the Synology interface, excluding metadata and recycle bin directories, fixing permissions, throttling bandwidth, deleting remote files in one-way syncs, and running the job in the background with nohup...

Containerised NGINX Proxy Manager & the 502 error

Running NGINX Proxy Manager in a Docker container breaks the usual habit of pointing proxies at 127.0.0.1, since localhost now refers to inside the container rather than the host. I explain why, cover the confusing exception of NPM’s own admin interface, and show how joining service containers to NPM’s Docker network lets the proxy reach them by container name via DNS, with no ports exposed to the host...

dockerfile - CMD vs ENTRYPOINT

I offer a short explainer on the difference between ENTRYPOINT and CMD at the end of a Dockerfile, with examples showing that command-line arguments replace CMD entirely but are appended to ENTRYPOINT. I also cover combining the two, and note that both execute at container launch, unlike RUN, which happens during the image build...

User environment variables are not available in cron

Docker environment variables set for a container aren’t available to scripts run by cron inside it, even when cron runs as the same user. After confirming the cause and finding no clean fix, I settle on a workaround: saving the needed variable to a file in the entry point script and reading it back from the cron job...

Outputting to the console, in Docker, from a cron job

How to get console output from a cron job running inside a Docker container. I cover running cron in the foreground so the container doesn’t exit, and redirecting job output to /proc/1/fd/1 so it appears in the container’s stdout, since cron normally sends output to mail. I mention common gotchas like user permissions and file paths, with an example project on GitHub...

Peek inside a Docker image

A first-draft Dockerfile for my Node project, and how I keep the resulting image tidy by inspecting its contents with an interactive bash shell and adding entries to .dockerignore. I also explain my preference for copying everything over explicitly listing files, citing debugging time and security...

Hosting Your Own Docker Registry

I walk through hosting a private Docker registry on my homelab network using Docker’s official registry image, motivated by the free tier’s single-repository limit and Docker Hub’s pull/push rate limits. I cover setting up the registry with docker-compose, building a small test image, and pushing it to the registry without TLS by adding an insecure-registries exception to daemon.json. A third machine pulls and runs the image to confirm it all works...

Certbot - removing a domain

After I moved a domain to a new host, the original certbot certificate on the old host still included it, causing renewal errors. I explain that domains can’t be deleted from a certificate directly; instead you renew the certificate specifying only the domains you want to keep, and certbot warns about the ones being dropped. I include the commands for listing certificates and their domains, and for renewing with a reduced domain set...