├── Dockerfile ├── README.md ├── as-root.sh ├── as-user.sh ├── build-l4d.sh ├── build-l4d2.sh ├── docker-compose.yml └── entrypoint.sh /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rockylinux/rockylinux:9-minimal AS base 2 | 3 | ADD as-root.sh . 4 | RUN ./as-root.sh 5 | 6 | WORKDIR /home/louis 7 | USER louis 8 | 9 | FROM base AS game 10 | 11 | ARG GAME_ID=222860 \ 12 | INSTALL_DIR="l4d2" \ 13 | DEFAULT_MAP="c14m1_junkyard" 14 | 15 | EXPOSE 27015/tcp 27015/udp 16 | 17 | ADD as-user.sh . 18 | RUN ./as-user.sh 19 | 20 | VOLUME ["/addons", "/cfg"] 21 | 22 | ENV DEFAULT_MAP=$DEFAULT_MAP \ 23 | DEFAULT_MODE="coop" \ 24 | PORT=0 \ 25 | HOSTNAME="Left4DevOps" \ 26 | REGION=255 \ 27 | GAME_ID=$GAME_ID \ 28 | INSTALL_DIR=$INSTALL_DIR \ 29 | STEAM_GROUP=0 \ 30 | HOST_CONTENT="" \ 31 | MOTD_CONTENT="Play nice, kill zombies" \ 32 | MOTD=0 33 | 34 | ADD entrypoint.sh . 35 | ENTRYPOINT ["./entrypoint.sh"] 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Left 4 Dead / Left 4 Dead 2 Dedicated Server Docker Image 2 | 3 | > "Oh, MAN! This is just like Counter-Strike!" 4 | 5 | This repository can be used to build docker images for both Left 4 Dead and Left 4 Dead 2. You can use this repository 6 | to build these images yourself, or pull them from our registry on [Docker Hub](https://hub.docker.com/u/left4devops). 7 | 8 | ## Getting Started 9 | 10 | Running a vanilla server can as simple as running: 11 | 12 | ```shell 13 | docker run --name l4d2 \ 14 | --network host \ 15 | left4devops/l4d2 16 | ``` 17 | 18 | > [!IMPORTANT] 19 | > The above example uses host networking (`--network host`), which is the preferred networking method, as it allows the 20 | > game server to correctly identify the IP of connecting players and can be used to ban players or anyone on the net 21 | > attempting to force access to RCON. To use host networking on windows or macOS, **enable host networking** from 22 | > [experimental features](https://docs.docker.com/network/network-tutorial-host/#prerequisites) in Docker version 4.29. 23 | 24 | ### Bridge Networking Mode 25 | Although the game server won't be able to correctly identify player's IP correctly, you can use Docker's default bridge 26 | networking mode instead. Ensure your game port is published using both the TCP and UDP protocols. 27 | 28 | ```shell 29 | docker run --name l4d2-bridged \ 30 | -p 27015:27015/tcp \ 31 | -p 27015:27015/udp \ 32 | left4devops/l4d2 33 | ``` 34 | 35 | ### Restart behaviour 36 | 37 | > "Someone needs to restart that generator!" 38 | 39 | `srcds_run` includes a watchdog to restart the server process in the event of a crash. However, you may want to include 40 | the `restart` parameter to ensure your server comes back up after a reboot, like so: 41 | 42 | ```shell 43 | docker run --name l4d2-reboot \ 44 | --restart unless-stopped \ 45 | left4devops/l4d2 46 | ``` 47 | 48 | ### Denial of Service Protection 49 | 50 | > "But you know, as long as I have a Molotov I can make a firewall!" 51 | 52 | This section is in draft, see the [discussion here](https://github.com/Left4DevOps/l4d2-docker/issues/6) for more 53 | detail. 54 | 55 | ## Joining a game 56 | 57 | There are a couple of ways you can achieve this: 58 | 59 | * Start a lobby, then as host, set the server type to **Best Available Dedicated**, open the developer console and 60 | type `mm_dedicated_force_servers [ip:port]`. This will try to use your server when the game starts. 61 | * Set the `STEAM_GROUP` variable and choose to start the game from a Steam Group server in your lobby. 62 | * Add the server as a favourite using the server browser `openserverbrowser` in game. This will make it appear as a 63 | steam group server in the menu. 64 | * Using the developer console, you can connect directly to the server `connect [ip:port]` 65 | * You can also open the Server Browser from steam under **View** > **Game Servers**. 66 | * Servers on your LAN will be shown as Steam Group servers automatically 67 | 68 | ## Custom Addons 69 | 70 | If you wanted to play a custom campaign on your server, or include sourcemod, you can mount a directory with any custom 71 | content into the `/addons/` directory. 72 | 73 | e.g. If your working directory had a addons folder: 74 | 75 | ```shell 76 | docker run --name l4d2-server-addons \ 77 | -v $(pwd)/addons:/addons/ 78 | left4devops/l4d2 79 | ``` 80 | 81 | ## Configuration 82 | 83 | ### Environment Variables 84 | You can pass environment variables to your Docker Container to configure a number of common settings. 85 | 86 | #### HOSTNAME 87 | 88 | This is the name of you server, as shown in the Server Browser and Steam Group Servers. Defaults to **Left4DevOps**. 89 | 90 | To change your hostname to **BILLS HERE**: 91 | 92 | ```shell 93 | docker run --name l4d2-hostname \ 94 | -e HOSTNAME="BILLS HERE" \ 95 | left4devops/l4d2 96 | ``` 97 | 98 | #### MOTD 99 | Automatically show the message of the day when players connect. Enable/disabled using `1`/`0`. Off (`0`) by default. If 100 | you run a lot of gameplay mods on your server, it can gbe helpful to outline them here 101 | 102 | #### HOST_CONTENT 103 | This is the slim banner shown at the top of the MOTD. In L4D1, it is also shown when players hold the `TAB` key. 104 | 105 | Can display plain text or a http URL. Neither game understands https (although L4D2 can redirect to a https url). 106 | 107 | By default, will show the hostname of your server. For simple banners, `HOST_CONTENT` using the environment variable. 108 | For more complex host banners, leave `HOST_CONTENT` empty and use a volume mount `/motd/host.txt`. 109 | 110 | #### MOTD_CONTENT 111 | Works in a similar fashion to `MOTD_CONTENT`, but is only visible when viewing the MOTD. This can be shown 112 | automatically or when the user presses `h`. Mount for complex motd files is `/motd/mymotd.txt` 113 | 114 | #### REGION 115 | 116 | > "I need every one of you inside, now!" 117 | 118 | To help hint to Steam where your server is located, set the `REGION` environment variable as one of the following 119 | numeric regions: 120 | 121 | | Location | REGION | 122 | |-----------------|--------| 123 | | East Coast USA | 0 | 124 | | West Coast USA | 1 | 125 | | South America | 2 | 126 | | Europe | 3 | 127 | | Asia | 4 | 128 | | Australia | 5 | 129 | | Middle East | 6 | 130 | | Africa | 7 | 131 | | World (Default) | 255 | 132 | 133 | If your server was in Europe: 134 | 135 | ```shell 136 | docker run --name l4d2-region \ 137 | -e REGION=3 \ 138 | left4devops/l4d2 139 | ``` 140 | 141 | #### STEAM_GROUP 142 | 143 | Make your server easier to join, by attaching it to a Steam Group. Steam Group Servers are shown in the bottom right 144 | corner of the Game menu and can be easily picked as a hosting option in Left 4 Dead 2. In game, players can press `h` to 145 | show the message of the day and then join your steam group, so they can find it again in the future. You can find the ID 146 | of your Steam Group at the top of the Admin page. 147 | 148 | ```shell 149 | docker run --name l4d2-group \ 150 | -e STEAM_GROUP=666 \ 151 | left4devops/l4d2 152 | ``` 153 | 154 | ##### STEAM_GROUP_EXCLUSIVE 155 | 156 | Normally, your server will be available for anyone using the **Best Available Dedicated** setting in their lobby. You 157 | can prevent this by setting `STEAM_GROUP_EXCLUSIVE` to true. Any member of your group will be able to start a game as 158 | lobby leader. 159 | 160 | ```shell 161 | docker run --name l4d2-group-exclusive \ 162 | -e STEAM_GROUP=666 \ 163 | -e STEAM_GROUP_EXCLUSIVE=true \ 164 | left4devops/l4d2 165 | ``` 166 | 167 | > [!NOTE] 168 | > Non members can still pick your server using `mm_dedicated_force_servers` 169 | 170 | #### PORT 171 | 172 | The game should find the next free port after 27015 to host the game on. If using bridge networking, 27015 will usually 173 | be picked automatically, as no other services should be running in the container. You may prefer to hardcode the game 174 | port to simply your port forwarding or firewall configuration. Simply set `PORT` to your desired port. Game traffic and 175 | RCON always operate on the same port number. 176 | 177 | Change the port with host networking: 178 | 179 | ```shell 180 | docker run --name l4d2-port \ 181 | -e PORT=27016 \ 182 | --network host \ 183 | left4devops/l4d2 184 | ``` 185 | 186 | or if using bridge networking, remember to expose your new ports: 187 | 188 | ```shell 189 | docker run --name l4d2-port-bridged \ 190 | -e PORT=27016 \ 191 | -p 27016:27016/tcp \ 192 | -p 27016:27016/udp \ 193 | left4devops/l4d2 194 | ``` 195 | 196 | To make it slightly less painful to change the port, you could use: 197 | 198 | ```shell 199 | PORT=27017 200 | docker run --name l4d2-port-var \ 201 | -e PORT=$PORT \ 202 | -p $PORT:$PORT/tcp \ 203 | -p $PORT:$PORT/udp \ 204 | left4devops/l4d2 205 | ``` 206 | > [!WARNING] 207 | > When publishing your ports with bridge networking, ensure the same port is used for both client and host. Otherwise 208 | > matchmaking will advertise the wrong port and new players will not be able to connect 209 | 210 | #### DEFAULT_MAP 211 | 212 | Can be used to set the map when the server is first loaded. Defaults to the newest map for each game. The map names are 213 | different in each game. You can get a full list of maps names by typing `maps` in your Developer Console. To start your 214 | L4D2 server on Dead Center: 215 | 216 | ```shell 217 | docker run --name l4d2-map \ 218 | -e DEFAULT_MAP=c1m1_hotel \ 219 | left4devops/l4d2 220 | ``` 221 | 222 | #### DEFAULT_MODE 223 | 224 | Sets the mode used by your server when it is first loaded. Defaults to coop, but can be changed from a lobby. 225 | 226 | ```shell 227 | docker run --name l4d2-mode \ 228 | -e DEFAULT_MODE=versus \ 229 | left4devops/l4d2 230 | ``` 231 | 232 | #### GAME_TYPES 233 | 234 | Can be used to limit which game modes can be played on your server. Leave unspecified to allow all game modes. Values 235 | include `coop,realism,survival,versus,scavenge,dash,holdout,shootzones` 236 | 237 | ```shell 238 | docker run --name l4d2-modes \ 239 | -e GAME_TYPES="versus,mutation19" \ 240 | left4devops/l4d2 241 | ``` 242 | 243 | #### FORK 244 | 245 | Run multiple games from the same container, with ports being automatically allocated by default. The `PORT` variable is 246 | ignored. You can customise the behaviour of the different instances by mounting multiple **server##.cfg** files. 247 | 248 | ```shell 249 | docker run --name l4d2-forked \ 250 | --network host \ 251 | -e FORK=2 252 | -v $pwd/server01.cfg:/cfg/server01.cfg 253 | -v $pwd/server02.cfg:/cfg/server02.cfg 254 | left4devops/l4d2 255 | ``` 256 | 257 | #### LAN 258 | 259 | Runs the server without needing to talk to Steam, but connecting players need a local IP address. 260 | 261 | ```shell 262 | docker run --name l4d2-lan \ 263 | --network host \ 264 | -e LAN=true \ 265 | left4devops/l4d2 266 | ``` 267 | 268 | #### RCON_PASSWORD 269 | 270 | > Son, we're immune, we're tired, and there's infected in the damn woods. Now cut the shit and let us in! 271 | 272 | To issue commands to the server whilst you're playing on it, set an RCON password. Logging into a server with RCON also 273 | grants you vote kick immunity. To set a password: 274 | 275 | ```shell 276 | docker run --name l4d2-rcon \ 277 | --network host \ 278 | -e RCON_PASSWORD=cuttheshitandletusin \ 279 | left4devops/l4d2 280 | ``` 281 | 282 | Then in game, once you have joined the server, open the Developer Console and type 283 | 284 | ``` 285 | rcon_password cuttheshitandletusin 286 | rcon 287 | ``` 288 | 289 | > [!NOTE] 290 | > Make sure the password is unique, as bad actors commonly attempt dictionary attacks. `srcds_run` will 291 | > ban an IP after a few failed attempts. This can have the unintended side effect of banning everyone if using bridge 292 | > networking. 293 | 294 | #### NET_CON_PORT 295 | 296 | Similar to RCON, the Network Console allows you to administrator your server, but using a `telnet` client. If not using 297 | a password, I would strongly recommend using a firewall to block public access to the port. To start listening on port 298 | 17017: 299 | 300 | ```shell 301 | docker run --name l4d2-netcon \ 302 | --network host \ 303 | -e NET_CON_PORT=17017 \ 304 | left4devops/l4d2 305 | ``` 306 | 307 | You could then connect using your telnet client: 308 | 309 | ```shell 310 | telnet localhost 17015 311 | ``` 312 | 313 | ##### NET_CON_PASSWORD 314 | To set a net con password: 315 | 316 | ```shell 317 | docker run --name l4d2-netcon-password \ 318 | --network host \ 319 | -e NET_CON_PORT=17015 \ 320 | -e NET_CON_PASSWORD=cuttheshitandletusin \ 321 | left4devops/l4d2 322 | ``` 323 | 324 | Once connected via your telnet client, type **PASS** and then your password: 325 | 326 | ```shell 327 | telnet localhost 17015 328 | PASS cuttheshitandletusin 329 | ``` 330 | 331 | A telnet client is included in the image so you can connect using `docker exec`: 332 | 333 | ```shell 334 | docker exec -it l4d2 telnet localhost 17015 335 | ``` 336 | 337 | #### EXTRA_ARGS 338 | 339 | Got a small number of other settings you want to add? To spawn larger mobs more frequently: 340 | 341 | ```shell 342 | docker run --name l4d2-args \ 343 | -e EXTRA_ARGS="+z_mega_mob_size 100 +z_mega_mob_spawn_min_interval 180 +z_mega_mob_spawn_max_interval 600" \ 344 | left4devops/l4d2 345 | ``` 346 | 347 | ### server.cfg 348 | 349 | `srcds_run` will look for a `server.cfg` file. If you have lots of config, store them in a file and mount it to 350 | `/cfg/server.cfg`. 351 | 352 | ```shell 353 | docker run --name l4d2-server-cfg \ 354 | -v $(pwd)/server.cfg:/cfg/server.cfg 355 | left4devops/l4d2 356 | ``` 357 | 358 | ### Command line replacement 359 | 360 | You can completely replace the guided environment variable configuration by providing arguments when starting the 361 | container 362 | 363 | ```shell 364 | docker run --name l4d2-command-line \ 365 | left4devops/l4d2 +hostname "Vannah Hotel" 366 | ``` 367 | 368 | -------------------------------------------------------------------------------- /as-root.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | microdnf -y install SDL2.i686 \ 3 | libcurl.i686 \ 4 | glibc-langpack-en \ 5 | tar \ 6 | telnet 7 | microdnf -y update 8 | microdnf clean all 9 | 10 | useradd louis 11 | 12 | mkdir /addons /cfg /motd /tmp/dumps 13 | chown louis:louis /addons /cfg /motd /tmp/dumps -------------------------------------------------------------------------------- /as-user.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Install steamcmd 3 | mkdir -p .steam/sdk32/ 4 | ln -s ~/linux32/steamclient.so ~/.steam/sdk32/steamclient.so 5 | curl https://media.steampowered.com/installer/steamcmd_linux.tar.gz | tar -xzvf - 6 | 7 | # Convenient symlinks for mount points 8 | if [ "${INSTALL_DIR}" = "l4d2" ]; then 9 | GAME_DIR="${INSTALL_DIR}/left4dead2" 10 | elif [ "${INSTALL_DIR}" = "l4d" ]; then 11 | GAME_DIR="${INSTALL_DIR}/left4dead" 12 | else 13 | exit 100 14 | fi 15 | 16 | mkdir -p ./"${GAME_DIR}" 17 | ln -s /addons "./${GAME_DIR}/addons" 18 | ln -s /cfg "./${GAME_DIR}/cfg" 19 | ln -s /motd/host.txt "./${GAME_DIR}/myhost.txt" 20 | ln -s /motd/motd.txt "./${GAME_DIR}/mymotd.txt" 21 | 22 | # Install game 23 | echo """force_install_dir "/home/louis/${INSTALL_DIR}" 24 | login anonymous 25 | app_update ${GAME_ID} 26 | quit""" > update.txt 27 | if [ "${INSTALL_DIR}" = "l4d2" ]; then 28 | # https://github.com/ValveSoftware/steam-for-linux/issues/11522 29 | echo """force_install_dir "/home/louis/${INSTALL_DIR}" 30 | login anonymous 31 | @sSteamCmdForcePlatformType windows 32 | app_update ${GAME_ID} 33 | @sSteamCmdForcePlatformType linux 34 | app_update ${GAME_ID} validate 35 | quit""" > first-install-l4d2.txt 36 | ./steamcmd.sh +runscript first-install-l4d2.txt 37 | else 38 | ./steamcmd.sh +runscript update.txt 39 | fi -------------------------------------------------------------------------------- /build-l4d.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build --progress plain -t left4devops/l4d \ 3 | --build-arg GAME_ID=222840 \ 4 | --build-arg INSTALL_DIR="l4d" \ 5 | --build-arg DEFAULT_MAP="l4d_river01_docks" \ 6 | . -------------------------------------------------------------------------------- /build-l4d2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build --progress plain -t left4devops/l4d2 . -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | l4d2: 3 | image: left4devops/l4d2 4 | network_mode: host 5 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./steamcmd.sh +runscript update.txt 3 | 4 | cd "${INSTALL_DIR}" || exit 50 5 | 6 | if [ "${INSTALL_DIR}" = "l4d2" ]; then 7 | GAME_DIR="left4dead2" 8 | elif [ "${INSTALL_DIR}" = "l4d" ]; then 9 | GAME_DIR="left4dead" 10 | else 11 | exit 100 12 | fi 13 | 14 | if [ $# -gt 0 ]; then 15 | ./srcds_run "$@" 16 | else 17 | STARTUP=("./srcds_run") 18 | STARTUP+=("+sv_logecho 1") 19 | STARTUP+=("+hostname \"${HOSTNAME}\"") 20 | STARTUP+=("+sv_region ${REGION}") 21 | 22 | STARTUP+=("+motd_enabled ${MOTD}") 23 | 24 | if [[ -e "${GAME_DIR}/myhost.txt" ]]; then 25 | STARTUP+=("+hostfile myhost.txt") 26 | elif [ -n "${HOST_CONTENT}" ]; then 27 | echo "${HOST_CONTENT}" > "${GAME_DIR}/envhost.txt" 28 | STARTUP+=("+hostfile envhost.txt") 29 | else 30 | echo "${HOSTNAME}" > "${GAME_DIR}/myhostname.txt" 31 | STARTUP+=("+hostfile myhostname.txt") 32 | fi 33 | 34 | if [[ -e "${GAME_DIR}/mymotd.txt" ]]; then 35 | STARTUP+=("+motdfile mymotd.txt") 36 | elif [ -n "${MOTD_CONTENT}" ]; then 37 | echo "${MOTD_CONTENT}" > "${GAME_DIR}/envmotd.txt" 38 | STARTUP+=("+motdfile envmotd.txt") 39 | fi 40 | 41 | if [ "${STEAM_GROUP}" -gt 0 ]; then 42 | STARTUP+=("+sv_steamgroup ${STEAM_GROUP}") 43 | if [ "${STEAM_GROUP_EXCLUSIVE}" ] ; then 44 | STARTUP+=("+sv_steamgroup_exclusive 1") 45 | fi 46 | fi 47 | 48 | STARTUP+=("+map \"$DEFAULT_MAP $DEFAULT_MODE\"") 49 | 50 | if [ -n "${GAME_TYPES}" ]; then 51 | STARTUP+=("+sv_gametypes \"${GAME_TYPES}\"") 52 | fi 53 | 54 | if [ "${FORK:-0}" -gt 0 ]; then 55 | STARTUP+=("-fork ${FORK} +exec server##.cfg") 56 | else 57 | if [ "${PORT:-0}" -gt 0 ]; then 58 | STARTUP+=("-port $PORT") 59 | fi 60 | fi 61 | 62 | if [ "${LAN}" ] ; then 63 | STARTUP+=("+sv_lan 1") 64 | fi 65 | 66 | if [ -n "${RCON_PASSWORD}" ]; then 67 | STARTUP+=("+rcon_password \"${RCON_PASSWORD}\"") 68 | fi 69 | 70 | if [ "${NET_CON_PORT:-0}" -gt 0 ]; then 71 | STARTUP+=("-netconport ${NET_CON_PORT}") 72 | if [ -n "${NET_CON_PASSWORD}" ]; then 73 | STARTUP+=("-netconpassword \"${NET_CON_PASSWORD}\"") 74 | fi 75 | fi 76 | 77 | if [ -n "${EXTRA_ARGS}" ]; then 78 | STARTUP+=("${EXTRA_ARGS}") 79 | fi 80 | 81 | ${STARTUP[*]} 82 | fi --------------------------------------------------------------------------------