Lua as a Configuration Language
Overview
Lua tables are natural data structures for configuration. A Lua config file is just a .lua file that returns a table. You load it with loadfile or dofile, and you have a structured configuration object with all the expressiveness of Lua behind it.
The advantage over JSON or YAML is that Lua configs can contain computed values, environment checks, conditional logic, and functions. You can write host = os.getenv("DB_HOST") or "localhost" — something impossible in a static config format. This makes Lua a favourite for tools like OpenResty, Neovim, and custom applications that need flexible configuration without a separate config language parser.
Basic Config Files
Returning a table
A config file is a Lua file that ends with a return statement:
-- config.lua
return {
host = "localhost",
port = 5432,
debug = false,
max_connections = 100,
}
Loading the config
local load_config = loadfile("config.lua")
local config = load_config()
print(config.host) -- "localhost"
print(config.port) -- 5432
loadfile returns a function if the file compiles successfully, or nil plus an error message. dofile is simpler but throws an error directly if the file has a syntax problem:
-- Using dofile (throws on syntax error)
local config = dofile("config.lua")
-- Using loadfile (gives you control over errors)
local fn, err = loadfile("config.lua")
if not fn then
error("Config failed to load: " .. err)
end
local config = fn()
Environment-Based Configuration
One of the most useful patterns is varying config by environment without duplicating the whole file:
-- config.lua
local env = os.getenv("APP_ENV") or "development"
local defaults = {
host = "localhost",
port = 5432,
debug = true,
pool_size = 10,
}
if env == "production" then
defaults.host = "db.example.com"
defaults.port = 5433
defaults.debug = false
defaults.pool_size = 50
end
return defaults
The calling code never changes — it just loads config.lua and reads values. The config file handles the environment branching internally.
Sharing Values Across Config Sections
Config files can also define helpers that the application uses:
-- config.lua
local function read_env(key, default)
local val = os.getenv(key)
return val ~= "" and val or default
end
return {
database = {
host = read_env("DB_HOST", "localhost"),
port = tonumber(read_env("DB_PORT", "5432")),
},
cache = {
enabled = os.getenv("CACHE_DISABLED") ~= "true",
ttl = 300,
},
}
Since the config file is just Lua, you can define local functions and use them to build up the returned table. The application receives a flat table with no dependency on the helper functions.
Validating Config With Metatables
One risk of Lua config files is that typos in keys produce silent failures. You can guard against this with a metatable that warns when an unexpected key is accessed:
-- config_with_validation.lua
local known_keys = {
host = true, port = true, debug = true,
max_connections = true, pool_size = true,
}
local config_mt = {
__index = function(self, key)
if not known_keys[key] then
io.stderr:write("Warning: unknown config key '" .. tostring(key) .. "'\n")
end
return rawget(self, key)
end,
}
local config = {
host = "localhost",
port = 5432,
debug = false,
max_connections = 100,
}
setmetatable(config, config_mt)
return config
Now accessing config.max_connections (correct) works normally, but config.max_connection (typo) triggers a warning. This is especially useful in larger config files where it is easy to mistype a key.
Table-Driven Config for Multiple Environments
For projects with many environment-specific values, a single config file with a lookup table keeps things organised:
-- config.lua
local env = os.getenv("APP_ENV") or "development"
local configs = {
development = {
host = "localhost",
port = 5432,
log_level = "debug",
},
staging = {
host = "staging-db.example.com",
port = 5432,
log_level = "info",
},
production = {
host = "prod-db.example.com",
port = 5433,
log_level = "warn",
},
}
local env_config = configs[env]
if not env_config then
error("Unknown environment: " .. env)
end
return env_config
The calling code stays the same regardless of which environment is active — just set APP_ENV before loading.
Loading Config From Non-Standard Locations
loadfile accepts an absolute path, so you can load config from arbitrary locations:
local function load_config(path)
local fn, err = loadfile(path)
if not fn then
error("Cannot load config from " .. path .. ": " .. err)
end
return fn()
end
-- Allow override via environment variable
local config_path = os.getenv("CONFIG_PATH") or "./config.lua"
local config = load_config(config_path)
This pattern is common in CLI tools where the user specifies --config /path/to/config.lua.
Security: Sandboxed Config Loading
By default, loadfile runs in the global environment. If your config files come from an untrusted source, use a sandbox to limit what they can do:
local function safe_load_config(path)
local fn, err = loadfile(path)
if not fn then
error("Cannot load config: " .. err)
end
-- Restrict the environment: only allow safe globals
local safe_env = {
math = math,
string = string,
table = table,
ipairs = ipairs,
pairs = pairs,
tonumber = tonumber,
tostring = tostring,
os = {
.getenv = os.getenv,
},
rawget = rawget,
rawset = rawset,
setmetatable = setmetatable,
getmetatable = getmetatable,
}
setfenv(fn, safe_env)
return fn()
end
The config file can still compute values and use environment variables, but it cannot access the filesystem, network, or other sensitive parts of the host.
Common Mistakes
Using dofile in production without error handling. dofile throws directly on syntax errors. Wrap with loadfile and check the returned function before calling it.
Returning nil accidentally. If the last line of the config file does not have a return statement, loadfile returns a function that produces nil. Always end the config file with return { ... }.
Mutating the returned table. If multiple parts of your code modify config directly, changes in one place affect all others. Treat the config as read-only, or copy it with table.move or a shallow copy if you need to modify it per-request.
Typos in key names. Without validation, config.max_connectionS silently returns nil. Add the metatable validation pattern above if your config files are large.
See Also
- /guides/lua-serialization/ — serialising and deserialising Lua data structures
- /guides/lua-environments/ — Lua environments and how
setfenvworks (used in sandboxing) - /guides/lua-metatables/ — metatables for validation and custom access patterns