mirror of
https://github.com/factoriotools/factorio-docker.git
synced 2025-07-03 23:48:05 +02:00
Compare commits
1 Commits
master
...
feature/ro
Author | SHA1 | Date | |
---|---|---|---|
7e59315e3d |
136
README-ROOTLESS.md
Normal file
136
README-ROOTLESS.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Rootless Docker Support
|
||||
|
||||
This document describes the rootless Docker images for Factorio, which are designed to work better with rootless Docker installations and avoid permission issues.
|
||||
|
||||
## What is Rootless Docker?
|
||||
|
||||
Rootless Docker allows running the Docker daemon and containers as a non-root user, which improves security by eliminating the need for root privileges. However, it introduces complexity with UID/GID mapping that can cause permission issues with volumes.
|
||||
|
||||
## Rootless Image Tags
|
||||
|
||||
For each regular Factorio image tag, there's a corresponding rootless tag with the `-rootless` suffix:
|
||||
|
||||
- `latest` → `latest-rootless`
|
||||
- `stable` → `stable-rootless`
|
||||
- `2.0.55` → `2.0.55-rootless`
|
||||
- etc.
|
||||
|
||||
## Key Differences from Regular Images
|
||||
|
||||
1. **No dynamic UID/GID mapping**: The rootless images run as UID 1000 by default and don't support PUID/PGID environment variables
|
||||
2. **No runtime chown operations**: Eliminates the recursive chown that can cause race conditions
|
||||
3. **Simplified permissions**: All directories are created with open permissions (777) during build
|
||||
4. **USER directive**: The container runs as non-root from the start
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-p 34197:34197/udp \
|
||||
-p 27015:27015/tcp \
|
||||
-v /opt/factorio:/factorio \
|
||||
--name factorio \
|
||||
factoriotools/factorio:stable-rootless
|
||||
```
|
||||
|
||||
### With Rootless Docker
|
||||
|
||||
If you're running rootless Docker, the container will work out of the box:
|
||||
|
||||
```bash
|
||||
# As your regular user (not root)
|
||||
docker run -d \
|
||||
-p 34197:34197/udp \
|
||||
-p 27015:27015/tcp \
|
||||
-v ~/factorio:/factorio \
|
||||
--name factorio \
|
||||
factoriotools/factorio:stable-rootless
|
||||
```
|
||||
|
||||
### With Regular Docker
|
||||
|
||||
If you're running regular Docker but want to avoid permission issues:
|
||||
|
||||
```bash
|
||||
# Pre-create the volume directory with your user's permissions
|
||||
mkdir -p /opt/factorio
|
||||
sudo chown -R $(id -u):$(id -g) /opt/factorio
|
||||
|
||||
# Run the container
|
||||
docker run -d \
|
||||
--user $(id -u):$(id -g) \
|
||||
-p 34197:34197/udp \
|
||||
-p 27015:27015/tcp \
|
||||
-v /opt/factorio:/factorio \
|
||||
--name factorio \
|
||||
factoriotools/factorio:stable-rootless
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All the same environment variables from the regular image are supported, except:
|
||||
- `PUID` - Not supported (container runs as UID 1000)
|
||||
- `PGID` - Not supported (container runs as GID 1000)
|
||||
|
||||
## Migrating from Regular Images
|
||||
|
||||
If you're switching from a regular image to a rootless image:
|
||||
|
||||
1. Stop your existing container
|
||||
2. Fix permissions on your volume (one time only):
|
||||
```bash
|
||||
sudo chown -R 1000:1000 /opt/factorio
|
||||
# Or if you want to match your user:
|
||||
sudo chown -R $(id -u):$(id -g) /opt/factorio
|
||||
```
|
||||
3. Start the new rootless container
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Denied Errors
|
||||
|
||||
If you get permission errors, ensure your volume directory is writable by UID 1000 or your user:
|
||||
|
||||
```bash
|
||||
# Check current ownership
|
||||
ls -la /opt/factorio
|
||||
|
||||
# Fix ownership for UID 1000 (default)
|
||||
sudo chown -R 1000:1000 /opt/factorio
|
||||
|
||||
# Or fix for your current user
|
||||
sudo chown -R $(id -u):$(id -g) /opt/factorio
|
||||
```
|
||||
|
||||
### Running as a Different User
|
||||
|
||||
If you need to run as a different UID, override it at runtime:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--user 2000:2000 \
|
||||
-v /opt/factorio:/factorio \
|
||||
factoriotools/factorio:stable-rootless
|
||||
```
|
||||
|
||||
## Building Rootless Images
|
||||
|
||||
To build rootless images locally:
|
||||
|
||||
```bash
|
||||
# Build for current architecture
|
||||
python3 build-rootless.py
|
||||
|
||||
# Build and push multi-arch images
|
||||
python3 build-rootless.py --multiarch --push-tags
|
||||
```
|
||||
|
||||
## Why Use Rootless Images?
|
||||
|
||||
1. **Avoid permission issues**: No more files with unexpected ownership
|
||||
2. **Better security**: Runs as non-root by default
|
||||
3. **Simpler**: No complex permission logic at startup
|
||||
4. **Faster startup**: No recursive chown operations
|
||||
5. **Rootless Docker compatible**: Works seamlessly with rootless Docker installations
|
31
README.md
31
README.md
@ -439,6 +439,37 @@ stream {
|
||||
|
||||
If your factorio host uses multiple IP addresses (very common with IPv6), you might additionally need to bind Factorio to a single IP (otherwise the UDP proxy might get confused with IP mismatches). To do that pass the `BIND` envvar to the container: `docker run --network=host -e BIND=2a02:1234::5678 ...`
|
||||
|
||||
## Rootless Docker Support
|
||||
|
||||
If you're experiencing permission issues or want better security, consider using the rootless images. These images are designed to work seamlessly with rootless Docker installations and avoid common permission problems.
|
||||
|
||||
### Rootless Image Tags
|
||||
|
||||
Each regular tag has a corresponding rootless version with the `-rootless` suffix:
|
||||
- `latest-rootless`
|
||||
- `stable-rootless`
|
||||
- `2.0.55-rootless`
|
||||
|
||||
### Quick Start with Rootless
|
||||
|
||||
```shell
|
||||
docker run -d \
|
||||
-p 34197:34197/udp \
|
||||
-p 27015:27015/tcp \
|
||||
-v ~/factorio:/factorio \
|
||||
--name factorio \
|
||||
--restart=unless-stopped \
|
||||
factoriotools/factorio:stable-rootless
|
||||
```
|
||||
|
||||
Key differences:
|
||||
- No `chown` command needed
|
||||
- No PUID/PGID environment variables
|
||||
- Runs as UID 1000 by default
|
||||
- No permission issues with volumes
|
||||
|
||||
For more information, see the [Rootless Docker documentation](README-ROOTLESS.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My server is listed in the server browser, but nobody can connect
|
||||
|
128
build-rootless.py
Executable file
128
build-rootless.py
Executable file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
PLATFORMS = [
|
||||
"linux/arm64",
|
||||
"linux/amd64",
|
||||
]
|
||||
|
||||
|
||||
def create_builder(build_dir, builder_name, platform):
|
||||
check_exists_command = ["docker", "buildx", "inspect", builder_name]
|
||||
if subprocess.run(check_exists_command, stderr=subprocess.DEVNULL).returncode != 0:
|
||||
create_command = ["docker", "buildx", "create", "--platform", platform, "--name", builder_name]
|
||||
try:
|
||||
subprocess.run(create_command, cwd=build_dir, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Creating builder failed")
|
||||
exit(1)
|
||||
|
||||
|
||||
def build_and_push_multiarch(build_dir, build_args, push):
|
||||
builder_name = "factoriotools-rootless-multiarch"
|
||||
platform=",".join(PLATFORMS)
|
||||
create_builder(build_dir, builder_name, platform)
|
||||
build_command = ["docker", "buildx", "build", "--platform", platform, "--builder", builder_name] + build_args
|
||||
if push:
|
||||
build_command.append("--push")
|
||||
try:
|
||||
subprocess.run(build_command, cwd=build_dir, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Build and push of rootless image failed")
|
||||
exit(1)
|
||||
|
||||
|
||||
def build_singlearch(build_dir, build_args):
|
||||
build_command = ["docker", "build"] + build_args
|
||||
try:
|
||||
subprocess.run(build_command, cwd=build_dir, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Build of rootless image failed")
|
||||
exit(1)
|
||||
|
||||
|
||||
def push_singlearch(tags):
|
||||
for tag in tags:
|
||||
try:
|
||||
subprocess.run(["docker", "push", f"factoriotools/factorio:{tag}"],
|
||||
check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Docker push failed")
|
||||
exit(1)
|
||||
|
||||
|
||||
def build_and_push(sha256, version, tags, push, multiarch):
|
||||
build_dir = tempfile.mktemp()
|
||||
shutil.copytree("docker", build_dir)
|
||||
# Use the rootless Dockerfile
|
||||
build_args = ["-f", "Dockerfile.rootless", "--build-arg", f"VERSION={version}", "--build-arg", f"SHA256={sha256}", "."]
|
||||
for tag in tags:
|
||||
build_args.extend(["-t", f"factoriotools/factorio:{tag}"])
|
||||
if multiarch:
|
||||
build_and_push_multiarch(build_dir, build_args, push)
|
||||
else:
|
||||
build_singlearch(build_dir, build_args)
|
||||
if push:
|
||||
push_singlearch(tags)
|
||||
|
||||
|
||||
def login():
|
||||
try:
|
||||
username = os.environ["DOCKER_USERNAME"]
|
||||
password = os.environ["DOCKER_PASSWORD"]
|
||||
subprocess.run(["docker", "login", "-u", username, "-p", password], check=True)
|
||||
except KeyError:
|
||||
print("Username and password need to be given")
|
||||
exit(1)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Docker login failed")
|
||||
exit(1)
|
||||
|
||||
|
||||
def generate_rootless_tags(original_tags):
|
||||
"""Generate rootless-specific tags from original tags"""
|
||||
rootless_tags = []
|
||||
for tag in original_tags:
|
||||
# Add -rootless suffix to each tag
|
||||
rootless_tags.append(f"{tag}-rootless")
|
||||
return rootless_tags
|
||||
|
||||
|
||||
def main(push_tags=False, multiarch=False):
|
||||
with open(os.path.join(os.path.dirname(__file__), "buildinfo.json")) as file_handle:
|
||||
builddata = json.load(file_handle)
|
||||
|
||||
if push_tags:
|
||||
login()
|
||||
|
||||
# Build only the latest stable and experimental versions for rootless
|
||||
versions_to_build = []
|
||||
|
||||
# Find latest stable and experimental versions
|
||||
for version, buildinfo in builddata.items():
|
||||
if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]:
|
||||
versions_to_build.append((version, buildinfo))
|
||||
|
||||
for version, buildinfo in versions_to_build:
|
||||
sha256 = buildinfo["sha256"]
|
||||
original_tags = buildinfo["tags"]
|
||||
rootless_tags = generate_rootless_tags(original_tags)
|
||||
build_and_push(sha256, version, rootless_tags, push_tags, multiarch)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
push_tags = False
|
||||
multiarch = False
|
||||
for arg in sys.argv[1:]:
|
||||
if arg == "--push-tags":
|
||||
push_tags = True
|
||||
elif arg == "--multiarch":
|
||||
multiarch = True
|
||||
main(push_tags, multiarch)
|
93
docker/Dockerfile.rootless
Normal file
93
docker/Dockerfile.rootless
Normal file
@ -0,0 +1,93 @@
|
||||
# build rcon client
|
||||
FROM debian:stable-slim AS rcon-builder
|
||||
RUN apt-get -q update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install build-essential
|
||||
|
||||
WORKDIR /src
|
||||
COPY rcon/ /src
|
||||
RUN make
|
||||
|
||||
# build factorio image
|
||||
FROM debian:stable-slim
|
||||
LABEL maintainer="https://github.com/factoriotools/factorio-docker"
|
||||
|
||||
ARG BOX64_VERSION=v0.2.4
|
||||
|
||||
# optionally utilize a built-in map-gen-preset (see data/base/prototypes/map-gen-presets
|
||||
ARG PRESET
|
||||
|
||||
# number of retries that curl will use when pulling the headless server tarball
|
||||
ARG CURL_RETRIES=8
|
||||
|
||||
ENV PORT=34197 \
|
||||
RCON_PORT=27015 \
|
||||
SAVES=/factorio/saves \
|
||||
PRESET="$PRESET" \
|
||||
CONFIG=/factorio/config \
|
||||
MODS=/factorio/mods \
|
||||
SCENARIOS=/factorio/scenarios \
|
||||
SCRIPTOUTPUT=/factorio/script-output \
|
||||
DLC_SPACE_AGE="true"
|
||||
|
||||
SHELL ["/bin/bash", "-eo", "pipefail", "-c"]
|
||||
|
||||
RUN apt-get -q update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install ca-certificates curl jq pwgen xz-utils procps gettext-base --no-install-recommends \
|
||||
&& if [[ "$(uname -m)" == "aarch64" ]]; then \
|
||||
echo "installing ARM compatability layer" \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install unzip --no-install-recommends \
|
||||
&& curl -LO https://github.com/ptitSeb/box64/releases/download/${BOX64_VERSION}/box64-GENERIC_ARM-RelWithDebInfo.zip \
|
||||
&& unzip box64-GENERIC_ARM-RelWithDebInfo.zip -d /bin \
|
||||
&& rm -f box64-GENERIC_ARM-RelWithDebInfo.zip \
|
||||
&& chmod +x /bin/box64; \
|
||||
fi \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# version checksum of the archive to download
|
||||
ARG VERSION
|
||||
ARG SHA256
|
||||
|
||||
LABEL factorio.version=${VERSION}
|
||||
|
||||
ENV VERSION=${VERSION} \
|
||||
SHA256=${SHA256}
|
||||
|
||||
RUN set -ox pipefail \
|
||||
&& if [[ "${VERSION}" == "" ]]; then \
|
||||
echo "build-arg VERSION is required" \
|
||||
&& exit 1; \
|
||||
fi \
|
||||
&& if [[ "${SHA256}" == "" ]]; then \
|
||||
echo "build-arg SHA256 is required" \
|
||||
&& exit 1; \
|
||||
fi \
|
||||
&& archive="/tmp/factorio_headless_x64_$VERSION.tar.xz" \
|
||||
&& mkdir -p /opt /factorio \
|
||||
&& curl -sSL "https://www.factorio.com/get-download/$VERSION/headless/linux64" -o "$archive" --retry $CURL_RETRIES \
|
||||
&& echo "$SHA256 $archive" | sha256sum -c \
|
||||
|| (sha256sum "$archive" && file "$archive" && exit 1) \
|
||||
&& tar xf "$archive" --directory /opt \
|
||||
&& chmod ugo=rwx /opt/factorio \
|
||||
&& rm "$archive" \
|
||||
&& ln -s "$SCENARIOS" /opt/factorio/scenarios \
|
||||
&& ln -s "$SAVES" /opt/factorio/saves \
|
||||
&& mkdir -p /opt/factorio/config/
|
||||
|
||||
COPY files/*.sh /
|
||||
COPY files/docker-entrypoint-rootless.sh /docker-entrypoint.sh
|
||||
COPY files/config.ini /opt/factorio/config/config.ini
|
||||
COPY --from=rcon-builder /src/rcon /bin/rcon
|
||||
|
||||
# Make all scripts executable
|
||||
RUN chmod +x /*.sh
|
||||
|
||||
# Set proper permissions for the factorio directory
|
||||
RUN chmod -R 777 /opt/factorio /factorio
|
||||
|
||||
VOLUME /factorio
|
||||
EXPOSE $PORT/udp $RCON_PORT/tcp
|
||||
|
||||
# Run as non-root user (UID 1000 is common for the first user in rootless containers)
|
||||
USER 1000:1000
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
124
docker/files/docker-entrypoint-rootless.sh
Executable file
124
docker/files/docker-entrypoint-rootless.sh
Executable file
@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
set -eoux pipefail
|
||||
INSTALLED_DIRECTORY=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||
FACTORIO_VOL=/factorio
|
||||
LOAD_LATEST_SAVE="${LOAD_LATEST_SAVE:-true}"
|
||||
GENERATE_NEW_SAVE="${GENERATE_NEW_SAVE:-false}"
|
||||
PRESET="${PRESET:-""}"
|
||||
SAVE_NAME="${SAVE_NAME:-""}"
|
||||
BIND="${BIND:-""}"
|
||||
CONSOLE_LOG_LOCATION="${CONSOLE_LOG_LOCATION:-""}"
|
||||
|
||||
# Create directories if they don't exist
|
||||
# In rootless mode, these should be writable by the container user
|
||||
mkdir -p "$FACTORIO_VOL"
|
||||
mkdir -p "$SAVES"
|
||||
mkdir -p "$CONFIG"
|
||||
mkdir -p "$MODS"
|
||||
mkdir -p "$SCENARIOS"
|
||||
mkdir -p "$SCRIPTOUTPUT"
|
||||
|
||||
# Generate RCON password if needed
|
||||
if [[ ! -f $CONFIG/rconpw ]]; then
|
||||
pwgen 15 1 >"$CONFIG/rconpw"
|
||||
fi
|
||||
|
||||
# Copy default configs if they don't exist
|
||||
if [[ ! -f $CONFIG/server-settings.json ]]; then
|
||||
cp /opt/factorio/data/server-settings.example.json "$CONFIG/server-settings.json"
|
||||
fi
|
||||
|
||||
if [[ ! -f $CONFIG/map-gen-settings.json ]]; then
|
||||
cp /opt/factorio/data/map-gen-settings.example.json "$CONFIG/map-gen-settings.json"
|
||||
fi
|
||||
|
||||
if [[ ! -f $CONFIG/map-settings.json ]]; then
|
||||
cp /opt/factorio/data/map-settings.example.json "$CONFIG/map-settings.json"
|
||||
fi
|
||||
|
||||
# Clean up incomplete saves
|
||||
NRTMPSAVES=$( find -L "$SAVES" -iname \*.tmp.zip -mindepth 1 | wc -l )
|
||||
if [[ $NRTMPSAVES -gt 0 ]]; then
|
||||
rm -f "$SAVES"/*.tmp.zip
|
||||
fi
|
||||
|
||||
# Update mods if requested
|
||||
if [[ ${UPDATE_MODS_ON_START:-} == "true" ]]; then
|
||||
${INSTALLED_DIRECTORY}/docker-update-mods.sh
|
||||
fi
|
||||
|
||||
# Handle DLC
|
||||
${INSTALLED_DIRECTORY}/docker-dlc.sh
|
||||
|
||||
# In rootless mode, we don't need to handle user switching or chown
|
||||
# The container runs as the specified user from the start
|
||||
EXEC=""
|
||||
if [[ -f /bin/box64 ]]; then
|
||||
# Use emulator for ARM hosts
|
||||
EXEC="/bin/box64"
|
||||
fi
|
||||
|
||||
# Update config path
|
||||
sed -i '/write-data=/c\write-data=\/factorio/' /opt/factorio/config/config.ini
|
||||
|
||||
# Generate new save if needed
|
||||
NRSAVES=$(find -L "$SAVES" -iname \*.zip -mindepth 1 | wc -l)
|
||||
if [[ $GENERATE_NEW_SAVE != true && $NRSAVES == 0 ]]; then
|
||||
GENERATE_NEW_SAVE=true
|
||||
SAVE_NAME=_autosave1
|
||||
fi
|
||||
|
||||
if [[ $GENERATE_NEW_SAVE == true ]]; then
|
||||
if [[ -z "$SAVE_NAME" ]]; then
|
||||
echo "If \$GENERATE_NEW_SAVE is true, you must specify \$SAVE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f "$SAVES/$SAVE_NAME.zip" ]]; then
|
||||
echo "Map $SAVES/$SAVE_NAME.zip already exists, skipping map generation"
|
||||
else
|
||||
if [[ ! -z "$PRESET" ]]; then
|
||||
$EXEC /opt/factorio/bin/x64/factorio \
|
||||
--create "$SAVES/$SAVE_NAME.zip" \
|
||||
--preset "$PRESET" \
|
||||
--map-gen-settings "$CONFIG/map-gen-settings.json" \
|
||||
--map-settings "$CONFIG/map-settings.json"
|
||||
else
|
||||
$EXEC /opt/factorio/bin/x64/factorio \
|
||||
--create "$SAVES/$SAVE_NAME.zip" \
|
||||
--map-gen-settings "$CONFIG/map-gen-settings.json" \
|
||||
--map-settings "$CONFIG/map-settings.json"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build command flags
|
||||
FLAGS=(\
|
||||
--port "$PORT" \
|
||||
--server-settings "$CONFIG/server-settings.json" \
|
||||
--server-banlist "$CONFIG/server-banlist.json" \
|
||||
--rcon-port "$RCON_PORT" \
|
||||
--server-whitelist "$CONFIG/server-whitelist.json" \
|
||||
--use-server-whitelist \
|
||||
--server-adminlist "$CONFIG/server-adminlist.json" \
|
||||
--rcon-password "$(cat "$CONFIG/rconpw")" \
|
||||
--server-id /factorio/config/server-id.json \
|
||||
--mod-directory "$MODS" \
|
||||
)
|
||||
|
||||
if [ -n "$CONSOLE_LOG_LOCATION" ]; then
|
||||
FLAGS+=( --console-log "$CONSOLE_LOG_LOCATION" )
|
||||
fi
|
||||
|
||||
if [ -n "$BIND" ]; then
|
||||
FLAGS+=( --bind "$BIND" )
|
||||
fi
|
||||
|
||||
if [[ $LOAD_LATEST_SAVE == true ]]; then
|
||||
FLAGS+=( --start-server-load-latest )
|
||||
else
|
||||
FLAGS+=( --start-server "$SAVE_NAME" )
|
||||
fi
|
||||
|
||||
# Execute factorio
|
||||
# In rootless mode, we run directly without user switching
|
||||
exec $EXEC /opt/factorio/bin/x64/factorio "${FLAGS[@]}" "$@"
|
Reference in New Issue
Block a user