Compare commits

..

1 Commits

Author SHA1 Message Date
7e59315e3d feat: Add rootless Docker support
Implements #547 - Add support for rootless Docker images to avoid permission issues.

Key changes:
- Add Dockerfile.rootless that runs as UID 1000 by default
- Create simplified entrypoint script without chown operations
- Add build-rootless.py to build rootless variants with -rootless suffix
- Document rootless usage in README-ROOTLESS.md
- Update main README with rootless section

The rootless images eliminate common permission problems by:
- Running as non-root from the start (USER 1000:1000)
- Avoiding recursive chown operations that can cause race conditions
- Using open permissions (777) on directories during build
- Not supporting PUID/PGID environment variables

This provides a cleaner solution for rootless Docker users and those
experiencing permission issues with volumes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 20:07:42 +09:00
5 changed files with 512 additions and 0 deletions

136
README-ROOTLESS.md Normal file
View 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

View File

@ -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 ...` 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 ## Troubleshooting
### My server is listed in the server browser, but nobody can connect ### My server is listed in the server browser, but nobody can connect

128
build-rootless.py Executable file
View 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)

View 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"]

View 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[@]}" "$@"