- Shell 89.9%
- Dockerfile 7.1%
- Makefile 3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| lib | ||
| test | ||
| .commitlintrc.yml | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| database.docker.cfg.example | ||
| docker-compose.dev.yml | ||
| docker-compose.prod.yml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| healthcheck.sh | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
fivem-docker
A generic, reusable Docker image for running a FiveM (cfx) server. The image ships runtime only — the fxserver artifact version, the resources directory, and the config directory are all chosen at container run time. Content is supplied by host bind-mount or pulled from git on boot.
Launches via txAdmin. Alpine base, runs the server as a non-root user.
Layout
Everything persistent lives under the /data volume. txAdmin runs in host-config
mode, which requires the server content to sit under the txData path — so the
server base defaults to /data/txData/server:
/data/ # the volume
artifacts/<id>/ # downloaded fxserver builds (cache)
txData/ # txAdmin state
default/ # txAdmin profile (created on first run)
server/ # SERVER_ROOT — your server base folder
resources/ # RESOURCES_DIR
cfg/ # CFG_DIR
server.cfg # SERVER_CFG
cache/ # fxserver cache (persists across restarts)
Quick start
Bind-mount mode (host owns content)
docker build -t fivem-docker .
docker run -d --name fivem \
-p 30120:30120/tcp -p 30120:30120/udp -p 47291:47291/tcp \
-e FXSERVER_VERSION=recommended \
-e LICENSE_KEY=cfxk_xxx \
-v /path/to/resources:/data/txData/server/resources \
-v /path/to/cfg:/data/txData/server/cfg \
-v /path/to/server.cfg:/data/txData/server/server.cfg:ro \
-v fivem-data:/data \
fivem-docker
If your server.cfg execs configs by a relative path (e.g. exec .cfg/x.cfg),
mount that dir to match the relative name and set CFG_DIR to it, e.g.
-v /path/to/.cfg:/data/txData/server/.cfg -e CFG_DIR=/data/txData/server/.cfg.
Git monorepo mode (container pulls on boot)
docker run -d --name fivem \
-p 30120:30120/tcp -p 30120:30120/udp -p 47291:47291/tcp \
-e FXSERVER_VERSION=25770 \
-e GIT_REPO=https://github.com/you/your-server.git \
-e GIT_REF=main \
-e GIT_TOKEN=ghp_xxx \
-e LICENSE_KEY=cfxk_xxx \
-v fivem-data:/data \
fivem-docker
Compose (recommended)
Copy .env.example to .env and fill it in, then:
docker compose -f docker-compose.dev.yml up -d # dev (bind mounts)
docker compose -f docker-compose.prod.yml up -d # prod (git monorepo)
Dev binds the txAdmin panel to 127.0.0.1 only; prod publishes it (firewall it).
Configuration (.env)
Compose reads .env (gitignored) for both container env and ${VAR}
substitution. See .env.example for every setting — including secrets
(LICENSE_KEY, GIT_TOKEN, DB creds). .env stays out of git; on a private
host that's the simple, sufficient option.
Advanced: each sensitive var also accepts a _FILE variant
(LICENSE_KEY_FILE=/run/secrets/...) for docker/k8s secret mounts, if you'd
rather not keep values in .env.
First run
txAdmin starts on :47291. On first boot open the panel, register with the PIN
printed in the logs, then choose Existing Server Data and point it at the
server base:
- Server Data / Base Folder:
/data/txData/server - CFG File:
/data/txData/server/server.cfg
Setup is persisted in the /data volume (txData/default), so restarts skip it.
Launch modes
- txAdmin (default,
TXADMIN=1) —run.shboots the txAdmin panel on:47291. First run: point it at the cfg (see First run). Gives a web console, scheduled restarts, admin management. The cfg path is stored in txData. - Direct (
TXADMIN=0) — no panel. fxserver execsSERVER_CFGdirectly fromSERVER_ROOT, fully env-driven, zero-click boot. Healthcheck switches to the game port. Use when you don't want txAdmin and manage via RCON/server console. Needs a tty for fxserver's console: the compose files settty: true+stdin_open: true; with rawdocker runuse-ditinstead of-d.
Environment
| Var | Default | Purpose |
|---|---|---|
FXSERVER_VERSION |
recommended |
build number, NUM-HASH id, or channel: latest / recommended / critical / optional |
SERVER_ROOT |
/data/txData/server |
server base folder (under txData per txAdmin host mode) |
RESOURCES_DIR |
$SERVER_ROOT/resources |
in-container resources path |
CFG_DIR |
$SERVER_ROOT/cfg |
in-container cfg path |
SERVER_CFG |
$SERVER_ROOT/server.cfg |
entry cfg pointed to in txAdmin |
GIT_REPO |
— | monorepo; if set, clones/pulls all of SERVER_ROOT on boot |
GIT_RESOURCES_REPO |
— | split mode: resources source repo |
GIT_CFG_REPO |
— | split mode: cfg source repo |
GIT_REF |
main |
branch / tag / sha checked out |
GIT_AUTOPULL |
1 |
0/false/no/off → never fetch/reset an existing checkout (clone-if-missing only); protects a dev's local branch |
GIT_TOKEN / GIT_TOKEN_FILE |
— | https auth token |
GIT_USERNAME |
x-access-token |
https auth username |
LICENSE_KEY / LICENSE_KEY_FILE |
— (required) | cfx license key |
DB_PASSWORD / DB_PASSWORD_FILE |
— | resolved into env for your cfg to read |
TXADMIN |
1 |
1 = txAdmin panel; 0/false/no/off = direct mode (fxserver execs SERVER_CFG, no panel) |
TXADMIN_PORT |
47291 |
txAdmin panel port (TXHOST_TXA_PORT) |
TXADMIN_INTERFACE |
0.0.0.0 |
bind interface (TXHOST_INTERFACE) |
ARTIFACT_KEEP |
— | keep only N newest cached artifacts |
SECRETS_STRICT_PERMS |
— | if set, reject group/world-readable *_FILE secrets |
LOG_LEVEL |
info |
debug / info / warn / error |
Any VAR with a sensitive value supports a VAR_FILE variant pointing at a
file (docker/k8s secret). The file wins if both are set.
Mount matrix
A managed dir is bind-mounted (host owns) XOR git-managed (container clones)
— never both. The mode is chosen per dir by whether a GIT_* repo env is set.
| Dir | Bind mode | Git mode |
|---|---|---|
SERVER_ROOT (whole) |
bind subdirs individually | set GIT_REPO |
RESOURCES_DIR |
-v host:/data/txData/server/resources |
set GIT_RESOURCES_REPO |
CFG_DIR |
-v host:/data/txData/server/cfg |
set GIT_CFG_REPO |
/data |
always a volume | — |
Setting GIT_REPO (monorepo) together with GIT_RESOURCES_REPO/GIT_CFG_REPO
(split) is rejected at boot. Pointing a git repo at a populated bind mount is
also rejected.
Artifact versions
- Channel keyword (
recommended,latest,critical,optional) → resolved off the cfx changelog at boot. - Bare build number (e.g.
25770) → hash resolved off the artifact listing. - Full
NUM-HASHid → used directly (reproducible pin).
Downloaded builds are cached under /data/artifacts/<id>; switching versions
keeps old builds for fast rollback (cap with ARTIFACT_KEEP).
Ports
| Port | Proto | Use |
|---|---|---|
| 30120 | tcp + udp | game traffic |
| 47291 | tcp | txAdmin panel |
Updates
Git pull happens on boot only — update content with docker restart. This
keeps host edits and pulls from colliding mid-session.
For prod, leave GIT_AUTOPULL=1 (default): each boot resets SERVER_ROOT to
GIT_REF. For dev working on a personal branch, set GIT_AUTOPULL=0 so the
container clones once but never fetch/resets — your local branch + uncommitted
work survive restarts, and you sync from master yourself. (Pure bind-mount dev
sets no GIT_* at all and skips git entirely.)
External database (host MySQL)
The container reaches a database running on the host via host.docker.internal
(Docker Desktop resolves it; on a Linux engine add
--add-host=host.docker.internal:host-gateway). Two things are required:
- A user grant for the docker network. The connection arrives from the
docker net (e.g.
172.17.x), not localhost, soroot@localhostwon't match:
ScopeCREATE USER 'fivem'@'%' IDENTIFIED BY 'your-strong-pass'; GRANT ALL PRIVILEGES ON your_db.* TO 'fivem'@'%'; FLUSH PRIVILEGES;'%'or tighter ('172.17.%'). Use a dedicated user, notroot. - MySQL bound to all interfaces. Set
bind-address = 0.0.0.0in the server config and restart MySQL, otherwise it refuses non-local connections.
Point your server's DB config at host.docker.internal:3306 with that user. If
the same config file is shared with a bare-metal setup that uses 127.0.0.1,
keep both: bind a docker-specific config over the in-repo one (see the commented
override in docker-compose.dev.yml).
Security / ownership
The container starts as root only to chown the /data volume and the root-owned
bind-mount parent dirs, then drops to the unprivileged fivem user (uid 30120)
via su-exec for boot and the server process. The uid/gid are pinned so a
persisted volume keeps working across image rebuilds.
Tests
Shell unit tests use bats:
make lint # shellcheck all scripts
make test # bats test/
make check # both
make build # docker build
CI (.github/workflows/ci.yml) runs lint + test, then builds the image on
every push/PR.