Runs the 2011 Battlefield 3 Alpha Trial and Open Beta on Linux, in rootless Podman containers, with my from-scratch reimplementation of EA's Blaze backend so the games have something to connect to.
Built to use Wine and DXVK. You need the AIO repack archive; this repository is the tooling that runs it.
Operation Métro on the Open Beta. The terminal is the dedicated server's container log; the game is a second container, and both are talking to the backend in a third.
Rootless podman and a compose provider: either pip install podman-compose
or the docker-compose binary on your PATH. Check with
podman compose version.
Battlefield 3 Alpha-Beta AIO Repack v3.7z, in your Downloads folder.
About 11 GB of disk for the games, plus the archive.
AMD and Intel graphics work through /dev/dri and Mesa. NVIDIA needs the
NVIDIA container toolkit and a CDI device added to compose.yaml.
git clone https://github.com/patrick-brigel/bf3-alpha-beta-linux.git
cd bf3-alpha-beta-linux/deploy
./generate-env.sh
podman compose --profile install up
podman compose --profile beta upgenerate-env.sh writes deploy/.env: what it can detect about this machine,
plus the settings most people change, at a working default. That file is where
this stack is configured. Open it if you want a different map, a faster career
or a name of your own; anything else from Settings can be added to
it by hand.
--profile install unpacks the repack into three volumes, which takes a few
minutes. --profile beta then starts the backend, a dedicated server and your
client, in that order, and drops you into a game.
Swap beta for alpha to play the Alpha Trial instead.
Expect the very first beta run to take a couple of minutes longer than later
ones: it builds a wine prefix and downloads d3dcompiler_43.
podman compose --profile beta upEverything runs on this machine. Quitting the game takes the dedicated server down with it, since they share a container; the backend keeps running until you stop the compose run with Ctrl+C.
podman compose --profile blaze up -d # backend
podman compose --profile beta-server up # dedicated server
podman compose --profile beta-client up # a playeralpha-server and alpha-client do the same for the Alpha Trial. Use these
split profiles rather than the all-in-one alpha and beta ones for anything
you leave running.
Stop anything started with -d the same way you started it, profile included:
podman compose --profile blaze downEveryone has to be on one network with the backend, and two kinds of network work.
Your LAN. Put the backend machine's LAN address in BLAZE_IP, in
deploy/.env, on every machine:
BLAZE_IP=192.168.1.50The host then runs the whole stack, and everyone else runs a client:
podman compose --profile beta up # on 192.168.1.50
podman compose --profile beta-client up # everyone elseEach machine needs the repack installed the same way. If the host runs a firewall, let the others in on:
| Port | Protocol | What |
|---|---|---|
| 42127 | TCP | the redirector |
| 10041 | TCP | clients |
| 10071 | TCP | dedicated servers |
| 25100 / 25200 | TCP and UDP | the game itself: alpha / beta, plus the next port up for queries |
A 192.168.0.x subnet is known to cause join failures; 192.168.1.x works.
An overlay VPN, for anyone who is not in the building. WireGuard,
Tailscale, ZeroTier and Netbird all hand every machine a stable private
address, and that address goes in BLAZE_IP exactly like a LAN one. Nothing
else changes, and no port on anybody's router is opened.
Do not put any of this on the internet directly. There are no passwords anywhere in the backend — the name you log in with is the identity — so a reachable port lets a stranger play as you and overwrite your career.
The backend enforces that rather than trusting you to. By default it listens on
BLAZE_IP and nothing else — BLAZE_BIND is the deliberate way to widen that —
it hands the dedicated-server role only to peers arriving from a private or
overlay address range, and it says so at every startup if BLAZE_IP is a public
one.
Give each player their own name and their own compose project:
LOGIN_TOKEN=patrick podman compose -p bf3-patrick --profile beta-client up
LOGIN_TOKEN=sven podman compose -p bf3-sven --profile beta-client upWithout -p, both players are the same container, and the second up stops
and removes the first player's game before starting its own. Stop a client with
the same -p you started it with. The separate projects still share one
install, one bf3-wine-data volume and one set of careers — but each player
gets his own wine prefix inside it, because two clients in one prefix share a
wineserver and the game's single-instance lock, and the second would exit at
once.
The name the game shows is LOGIN_TOKEN@LOGIN_HOST, so these two are
patrick@thinkpad and sven@thinkpad. The same username from two machines,
patrick@thinkpad and patrick@workstation, is two players with two careers
rather than one career two people overwrite in turn. generate-env.sh puts this
machine's name into .env as LOGIN_HOST the first time it runs, and keeps
whatever is there afterwards — so if the machine gets renamed, change that line
yourself.
SERVER_ID picks which server you are starting. Each gets its own wine prefix,
instance directory and ports.
podman compose --profile beta-server up
SERVER_ID=2 SERVER_NAME="Second Server" podman compose -p bf3-server2 --profile beta-server upThe -p matters for the same reason it does for a second player: without it
both invocations name the same container, and the second stops the first.
Ports go up in twos: 25200, 25202, and so on. Each server also uses the next port up for queries.
The backend is a small Python image with no wine, no game files and no GPU in it, so a spare box, a NAS or a VPS can run it on its own, and nobody has to install the repack there:
git clone https://github.com/patrick-brigel/bf3-alpha-beta-linux.git
cd bf3-alpha-beta-linux/deploy
./generate-env.sh # then put this machine's address in BLAZE_IP
podman compose --profile blaze up -dBLAZE_IP is the address the players reach it at, and the only one it listens
on. On a VPS that means the machine's overlay address, not its public one: join
it to the same WireGuard or Tailscale network as everybody else and it behaves
exactly like a box in the room.
generate-env.sh warns that the repack is missing and that it found no
display, Wayland or X. Both are fine here: the backend uses neither.
Whoever runs a dedicated server has to be on that same network. That role — creating games, and submitting the round reports that write careers — is granted by where a peer connects from. There is nothing else to grant it with: the 2011 dedicated server offers no credential of any kind. On an overlay every machine qualifies, which is the point of using one.
Everyone's career lives on this machine, in the bf3-blaze-data volume. Read
it with the commands under Your career.
Both builds' stat lists, rank tables and unlock ladders are built into the backend image. They were read out of the game data itself, not out of the stat list the repack ships — that one is retail's, and fits neither build. So a backend that has never seen the archive records exactly what the games define. See Stat definitions in blaze/README.md.
To update the backend later:
git pull
podman compose --profile blaze build
podman compose --profile blaze up -dGAME_ID=1 means matchmaking: you land on any running server of that build,
picked at random. This is the default and usually what you want.
To join one specific server, give its game id:
podman compose --profile blaze up -d
podman compose --profile blaze run --rm blaze --show gamesgame title started players server / map / mode
----------------------------------------------------------------------------------------
3 beta 2026-08-29T14:12:04+00:00 2 Second Server (MP_007 / ConquestLarge) [running]
2 beta 2026-08-29T14:11:38+00:00 0 BF3 (patrick) (MP_007 / ConquestLarge) [running]
GAME_ID=3 podman compose --profile beta-client upIds count up and are never reused. Naming one that is not running is refused rather than silently swapped for another server.
The beta ships two maps:
BF3_LEVEL=Levels/MP_007/MP_007 BF3_GAMEMODE=ConquestLarge0 podman compose --profile beta up
BF3_LEVEL=Levels/MP_Subway/MP_Subway BF3_GAMEMODE=RushLarge0 podman compose --profile beta upMP_007 is Caspian Border, MP_Subway is Operation Métro. Modes are
ConquestLarge0, RushLarge0, TeamDeathmatch0 and SquadRush0.
Map and mode are per machine for the beta, not per server: both beta servers on one host read the same config file. The Alpha Trial server takes them on its command line instead, so there they follow whatever started it.
The Alpha Trial has Operation Métro only.
Stats, unlocks, ranks and awards are recorded from the round reports the dedicated server submits. Alpha and Beta are separate careers, and so are two machines. The commands below take the bare username and find every career it stands for:
podman compose --profile blaze run --rm blaze --show summary # totals per game
podman compose --profile blaze run --rm blaze --show players # everyone
podman compose --profile blaze run --rm blaze --show patrick # one player
podman compose --profile blaze run --rm blaze --show patrick --title beta
podman compose --profile blaze run --rm blaze --show games # game historyWeapons and attachments unlock as you earn them. To skip that:
podman compose --profile blaze run --rm blaze --unlock patrick # write a maxed career
BF3_ALL_UNLOCKS=1 podman compose --profile beta up # ignore the careerThe first is permanent and survives into later sessions. The second only affects the session it starts.
deploy/.env is where these live, and ./generate-env.sh writes it with the
ones worth changing already in it. Putting a variable in front of a command
still works, for a one-off or for the two cases that are per-invocation by
nature — a second player, and a second server.
| Variable | Default | Meaning |
|---|---|---|
BLAZE_IP |
127.0.0.1 |
Address the backend is reached at, and the only one it listens on |
LOGIN_TOKEN |
your username | Your half of the player name |
LOGIN_HOST |
this machine's name | The other half: the player name is LOGIN_TOKEN@LOGIN_HOST |
GAME_ID |
1 |
1 matchmakes; a real id joins that game |
SERVER_ID |
1 |
Which dedicated server this is |
SERVER_NAME |
BF3 (your username) |
Shown in the server list |
ADMIN_PASSWORD |
unset | RCON password; unset configures no remote admin |
BF3_LEVEL |
build default; alpha falls back to Levels/MP_Subway/MP_Subway |
Map |
BF3_GAMEMODE |
build default; alpha falls back to RushLarge0 |
Game mode |
BF3_ALL_UNLOCKS |
0 |
1 treats everything as owned |
BF3_CLIENT_EXTRA |
unset | Extra settings for the Beta client, appended to its command line as written |
BF3_SCORE_FACTOR |
25 |
Beta only: multiplies every score awarded (the build ships at 2) |
BF3_GAME_REPORTS |
1 |
Beta only: 0 stops stats being recorded |
BF3_SERVER_WINDOW |
0 |
1 shows the dedicated server's own window |
BF3_GRAPHICS_DRIVER |
auto |
How the client draws: auto, wayland, or x11 to go through XWayland and get your window manager's titlebar |
SERVER_SETTLE |
15 |
Seconds to wait before the client joins |
GAME_PORT |
25100 / 25200 |
Game port |
BF3_ARCHIVE_DIR |
~/Downloads |
Where the repack archive is |
BF3_ARCHIVE_NAME |
the repack's own name | Use a differently named archive in that directory |
BF3_CPU_LIMIT |
8 |
Hardware threads the game is shown. The one setting .env is not read for: put it in front of the script, BF3_CPU_LIMIT=4 ./generate-env.sh |
BF3_REPACK_URL |
unset | Download the archive from here |
BF3_REPACK_SHA256 |
unset | Checked against the downloaded archive |
BF3_REPACK_PASSWORD |
the repack's own | Archive password |
BF3_SKIP_CHECKSUM |
0 |
1 unpacks without checking the archive's size and hash |
BF3_FORCE_INSTALL |
0 |
1 runs the installer past its "already populated" exit |
WINEFSYNC / WINEESYNC |
1 |
wine-staging's fast synchronisation; turning both off serialises the game |
WINEDEBUG |
-all |
wine tracing |
BLAZE_LOG |
INFO |
Backend log level |
BLAZE_BIND |
BLAZE_IP |
Listen on something other than the announced address |
The installer skips any volume that already carries its .installed marker.
BF3_FORCE_INSTALL=1 only gets it past the "every volume is already populated"
exit, so it fills the volumes that are missing, not the ones that are there:
BF3_FORCE_INSTALL=1 podman compose --profile install upTo redo a volume that is intact, take that volume away first:
podman volume rm bf3-beta # nothing is holding it
podman compose --profile install upTo take it all off the machine — the containers, the games, the wine prefixes and the images built to run them:
cd deploy
./uninstall.sh # careers are kept
./uninstall.sh --dry-run # print what would go, remove nothing
./uninstall.sh --yes # do not ask first; needed where there is no terminalIt names every volume and image it removes and asks before it starts. Careers are the one thing here that cannot be reinstalled, so they survive unless you say otherwise:
./uninstall.sh --careers # also erase everyone's rank, unlocks and stats
./uninstall.sh --keep-images # leave the built images in placeReinstall afterwards with podman compose --profile install up.
Every build also tags a new image and leaves the previous one untagged, so an afternoon of changing the backend leaves dozens behind:
cd deploy
./clean-up.sh # untagged leftovers
./clean-up.sh --dry-run # print what would go, remove nothing
./clean-up.sh --containers # also this stack's exited containers
./clean-up.sh --images # also its own tagged images, forcing a rebuildNever run podman volume prune. It takes every volume no container is
holding, which is usually all of them: the games, the wine prefixes and every
career. Neither script here does that — clean-up.sh never touches a volume,
and uninstall.sh removes only the ones it names: four, or five with
--careers.
The dedicated server's own log goes to the container log:
podman compose --profile beta-server logs beta-server[UTC ...] Engine/Game/Server/Core/Server.cpp(238): "GameLoop": Info: Creating server...
[UTC ...] BlazeSDK(0): "GameLoop": Info: <- resp: ID[13], UtilComponent::ping
Under the all-in-one alpha and beta profiles the same lines arrive with a
[server] prefix, so you can tell them from the client's.
The Alpha Trial's server writes no log at all. BF3_SERVER_WINDOW=1 puts its
console window back, which works with the alpha and beta profiles and not
with alpha-server or beta-server:
BF3_SERVER_WINDOW=1 podman compose --profile alpha upThe backend log carries games registering, players joining and leaving, and round reports landing:
podman compose --profile blaze logs blazeWayland wherever there is one. The client uses wine's own Wayland driver and talks to your compositor directly, so no X server is in the picture. Nothing to configure: an X11 desktop has no Wayland socket to find, and the X session gets used instead, exactly as before.
The X socket and cookie are mounted into the client container either way, because compose cannot skip a bind mount on a condition. The Wayland path means the client no longer needs them, not that they are out of reach. Anything holding them can read every keystroke you type in any window, screenshot the session and synthesise input, so treat that container as trusted with your desktop.
What the Wayland path costs is the window frame. winewayland.drv asks for no
server-side decoration, so wine draws the titlebar itself, in the Windows 2000
look, and no desktop setting reaches it — the compositor never decorates that
window. BF3_GRAPHICS_DRIVER=x11 draws through XWayland instead, and then the
window is the window manager's to decorate like any other, so KDE puts Breeze
on it:
BF3_GRAPHICS_DRIVER=x11 podman compose --profile beta upPut it in .env to keep it; ./generate-env.sh carries it across a re-run.
The trade is the paragraph above: on that path the client is using your X
socket and cookie again. It matters least in fullscreen, where there is no
frame to look at either way.
The dedicated server is the exception, and the only thing here still rendering
through X. It creates an 800x600 window it never shows and blocks until it has
one, so it gets a throwaway Xvfb. Under the -server profiles that Xvfb
lives in a container connected to nothing of yours: no X socket, no cookie, no
/dev/dri, no pulse socket, and not your IPC namespace. That is why those
profiles need no display from you at all. Under the all-in-one alpha and
beta profiles the server shares the client's container, so it draws on the
same private Xvfb but sits beside everything the client was given.
BF3_SERVER_WINDOW=1 asks for the window on screen instead, and then it draws
the way the client does.
X cannot leave the image altogether — wine-staging hard-depends on
libx11-6, libxext6, libxkbcommon0 and libxkbregistry0 — but nothing
you run renders through it except that Xvfb.
No window appears, but you can hear the game. Run ./generate-env.sh
again — it finds your session, and both the Wayland socket and the X cookie
move between logins.
The window has a grey Windows 2000 titlebar instead of your desktop's.
That is wine drawing its own frame, which is what its Wayland driver does.
Set BF3_GRAPHICS_DRIVER=x11 in .env to draw through XWayland and let your
window manager decorate it — see "How the game reaches your screen" for what
that path costs.
The game exits about twenty seconds in. Check deploy/cpu-online contains
something like 0-7. ./generate-env.sh writes that file.
BadValue ... X_ShmPutImage and the game dies at startup. ipc: host must
be present on the client services in compose.yaml — alpha, beta,
alpha-client, beta-client. The -server services never draw through your
X server, so they do not have it and do not need it. It is intermittent, so a run
that worked proves nothing.
Starting a second client stopped the first, with not all containers could be removed from pod on the way out. Give each player their own project, as
Two people at one machine shows. The pod errors
themselves are noisy but harmless.
The client starts, then quits without joining. Look at the backend:
podman compose --profile blaze logs blazeasked for beta game 7, which is not running means the GAME_ID you gave is
stale. Use 1 to matchmake.
missing services [blaze], and the service is plainly in compose.yaml.
Every service here sits behind a profile, and podman-compose up to 1.6.0 does
not enable a service's own profile when you name it. Docker Compose does, and
so does podman-compose after 1.6.0, which is why the same command works
elsewhere. Put the profile before the subcommand:
podman compose --profile blaze run --rm blaze --show gamesIt is needed even while --profile beta up is running: the flag selects what
this invocation can see, not what is started. To use the container that is
already up instead, name the command in full, because exec does not apply
the image's entrypoint:
podman compose --profile beta exec blaze python -m blaze --show gamesThe installer refuses the archive. It checks the size and SHA256 of a
complete copy before unpacking. Finish the download, or set
BF3_SKIP_CHECKSUM=1 if you know your archive is fine.
A shell in the game container. The image has a role for it, which lands in a wine prefix of its own rather than a game's:
podman compose --profile beta-client run --rm beta-client shellCheck the GPU is visible:
podman compose --profile beta-client run --rm beta-client vulkan-check| Path | What it does |
|---|---|
deploy/compose.yaml |
Every service and profile |
deploy/generate-env.sh |
Writes .env: this machine, and the settings most people change |
deploy/uninstall.sh |
Removes the containers, volumes and images; keeps careers |
deploy/clean-up.sh |
Reclaims what rebuilding leaves behind; never touches volumes |
wine/ |
The wine image and its launcher |
blaze/ |
The Blaze backend, see blaze/README.md |
installer/ |
Unpacks the repack into the game volumes |
Volumes: bf3-alpha, bf3-beta and bf3-beta-server hold the games,
bf3-wine-data the wine prefixes, and bf3-blaze-data the careers.
MIT — see LICENSE. That covers the code in this repository and nothing else: Battlefield 3 and everything in the repack belong to EA and DICE, and none of it is distributed here.
