How to Automatically Update a Fabric Minecraft Server with Node.js
I run a small modded Minecraft server for me and my friends on a VPS. Every time Minecraft updated, all of my dozen or so mods would have to be manually downloaded, the server had to be stopped, the mods put in the mods folder, and the server would have to be restarted. This worked, but it was repetitive and fragile. So, as any good software engineer does, I decided to spend a lot more time developing an automatic solution to a problem than I would have spent doing it manually. Along the way, I learned a thing or two about APIs, scraping, and designing small automation systems.
The Problem:
The manual process is as follows:
- Check each mod individually on Modrinth
- Download compatible versions
- Stop the server
- Replace the old mod files
- Update the Fabric loader
- Restart the server
The Solution:
I wrote a Node.js script with two different approaches for two different websites:
Approach 1: Web Scraping to Update the Fabric Loader
The Fabric project unfortunately doesn’t offer a direct API for their server launcher, so I used Microsoft’s Playwright framework to scrape Fabric’s website for a download link. I mainly chose Playwright because I am familiar with it from prior usage. If you’re following along at home, you might want to use a lighter framework, such as Axios, which I use later anyway.
Inspecting the HTML for https://fabricmc.net/use/server/, we can see that the game version selector has an ID of #minecraft-version, and the download button has text Executable Server. Thus, using a locator, we can very easily change the Minecraft version and download the proper .jar file as follows:
// Find and interact with the version selector
const gameVersionSelect = this.page.locator("#minecraft-version")
await gameVersionSelect.selectOption(this.config.minecraftVersion)
// Download the server launcher
const downloadLink = this.page.locator('a.button:has-text("Executable Server")')
const downloadPromise = this.page.waitForEvent("download")
await downloadLink.first().click()
Approach 2: Using the Modrinth API for Mod Management
Initially, I had the idea to use web scraping for Modrinth too. This soon proved itself to be overcomplicated for reasons I don’t have time to get into here.
Fortunately, Modrinth has an excellent REST API, making it very easy to update mods. To get the compatible versions for a mod, we can simply request the API using Axios:
// Get compatible versions for a mod
const response = await axios.get(
`https://api.modrinth.com/v2/project/${projectId}/version?${params}`,
)
const latestVersion = response.data[0] // First result is newest
Then, with latestVersion, we can download the mod:
// Download the latest version
primaryFile = latestVersion.files[0]
const response = await axios.get(primaryFile.url, { responseType: "stream" })
// Store the file as /var/minecraft/mods/{mod file name}
const filePath = path.join("/var/minecraft/mods", primaryFile.filename)
const writer = require("fs").createWriteStream(filePath)
response.data.pipe(writer)
Other Features
Automatic Backups before Changes
Automatically backs up the mods folder by copying it to a timestamped backup folder. For example, /var/minecraft/mods-backup/mods-2025-09-03T17-22-10.
Automatic Updates
Uses a systemd timer to stop the server, run the script, then restart the server. Initially, I had written a complicated system to stop and start the server when running the script, but I never quite got it to work and decided it would be simpler and more robust if I just used a timer to update the server automatically.
Basic Systemd Integration
Checks to see if the minecraft systemd unit is active. If it is, the script does not run unless the option restartServer is true, in which case the script stops the server and restarts it when done.
Error Handling and Logging
Contains important functions in try-catch blocks for error handling. Writes steps and results to console.
File Integrity
Every mod download is checked against its SHA-1 hash to prevent corrupted or tampered files.
Robust Configuration
The script takes the following variables and defaults:
serverPath: './server' // The path in which the server.jar file sits
modsPath: './server/mods' // The path of the mods folder
minecraftVersion: '1.20.4' // The version of Minecraft to update to
fabricVersion: null // The version of Fabric to use, null for latest compatible
timeout: 30000 // The timeout on scraping the Fabric website
backupMods: true // Whether to back up the mods folder before overwriting
restartServer: false // Whether or not to stop & start the server if it is active
Update Summary
As the script runs, it tells the user which mods are grabbed, which ones are skipped, and which ones failed.
Example output with a skipped mod:
📦 Processing noisium...
⏭️ Noisium v2.7.0+mc1.21.6 already exists
📊 Update Summary:
✅ Successful updates: 0
❌ Failed updates: 0
⏭️ Skipped: 1
Key DevOps Takeaways
-
Start simple, iterate based on real needs. Don’t over engineer the first version (guilty).
-
Plan for failure. Web scraping will break. APIs will have outages. Build resilient systems.
-
Consider the maintenance burden. Follow the KISS principle. Complex systems can look cool, but simple is often more manageable. My systemd integration that I ultimately ended up scrapping was a key example.
-
Don’t be afraid to use more than one approach to solve a problem. I had to use both web scraping and a REST API in this project, because Fabric didn’t have an API. Initially, I had the idea to just use scraping for both, but it ballooned out of control.
-
Managing infrastructure through code isn’t just about the initial automation—it’s about building systems that remain reliable and maintainable over time. Sometimes the best architecture is the one that does exactly what it needs to do, nothing more — the UNIX principle.