Build a Factorio Mod: From Your First Prototype to Publishing
Factorio has one of the most approachable modding systems of any game. The data is plain Lua, the engine reloads your changes on restart, and the official mod portal does the distribution. This guide takes you from a two-file "hello world" tweak to a real item with custom behaviour, then to a published mod running on a dedicated server. It targets Factorio 2.0 (the Space Age engine); every code sample below is verified against the official API docs and the Wube modding tutorial.
You write mods in Lua, not the engine's language. Factorio's core is C++, but everything a mod touches - items, recipes, entities, and runtime logic - is defined in Lua scripts the engine loads at startup. You do not compile anything. You do not need a build tool. A text editor and the game are enough.
Just want a quick server tweak? If you only need to change stack sizes, recipe costs, crafting times, or unlock a recipe, the Factorio Server Tweak Mod Generator builds a ready-to-install mod for you in seconds - no coding. This guide is for when you want to understand the files and go further.
What you'll build
Work through the sections in order. Each one stands on its own, but together they walk the full ladder from trivial tweak to published, server-ready mod:
- Anatomy of a mod - the folder,
info.json, and the files Factorio recognises - Your first mod - a two-file tweak that doubles a stack size
- How Factorio loads your mod - the data lifecycle (this is the concept everything else rests on)
- Add a real item and recipe - the Fire Armor mod, with localization
- Make it do something - runtime behaviour in
control.lua - Add a config option - mod settings
- Package and publish - the mod portal
- Run it on a dedicated server - the hosting payoff
- Common pitfalls
1. Anatomy of a Factorio mod
A mod is a folder (or a zip of that folder) that lives in Factorio's mods/ directory. The only mandatory file is info.json. Everything else is optional and loaded by convention if present.
Factorio looks for the mods folder inside your user data directory:
| OS | mods folder |
|---|---|
| Windows | %APPDATA%\Factorio\mods\ |
| Linux | ~/.factorio/mods/ |
| macOS | ~/Library/Application Support/factorio/mods/ |
| Dedicated server | the mods/ folder next to the server's data directory (see section 8) |
info.json
This file identifies your mod and declares its compatibility. Required fields:
| Field | Format | Notes |
|---|---|---|
name | string | Internal id. Letters, digits, dashes, underscores; 3-100 chars. Must match the folder name. |
version | "number.number.number" | Each part 0-65535, e.g. "0.1.0" |
title | string | Display name in the mods list (max 100 chars) |
author | string | Your name or handle |
factorio_version | "number.number" | The base game version, e.g. "2.0" |
Optional fields: contact, homepage, description, and dependencies (an array of dependency strings). A minimal valid file:
{
"name": "fire-armor",
"version": "0.1.0",
"title": "Fire Armor",
"author": "You",
"factorio_version": "2.0",
"dependencies": ["base >= 2.0"],
"description": "Adds fire armor that leaves a trail of flame as you walk."
}
Dependency prefixes
Each dependency string is "<prefix> mod-name <operator> version". The prefix controls the relationship:
| Prefix | Meaning |
|---|---|
| (none) | Required dependency - must be present, and loads before your mod |
! | Incompatibility - the two mods cannot be enabled together |
? | Optional - loads before yours if present, but not required |
(?) | Hidden optional - same as ? but not shown to users |
~ | Does not affect load order (use when you depend on a mod but must not load after it) |
Files Factorio recognises
| File | Stage | Purpose |
|---|---|---|
info.json | - | Required. Identity and metadata |
settings.lua | settings | Define mod settings (config options) |
data.lua | data | Define prototypes (items, recipes, entities) |
data-updates.lua | data | Modify prototypes other mods defined |
data-final-fixes.lua | data | Last-chance prototype adjustments |
control.lua | control | Runtime scripting (gameplay logic) |
locale/ | - | Translated names and descriptions (.cfg files) |
graphics/, sound/ | - | Assets referenced by prototypes |
migrations/ | - | Handlers that fix up old saves when your mod updates |
changelog.txt | - | Version history (strict format, shown in-game) |
2. Your first mod
The smallest useful mod is two files. Make a folder named exactly bigger-stacks in your mods/ directory, and put this info.json in it:
{
"name": "bigger-stacks",
"version": "0.1.0",
"title": "Bigger Iron Stacks",
"author": "You",
"factorio_version": "2.0",
"dependencies": ["base >= 2.0"]
}
Next to it, create data.lua with a single line that reaches into the base game's prototypes and changes one:
data.raw["item"]["iron-plate"].stack_size = 200
data.raw is the table of every prototype loaded so far, indexed by [type][name]. Because the base mod is a dependency, its prototypes already exist when your data.lua runs, so you can edit them directly. Iron plates normally stack to 100; this doubles it.
Test it: launch Factorio, click Mods on the main menu, confirm "Bigger Iron Stacks" is in the list and enabled, then start or load a game. Iron plates now stack to 200. Any time you change a data-stage file, you must restart Factorio for it to take effect - the prototype stage only runs at startup.
3. How Factorio loads your mod (the data lifecycle)
This is the single most important concept in Factorio modding. The engine loads mods in three stages, in this order:
- Settings stage - runs
settings.luafor every mod. Defines the config options players see before the game starts. - Data (prototype) stage - defines everything that exists in the game: items, recipes, entities, technologies. Runs in three sub-rounds (below).
- Control stage - runtime.
control.luaruns while you actually play, reacting to events.
The data stage runs in three consecutive rounds, and each round completes for every mod before the next round begins:
| Round | File | Use it to |
|---|---|---|
| 1 | data.lua | Define your own new prototypes |
| 2 | data-updates.lua | Modify prototypes from other mods (they all exist by now) |
| 3 | data-final-fixes.lua | Final adjustments after every mod has had its say |
Why three rounds matter: if your mod changes another mod's item, doing it in data-updates.lua guarantees that item already exists. Doing the same edit in data.lua can fail with a nil error if that mod happens to load after yours. After the data stage's three rounds finish, the Lua state is discarded - locals and functions do not carry into the control stage. The two stages share nothing but the finished prototypes.
4. Add a real item and recipe (Fire Armor)
Now build something that does not exist in the base game. We will add a new armor by copying heavy armor, retinting it red, hardening its resistances, and giving it a recipe. Make a folder fire-armor with the info.json from section 1, then this data.lua:
-- data.lua
local fireArmor = table.deepcopy(data.raw["armor"]["heavy-armor"])
fireArmor.name = "fire-armor"
fireArmor.icons = {
{
icon = fireArmor.icon,
icon_size = fireArmor.icon_size,
tint = {r=1, g=0, b=0, a=0.3}
},
}
fireArmor.resistances = {
{type = "physical", decrease = 6, percent = 10},
{type = "explosion", decrease = 10, percent = 30},
{type = "acid", decrease = 5, percent = 30},
{type = "fire", decrease = 0, percent = 100}
}
local recipe = {
type = "recipe",
name = "fire-armor",
enabled = true,
energy_required = 8,
ingredients = {
{type = "item", name = "copper-plate", amount = 200},
{type = "item", name = "steel-plate", amount = 50}
},
results = {{type = "item", name = "fire-armor", amount = 1}}
}
data:extend{fireArmor, recipe}
Two techniques are doing the work here:
table.deepcopy(data.raw["armor"]["heavy-armor"])clones an existing prototype so you inherit all its fields and only override what you want. This is the most common way to make a new thing - start from something that already works.data:extend{...}registers your new prototypes with the engine. Pass it a list of prototype tables. The armor needs a newname(a prototype's name must be unique within its type), and the recipe ties ingredients to the result.
Localization
Right now the item shows up as fire-armor in tooltips. Names and descriptions live in locale files, not in the prototype. Create locale/en/fire-armor.cfg (the filename is free; the folder must be the locale code):
[item-name]
fire-armor=Fire armor
[item-description]
fire-armor=Armor that scorches the ground with every step. Warm to the touch.
The section headers ([item-name], [item-description]) are how Factorio maps a prototype name to its displayed text. Restart, craft the armor, and you have a real, named item in the game.
5. Make it do something at runtime (control.lua)
So far everything has been in the data stage - static definitions. To give the armor behaviour, you move to the control stage. control.lua runs during the game and reacts to events. The pattern is always the same: register a handler with script.on_event, keyed by an event from the defines.events table.
-- control.lua
script.on_event(defines.events.on_player_changed_position,
function(event)
local player = game.get_player(event.player_index)
if player.controller_type == defines.controllers.character then
local armor = player.get_inventory(defines.inventory.character_armor)
if armor.get_item_count("fire-armor") >= 1 then
player.surface.create_entity{
name = "fire-flame",
position = player.position,
force = "neutral"
}
end
end
end
)
Every time a player moves, this checks whether they are wearing fire armor and, if so, spawns a flame entity at their feet. A few rules of the control stage worth internalising:
gameis your handle on the live world:game.get_player(index),game.print(text),game.tick,game.players,game.surfaces.- Only one handler per event. Registering a second handler for the same event overwrites the first.
- Persistent state goes in the
storagetable. In Factorio 2.0 this table was renamed fromglobaltostorage- older tutorials still sayglobal, which is now wrong. Anything you want to survive a save/load must live instorage. - Use
script.on_initto initialise that state once, when the mod is first added to a save:
script.on_init(function()
storage.steps = 0
end)
6. Add a config option (mod settings)
Real mods expose settings. Settings are defined in their own stage, in settings.lua, before any prototype loads. A startup setting that lets players turn the fire trail off:
-- settings.lua
data:extend({
{
type = "bool-setting",
name = "fire-armor-leave-fire",
setting_type = "startup",
default_value = true
}
})
There are three setting scopes: startup (locked once the game starts, can affect prototypes), runtime-global (changeable mid-game, shared by all players), and runtime-per-user (per-player). Read a setting in control.lua by its scope:
if settings.startup["fire-armor-leave-fire"].value then
-- spawn the flame
end
7. Package and publish to the mod portal
When your mod works locally, distribution is a zip upload. Two rules matter:
- The zip must be named
{name}_{version}.zip- for our example,fire-armor_0.1.0.zip. The folder inside the zip can be named anything, but its contents must be your mod files (withinfo.jsonat that folder's root). - Add a
changelog.txtso players see what changed. The format is strict - the separator line is exactly 99 dashes, and entries are indented under a category:
---------------------------------------------------------------------------------------------------
Version: 0.1.0
Date: 2026-06-17
Features:
- Added fire armor that leaves a trail of flame.
Upload the zip at mods.factorio.com (sign in with your factorio.com account, then use the publish/upload form). The portal reads the zip's info.json for the title, version, and dependencies. Subsequent updates are just a new zip with a bumped version - the portal rejects a re-upload of a version that already exists, which is why the semantic version number matters.
8. Run your mod on a dedicated server
A mod that changes the game world is part of the save, so a modded server and every connecting client must run the same mod list - Factorio's matchmaker compares the lists at connect time and rejects mismatches. There are two ways to get your mod onto a server:
- Published mod: put your factorio.com
usernameandtokenin the server'sserver-settings.json, then enable the mod in the server'smods/mod-list.json. On first start the server downloads it from the portal. - Unpublished or local mod: drop the
fire-armor_0.1.0.zipstraight into the server'smods/folder and add an entry tomod-list.json:{ "mods": [ {"name": "base", "enabled": true}, {"name": "fire-armor", "enabled": true} ] }
On Linux servers the mod name and file references are case-sensitive, so fire-armor and Fire-Armor are different mods - a frequent cause of "mod not found" on a host that worked fine on a Windows desktop. For the full server setup, see Install Mods on a Factorio Server and How to Host a Factorio Headless Server.
9. Common pitfalls
| Symptom | Cause / fix |
|---|---|
| Mod does not appear in the list | Folder name does not match info.json's name, or info.json has a JSON syntax error |
attempt to index nil in data stage | You edited another mod's prototype in data.lua; move it to data-updates.lua so the target exists first |
| Changes do nothing | You edited a data-stage file but did not restart Factorio - the prototype stage only runs at startup |
storage is nil / state lost on reload | You used the old global table; in 2.0 it is storage, and it must be set in on_init |
| Second event handler "doesn't fire" | Only one handler per event - the later registration overwrote the earlier one |
| "mod not found" on a Linux server only | Case mismatch between the zip/folder name and mod-list.json |
| Players rejected at connect | Client mod list differs from the server's - they need the same mod and version |
Related reading
- Install Mods on a Factorio Server - getting existing portal mods onto your host
- How to Host a Factorio Headless Server - the dedicated server your mod will run on
- Official: Factorio mod structure reference
- Official: Factorio Lua API documentation
- Official: Wube's beginner modding tutorial
Host Your Modded Factorio Server with Supercraft
Supercraft runs Factorio dedicated servers with the headless build pre-installed, the mods/ folder and mod-list.json exposed through the panel, autosave tuned, and mod auto-install wired to your factorio.com credentials. Build your mod, drop the zip in, and your friends are playing it minutes later.