Skip to content

Add Docker support and CI workflow for Docker image publish#459

Open
enzofrnt wants to merge 31 commits intoBeamMP:minorfrom
enzofrnt:docker-support
Open

Add Docker support and CI workflow for Docker image publish#459
enzofrnt wants to merge 31 commits intoBeamMP:minorfrom
enzofrnt:docker-support

Conversation

@enzofrnt
Copy link

This pull request introduces comprehensive Docker support for the project, enabling streamlined containerized builds, testing, and deployment. It adds a multi-stage Dockerfile, a Docker Compose configuration, a GitHub Actions workflow for automated multi-architecture image builds, and improves .dockerignore handling. Additionally, it refines the Git submodule logic in CMake for more robust and context-aware submodule management.

Docker support and automation:

  • Added a multi-stage Dockerfile to build and package the server and its dependencies, including cache optimizations, debug symbol handling, and a non-root runtime user.
  • Introduced compose.yaml for local development and deployment, mapping configuration, resources, and data directories, and exposing necessary ports.
  • Added .github/workflows/docker.yml to automate Docker builds for both amd64 and arm64 architectures, supporting branch and tag triggers, and pushing images to GitHub Container Registry.
  • Created a .dockerignore file to exclude unnecessary files and directories from Docker build context, improving build performance and reducing image size.

Build system improvements:

  • Enhanced cmake/Git.cmake to update submodules only when in a real Git checkout, provide clearer status messages, and avoid errors when Git or .git is not present. (Which can append in Docker build)

By creating this pull request, I understand that code that is AI generated or otherwise automatically generated may be rejected without further discussion.
I declare that I fully understand all code I pushed into this PR, and wrote all this code myself and own the rights to this code.

Introduces Dockerfile, .dockerignore, and compose.yaml for containerization and deployment. Adds a GitHub Actions workflow for automated Docker builds and pushes. Updates .gitignore to exclude build artifacts.
The GitHub Actions workflow will now run on pushes to the docker-support branch, enabling CI/CD for Docker-related changes.
Improves Dockerfile with multi-stage builds, BuildKit caching, and more granular dependency installation. Updates GitHub Actions workflow to use registry-based build cache. Refines .dockerignore and compose.yaml for better Docker context and volume management. Enhances CMake Git submodule logic to avoid errors outside git checkouts.
Moves IMAGE_NAME definition from env to a workflow step, setting it to the lowercased repository name using bash. This improves flexibility and ensures consistent image naming.
Changed the Docker GitHub Actions workflow to set IMAGE_NAME using the @l parameter expansion and store it in GITHUB_ENV instead of GITHUB_OUTPUT. Also added a step to echo the IMAGE_NAME environment variable.
Changed the COPY source from /work/build-server/BeamMP-Server to /work/out/BeamMP-Server to reflect the new build artifact location. This ensures the final artifact is correctly copied during the Docker build process.
Refactored the GitHub Actions workflow to separate Docker image builds for amd64 and arm64 architectures into distinct jobs. This improves build clarity and allows for targeted platform builds on appropriate runners.
Consolidated separate amd64 and arm64 jobs into a single matrix-based 'build' job for multi-architecture Docker image builds. This simplifies the workflow, reduces duplication, and ensures both architectures are built consistently. Also added recursive submodule checkout and standardized registry/image references.
Bump the docker/build-push-action version from v5 to v6 in the GitHub Actions workflow to use the latest features and improvements.
Configured the GitHub Actions Docker workflow to use registry-based build cache with cache-from and cache-to options. This should improve build times by leveraging cached layers across builds.
Added 'provenance: false' and 'sbom: false' to the Docker build step in the GitHub Actions workflow to prevent generation of provenance and SBOM artifacts.
Removed outdated and redundant comments, including French notes, from the Dockerfile. Updated the artifact copy comment to clarify the use of BuildKit cache and artifact location.
Changed the Docker image reference from 'beammp-server:latest' to 'ghcr.io/BeamMP/beammp-server' to use the official image from GitHub Container Registry.
Cleaned up the Docker Compose file by removing unused and commented-out environment variables and resource limits for improved readability.
The GitHub Actions workflow will no longer run for pushes to the 'docker-support' branch. This streamlines workflow execution to only relevant branches and tags.
@enzofrnt
Copy link
Author

enzofrnt commented Jan 11, 2026

Here is how it's look when build :
https://github.com/enzofrnt/BeamMP-Server/pkgs/container/beammp-server
https://github.com/enzofrnt/BeamMP-Server/actions/runs/20898778316

To try it out :

Create compose.yaml file :

services:
  beammp-server:
    image: ghcr.io/enzofrnt/beammp-server:docker-support
    container_name: beammp-server
    restart: unless-stopped
    ports:
      - "30814:30814/tcp"
      - "30814:30814/udp"
    volumes:
      # Mount configuration directory
      - ./config:/config
      # Mount resources directory (mods, plugins, etc.)
      - ./resources:/resources:ro
      # Mount working directory for logs and data
      - ./data:/app/data
    environment:
      - TZ=UTC

Start the container (it will automaticly download the Docker image):
docker compose up -d

@OfficialLambdax
Copy link
Collaborator

Awesome PR! I would suggest one change and that is to install LuaRocks into the container as well. A handful of scripts require rock modules to run and many users have trouble setting it up which in return makes scripters trying to not require lua rocks. If a docker container had support for that at default then that would open up a whole new world

@OfficialLambdax OfficialLambdax added the good first issue Good for newcomers label Jan 11, 2026
@enzofrnt
Copy link
Author

Let's do it <3 But I will not be able to try it.

@enzofrnt
Copy link
Author

Can I get some Lua examples I can try, along with a brief explanation of how they work?

@OfficialLambdax
Copy link
Collaborator

Can I get some Lua examples I can try, along with a brief explanation of how they work?

Sure. Lets do an example with LuaSocket. You can install a rock like this luarocks install luasocket, just push it into a console.

Then create a folder inside the beammp servers Resources/Server folder. Name it anyway you want. In that new folder create a new file, name it main.lua. Inside it paste

local Socket = require("socket")

Reboot the server and done (:

if this throws an error the setup failed.

Updated the image name in compose.yaml from 'BeamMP' to 'beammp' to ensure consistency and avoid potential issues with case sensitivity in Docker image references.
@OfficialLambdax
Copy link
Collaborator

If you go into the container, install something, and then restart it, anything you just changed or installed will disappear unless it’s stored in a volume.

A simple container restart doesnt remove any data. Redeploying that container would do

docker compose down -- would remove!
docker compose stop -- just stops!

@enzofrnt
Copy link
Author

enzofrnt commented Jan 11, 2026

If you go into the container, install something, and then restart it, anything you just changed or installed will disappear unless it’s stored in a volume.

A simple container restart doesnt remove any data. Redeploying that container would do

docker compose down -- would remove!
docker compose stop -- just stops!

yes, my bad ^^ I havedelete my comment.

But it’s a bit tricky to install things manually like that. Some documentation should provide instructions on how to properly create your own image, so you can update it and bring containers up/down without too many issues.

@Starystars67
Copy link
Member

Furthering your discussion there of luarocks inclusion, What if as part of the entrypoint script it checks an ENV value for the modules to install via luarocks?

This way it could install them at start and (maybe?) skip them if already installed without requiring the user to have to enter the container which is great for those hosting via the likes of a game panel or hosting provider.

@toinopt
Copy link

toinopt commented Jan 11, 2026

Furthering your discussion there of luarocks inclusion, What if as part of the entrypoint script it checks an ENV value for the modules to install via luarocks?

This way it could install them at start and (maybe?) skip them if already installed without requiring the user to have to enter the container which is great for those hosting via the likes of a game panel or hosting provider.

Along with this having a ENV for the LUA version to install might be useful too.

@OfficialLambdax
Copy link
Collaborator

But it’s a bit tricky to install things manually like that. Some documentation should provide instructions on how to properly create your own image, so you can update it and bring containers up/down without too many issues.

I believe the proper way to have a rock installed is by the script that requires it. eg (havent tested)

local is_ok, Socket = pcall(require, "socket")
if not is_ok then -- if module is not present then install it
   os.execute("luarocks install luasocket") -- will block until done
   Socket = require("socket")
end

@Starystars67
Copy link
Member

But it’s a bit tricky to install things manually like that. Some documentation should provide instructions on how to properly create your own image, so you can update it and bring containers up/down without too many issues.

I believe the proper way to have a rock installed is by the script that requires it. eg (havent tested)

local is_ok, Socket = pcall(require, "socket")
if not is_ok then -- if module is not present then install it
   os.execute("luarocks install luasocket") -- will block until done
   Socket = require("socket")
end

With this, are you thinking that this would be done from a script intended to be running from the BeamMP server or as a separate install script that is run before the BeamMP server is started?

@OfficialLambdax
Copy link
Collaborator

OfficialLambdax commented Jan 11, 2026

With this, are you thinking that this would be done from a script intended to be running from the BeamMP server or as a separate install script that is run before the BeamMP server is started?

By a BeamMP server script. That way a user wanting to use a public server script can just drop it into their server and it will set itself up. Otherwise the user would have to edit the compose again. It takes a setup out of the "How to install" instructions, right? As in its more convenient

@enzofrnt
Copy link
Author

enzofrnt commented Jan 12, 2026

How do you build it ?

I took a copy of your branch and then simply docker compose up

Have you init submodules ?

git clone --recursive <...>

If you already cloned:

git submodule update --init --recursive

If you prefer, the image is available here:

docker pull ghcr.io/enzofrnt/beammp-server:pr-2

@OfficialLambdax
Copy link
Collaborator

If you prefer, the image is available here:

docker pull ghcr.io/enzofrnt/beammp-server:pr-2

Yes that helps. Im on a slow machine atm and just to get to the error took me a hour. This helps.
As expected the dirs the container creates are root owned and the server itself is even not able to write to them

beammp-server  | [12/01/26 11:45:15] [INFO] Custom config requested via commandline arguments: '/config/ServerConfig.toml'
beammp-server  | [12/01/26 11:45:15] [INFO] No config file found! Generating one...
beammp-server  | [12/01/26 11:45:15] [ERROR] Failed to create/write to config file: Permission denied
beammp-server  | [12/01/26 11:45:15] [ERROR] A fatal exception has occurred and the server is forcefully shutting down.
beammp-server  | [12/01/26 11:45:15] [ERROR] Failed to create/write to config file

A simple chown fixed it, but just for your info

Set default values for BeamMP environment variables in .env.example to provide clearer configuration guidance for new users.
Replaced hardcoded port values with the BEAMMP_PORT environment variable in the Docker Compose file to allow configurable port mapping for the beammp-server service.
@enzofrnt
Copy link
Author

enzofrnt commented Jan 12, 2026

If you prefer, the image is available here:

docker pull ghcr.io/enzofrnt/beammp-server:pr-2

Yes that helps. Im on a slow machine atm and just to get to the error took me a hour. This helps. As expected the dirs the container creates are root owned and the server itself is even not able to write to them

beammp-server  | [12/01/26 11:45:15] [INFO] Custom config requested via commandline arguments: '/config/ServerConfig.toml'
beammp-server  | [12/01/26 11:45:15] [INFO] No config file found! Generating one...
beammp-server  | [12/01/26 11:45:15] [ERROR] Failed to create/write to config file: Permission denied
beammp-server  | [12/01/26 11:45:15] [ERROR] A fatal exception has occurred and the server is forcefully shutting down.
beammp-server  | [12/01/26 11:45:15] [ERROR] Failed to create/write to config file

A simple chown fixed it, but just for your info

Hmm, I’m not able to reproduce it. I don’t really understand why you’re running into this issue, are you running the container as root user on your local device?

@OfficialLambdax
Copy link
Collaborator

Hmm, I’m not able to reproduce it. I don’t really understand why you’re running into this issue, are you running the container as root user on your local device?

Your right that was my bad. I ran it with sudo. In the meantime ive played around with luarocks in this container. Where as a rock isnt installed in the image but via any script that requires it at runtime. This here is a working solution ive came up with

local function addLocalLuaRocksToPackagPath()
    local username = io.popen("whoami"):read()
    local path = string.format('/home/%s/.luarocks/share/lua/5.3/?.lua', username)
    local cpath = string.format('/home/%s/.luarocks/lib/lua/5.3/?.so', username)

    if not package.path:find(username .. '/.luarocks') then
        package.path = package.path .. ';' .. path
    end
    if not package.cpath:find(username .. '/.luarocks') then
        package.cpath = package.cpath .. ';' .. cpath
    end
end

local function requireRock(name, package_name)
    local is_ok, rock = pcall(require, name)
    if not is_ok then
        os.execute('luarocks install --local ' .. package_name)
        rock = require(name)
    end
    return rock
end

addLocalLuaRocksToPackagPath() -- called on state init

local Socket = requireRock("socket", "luasocket") -- try require, if fail then install
print(Socket)

@enzofrnt
Copy link
Author

enzofrnt commented Jan 12, 2026

In the meantime ive played around with luarocks in this container. Where as a rock isnt installed in the image but via any script that requires it at runtime. This here is a working solution ive came up with

I'm not sure that's the best approach. I feel that installing packages on the fly isn't really a best practice; it seems much more robust to install them cleanly once during the build process.

That said, it's open for debate. We should see what the project maintainers think and which method they prefer to define as the 'standard' moving forward.

I do admit, however, that your method could effectively work within the current image. It might be a handy solution for 'Docker noobies' (beginners) who aren't comfortable building their own custom images.

By the way, do you understand how my current implementation works?

@OfficialLambdax
Copy link
Collaborator

OfficialLambdax commented Jan 12, 2026

I'm not sure that's the best approach. I feel that installing packages on the fly isn't really a best practice; it seems much more robust to install them cleanly once during the build process.

The idea is that a user of the image can just drag and drop a script they sourced from a third party into their server and it install the dependencies it needs itself. Without the creator having to explain that the user must now clone the repo, edit the dockerfile and build it themselfs. No it will just work out of the box. That makes it very convenient for beginners and advanced users alike.

If the image forced them to install rocks during the build process then anytime the user would want to use a new rock theyd have to update the entire image and recreate all containers using it. That be rather in the way then be productive.

By the way, do you understand how my current implementation works?

Could you specify what implementation you mean?

@enzofrnt
Copy link
Author

That is actually how Docker is supposed to work. It's the standard practice to ensure stability. Beginners can indeed use your proposition for ease of use, but this doesn't mean we shouldn't provide a solution to do it 'the right way' for others. By 'my implementation', I meant the Dockerfile structure I proposed earlier by adding dependecies in custom Dockerfile to make your own image.

@OfficialLambdax
Copy link
Collaborator

That is actually how Docker is supposed to work. It's the standard practice to ensure stability. Beginners can indeed use your proposition for ease of use, but this doesn't mean we shouldn't provide a solution to do it 'the right way' for others. By 'my implementation', I meant the Dockerfile structure I proposed earlier by adding dependecies in custom Dockerfile to make your own image.

Yes its the standard to install everything during the build process, i just want to respect that most users dont know how to set things up and make it as convenient as possible for users to install server side mods. And its just very convenient to make on the fly changes (drag, drop, restart, done - compared to fully setting everything up again after one change).

You ment this?

FROM ghcr.io/beammp/beammp-server:latest

USER root
RUN luarocks --lua-version=5.3 install luasocket
USER beammp

Yes thats totally fine!

@enzofrnt
Copy link
Author

Yes, we agree !

@toinopt
Copy link

toinopt commented Jan 12, 2026

That is actually how Docker is supposed to work. It's the standard practice to ensure stability. Beginners can indeed use your proposition for ease of use, but this doesn't mean we shouldn't provide a solution to do it 'the right way' for others. By 'my implementation', I meant the Dockerfile structure I proposed earlier by adding dependecies in custom Dockerfile to make your own image.

What about providing both options?
One is the gold image with just the basics without lua and another is the noobie version.

I say this a someone that that to learn how to build docker images by trial and error pretty much on my own, at one point I even gave up because I couldn't figure out all the LUA modules needed.

I understand how much easier it would be for a mod developer to just have a small instructions basically being, use this noobie image and add the files to the correct folder and it will install everything it needs on its own.

Building a new image is a non-trivial step that requires learning a lot of stuff and ideally have a CI-CD system.

@enzofrnt
Copy link
Author

@toinopt I said: “Beginners can indeed use your proposal for ease of use, but that doesn’t mean we shouldn’t provide a solution to do it ‘the right way’ for others.”

To clarify: the current image contains Luarock and Lua and can be modified by someone who has the necessary skills ("actually how Docker is supposed to work"). More beginner users can also use the solution proposed by Lambdax. Both will work in the current state of the pull request, and personally I don’t want to change it.

Only the maintainers of BeamMP-Server can say whether they want modifications or not.

@enzofrnt
Copy link
Author

enzofrnt commented Mar 8, 2026

Nothing new here ??

Copy link
Collaborator

@WiserTixx WiserTixx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could all french text be translated to English and could the BEAMMP_PROVIDER_DISABLE_CONFIG env var be added set to true in the compose file?

Minor cleanup across Docker-related files and CI workflow for clarity and consistency:

- .dockerignore: simplified comments and ensured docs/ scripts entries are listed (keeps README.md).
- .github/workflows/docker.yml: translated French comments to English for the Inspect step.
- .gitignore: fixed spacing in Docker env file comment.
- Dockerfile: removed an outdated comment line (no functional change to the COPY/WORKDIR steps).

These are non-functional cleanup changes to improve readability and consistency.
@enzofrnt
Copy link
Author

enzofrnt commented Mar 8, 2026

@WiserTixx
I've converted all French text to English.

Regarding BEAMMP_PROVIDER_DISABLE_CONFIG: I’m not sure why it needs to be in the compose file by default. If someone wants to rely only on environment variables for config, they can add this variable themselves. The current setup already provides a volume for the configuration file, so users can choose either a mounted config or env vars.

If the preferred approach for Docker is to use only environment variables for server config, I'd rather do it explicitly: remove the configuration volume from the compose file and set BEAMMP_PROVIDER_DISABLE_CONFIG=true in the Dockerfile so the image is env-only by default. That would make the intended use clear and avoid mixing "config file via volume" with "env-only" in the same example. (Note that someone who wants to use a config file can easily add a volume and override the BEAMMP_PROVIDER_DISABLE_CONFIG value.)

What do you prefer: adding the variable in the compose file as requested, or switching the Docker example to env-only (no config volume + variable in the Dockerfile)?

@WiserTixx
Copy link
Collaborator

There's already an env file loaded in the compose and there's also an env example file. Environment variables take priority over the config so there is no point in having both. env's are imo much easier to work with compared to mounts or editing the container, so let's go with env only. However because the env vars are registered in the compose it makes more sense to have the disable config var also in there. That way the user can still switch to a mounted config without editing the image.

@enzofrnt
Copy link
Author

enzofrnt commented Mar 9, 2026

Ok I will do so.

Update .env.example to include BEAMMP_PROVIDER_DISABLE_CONFIG so the provider config can be disabled via env. Simplify compose.yaml by removing the ./config volume mount and the explicit TZ environment entry while keeping the Resources volume and .env file reference.
@enzofrnt
Copy link
Author

enzofrnt commented Mar 9, 2026

Done : )

Remove the bind mount that mounted ./Resources into /app/Resources in compose.yaml. This stops the host Resources directory from being injected into the container (avoiding accidental overrides, permission/sync issues). Ports and env_file entries are unchanged; if runtime resources are still required, consider baking them into the image or adding a controlled volume.
Add a top-line comment to .env.example that points to the official BeamMP Server environment variable documentation (https://docs.beammp.com/server/manual/#env). This helps users locate authoritative guidance for configuring environment variables.
Remove BEAMMP_PROVIDER_DISABLE_CONFIG from .env.example and add it to the service environment in compose.yaml so the container receives the variable explicitly. .env.example retains BEAMMP_LOG_CHAT=true. This ensures the provider disable flag is injected at runtime via Docker Compose rather than left in the example env file.
Drop the fixed CMD ("--config=/config/ServerConfig.toml") from the Dockerfile, leaving only the ENTRYPOINT. This allows callers to pass runtime arguments or override the command without being forced to use the baked-in config flag.
Remove a redundant comment above the Resources volume mount in compose.yaml. This is a non-functional cleanup to reduce clutter in the compose file; the actual volume mapping (./Resources:/app/Resources) is unchanged.
@enzofrnt
Copy link
Author

Everythings done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

good first issue Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants