Skip to content

Commit e8adbf5

Browse files
fankclaude
andauthored
feat: Add rootless Docker support (#574)
* 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 <[email protected]> * fix: Address linting issues in rootless Docker implementation - Add --no-install-recommends to apt-get install in Dockerfile - Consolidate consecutive RUN instructions in Dockerfile - Fix shellcheck warnings: quote variables and use -n instead of \! -z - These changes improve best practices without affecting functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: Add rootless image building to CI pipeline - Update docker-build.yml workflow to build rootless variants - Rootless images are built after regular images with -rootless suffix - Both use the same multi-architecture build process - Triggered automatically when buildinfo.json changes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: Unify build system for regular and rootless images - Create build-unified.py that handles both regular and rootless builds - Convert build.py and build-rootless.py to wrapper scripts for backwards compatibility - Update CI workflow to use unified build command - Add BUILD_MIGRATION.md documentation - Eliminate code duplication between build scripts - Support flexible build options: --rootless, --both, --only-stable-latest This maintains all existing functionality while providing a cleaner, more maintainable build system. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * chore: Add Python cache to .gitignore and remove from repo - Add __pycache__/ and Python compiled files to .gitignore - Remove accidentally committed __pycache__ directory - Prevent future Python cache files from being tracked 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: Replace build system with unified solution - Remove old build.py and build-rootless.py wrapper scripts - Rename build-unified.py to build.py as the main build script - Delete BUILD_MIGRATION.md (no longer needed) - Update CI workflow to use new build.py syntax - Update documentation in CLAUDE.md and README-ROOTLESS.md The new build system provides all functionality in a single script: - Default: builds regular images - --rootless: builds only rootless images - --both: builds both regular and rootless images - --multiarch and --push-tags: work as before This creates a cleaner, more maintainable build system. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * docs: Consolidate rootless documentation and mark as experimental - Remove separate README-ROOTLESS.md file - Integrate rootless documentation into main README.md - Mark rootless support as experimental - Add clear documentation about limitations and use cases - Include warning about experimental nature This consolidates all documentation in one place and makes it clear that rootless support is still experimental. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 5337894 commit e8adbf5

File tree

7 files changed

+362
-29
lines changed

7 files changed

+362
-29
lines changed

.github/workflows/docker-build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ jobs:
2020
- name: Set up QEMU
2121
uses: docker/setup-qemu-action@v3
2222

23-
- name: build and push
23+
- name: build and push all images
2424
if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }}
2525
env:
2626
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
2727
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
2828
run: |
29-
./build.py --push-tags --multiarch
29+
./build.py --push-tags --multiarch --both

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
# IDE
22
.idea
3+
4+
# Python
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class

CLAUDE.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ This is a Docker image for running a Factorio headless server. It provides autom
1111
### Key Components
1212

1313
1. **Docker Image Build System**
14-
- `build.py` - Python script that builds Docker images from `buildinfo.json`
14+
- `build.py` - Unified Python script that builds both regular and rootless Docker images from `buildinfo.json`
1515
- `docker/Dockerfile` - Main Dockerfile that creates the Factorio server image
16+
- `docker/Dockerfile.rootless` - Dockerfile for rootless variant (runs as UID 1000)
1617
- `buildinfo.json` - Contains version info, SHA256 checksums, and tags for all supported versions
1718
- Supports multi-architecture builds (linux/amd64, linux/arm64) using Docker buildx
1819

@@ -38,11 +39,20 @@ This is a Docker image for running a Factorio headless server. It provides autom
3839
### Building Images
3940

4041
```bash
41-
# Build a single architecture image locally
42+
# Build regular images locally (single architecture)
4243
python3 build.py
4344

44-
# Build and push multi-architecture images
45+
# Build rootless images only
46+
python3 build.py --rootless
47+
48+
# Build both regular and rootless images
49+
python3 build.py --both
50+
51+
# Build and push multi-architecture images (regular only)
4552
python3 build.py --multiarch --push-tags
53+
54+
# Build and push both regular and rootless multi-architecture images
55+
python3 build.py --multiarch --push-tags --both
4656
```
4757

4858
### Running the Container
@@ -109,6 +119,8 @@ Version updates are automated via GitHub Actions that run `update.sh` periodical
109119
## Testing Changes
110120

111121
1. Modify `buildinfo.json` to test specific versions
112-
2. Run `python3 build.py` to build locally
122+
2. Run `python3 build.py` to build regular images locally
123+
- Use `python3 build.py --rootless` for rootless images
124+
- Use `python3 build.py --both` to build both variants
113125
3. Test the container with your local data volume
114126
4. For production changes, ensure `update.sh` handles version transitions correctly

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,59 @@ stream {
450450

451451
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 ...`
452452

453+
## Rootless Docker Support (Experimental)
454+
455+
> **Note**: Rootless support is currently experimental. Please report any issues you encounter.
456+
457+
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.
458+
459+
### What are Rootless Images?
460+
461+
The rootless images differ from regular images in several ways:
462+
- Run as UID 1000 (non-root) by default
463+
- No dynamic UID/GID mapping (PUID/PGID not supported)
464+
- No runtime chown operations
465+
- All directories created with open permissions during build
466+
467+
### Rootless Image Tags
468+
469+
Each regular tag has a corresponding rootless version with the `-rootless` suffix:
470+
- `latest-rootless` (experimental)
471+
- `stable-rootless` (experimental)
472+
- `2.0.55-rootless` (experimental)
473+
474+
### Quick Start with Rootless
475+
476+
```shell
477+
docker run -d \
478+
-p 34197:34197/udp \
479+
-p 27015:27015/tcp \
480+
-v ~/factorio:/factorio \
481+
--name factorio \
482+
--restart=unless-stopped \
483+
factoriotools/factorio:stable-rootless
484+
```
485+
486+
Key differences:
487+
- No `chown` command needed
488+
- No PUID/PGID environment variables
489+
- Runs as UID 1000 by default
490+
- No permission issues with volumes
491+
492+
### When to Use Rootless Images
493+
494+
Consider using rootless images if you:
495+
- Are running Docker in rootless mode
496+
- Experience permission issues with volume mounts
497+
- Want to avoid containers running as root
498+
- Don't need dynamic UID/GID mapping via PUID/PGID
499+
500+
### Limitations
501+
502+
- PUID/PGID environment variables are not supported
503+
- Fixed to UID 1000 (may not match your host user)
504+
- Experimental feature - may have undiscovered issues
505+
453506
## Troubleshooting
454507

455508
### My server is listed in the server browser, but nobody can connect

build.py

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import shutil
77
import sys
88
import tempfile
9+
import argparse
910

1011

1112
PLATFORMS = [
@@ -25,26 +26,26 @@ def create_builder(build_dir, builder_name, platform):
2526
exit(1)
2627

2728

28-
def build_and_push_multiarch(build_dir, build_args, push):
29-
builder_name = "factoriotools-multiarch"
30-
platform=",".join(PLATFORMS)
29+
def build_and_push_multiarch(build_dir, build_args, push, builder_suffix=""):
30+
builder_name = f"factoriotools{builder_suffix}-multiarch"
31+
platform = ",".join(PLATFORMS)
3132
create_builder(build_dir, builder_name, platform)
3233
build_command = ["docker", "buildx", "build", "--platform", platform, "--builder", builder_name] + build_args
3334
if push:
3435
build_command.append("--push")
3536
try:
3637
subprocess.run(build_command, cwd=build_dir, check=True)
3738
except subprocess.CalledProcessError:
38-
print("Build and push of image failed")
39+
print(f"Build and push of {builder_suffix or 'regular'} image failed")
3940
exit(1)
4041

4142

42-
def build_singlearch(build_dir, build_args):
43+
def build_singlearch(build_dir, build_args, image_type="regular"):
4344
build_command = ["docker", "build"] + build_args
4445
try:
4546
subprocess.run(build_command, cwd=build_dir, check=True)
4647
except subprocess.CalledProcessError:
47-
print("Build of image failed")
48+
print(f"Build of {image_type} image failed")
4849
exit(1)
4950

5051

@@ -58,16 +59,19 @@ def push_singlearch(tags):
5859
exit(1)
5960

6061

61-
def build_and_push(sha256, version, tags, push, multiarch):
62+
def build_and_push(sha256, version, tags, push, multiarch, dockerfile="Dockerfile", builder_suffix=""):
6263
build_dir = tempfile.mktemp()
6364
shutil.copytree("docker", build_dir)
64-
build_args = ["--build-arg", f"VERSION={version}", "--build-arg", f"SHA256={sha256}", "."]
65+
build_args = ["-f", dockerfile, "--build-arg", f"VERSION={version}", "--build-arg", f"SHA256={sha256}", "."]
6566
for tag in tags:
6667
build_args.extend(["-t", f"factoriotools/factorio:{tag}"])
68+
69+
image_type = "rootless" if "rootless" in dockerfile.lower() else "regular"
70+
6771
if multiarch:
68-
build_and_push_multiarch(build_dir, build_args, push)
72+
build_and_push_multiarch(build_dir, build_args, push, builder_suffix)
6973
else:
70-
build_singlearch(build_dir, build_args)
74+
build_singlearch(build_dir, build_args, image_type)
7175
if push:
7276
push_singlearch(tags)
7377

@@ -85,25 +89,69 @@ def login():
8589
exit(1)
8690

8791

88-
def main(push_tags=False, multiarch=False):
92+
def generate_rootless_tags(original_tags):
93+
"""Generate rootless-specific tags from original tags"""
94+
return [f"{tag}-rootless" for tag in original_tags]
95+
96+
97+
def main():
98+
parser = argparse.ArgumentParser(description='Build Factorio Docker images')
99+
parser.add_argument('--push-tags', action='store_true', help='Push images to Docker Hub')
100+
parser.add_argument('--multiarch', action='store_true', help='Build multi-architecture images')
101+
parser.add_argument('--rootless', action='store_true', help='Build only rootless images')
102+
parser.add_argument('--both', action='store_true', help='Build both regular and rootless images')
103+
parser.add_argument('--only-stable-latest', action='store_true',
104+
help='Build only stable and latest versions (for rootless by default)')
105+
106+
args = parser.parse_args()
107+
108+
# Default behavior: build regular images unless specified otherwise
109+
build_regular = not args.rootless or args.both
110+
build_rootless = args.rootless or args.both
111+
89112
with open(os.path.join(os.path.dirname(__file__), "buildinfo.json")) as file_handle:
90113
builddata = json.load(file_handle)
91114

92-
if push_tags:
115+
if args.push_tags:
93116
login()
94117

118+
# Filter versions if needed
119+
versions_to_build = []
95120
for version, buildinfo in sorted(builddata.items(), key=lambda item: item[0], reverse=True):
96-
sha256 = buildinfo["sha256"]
97-
tags = buildinfo["tags"]
98-
build_and_push(sha256, version, tags, push_tags, multiarch)
121+
if args.only_stable_latest or (build_rootless and not build_regular):
122+
# For rootless-only builds, default to stable/latest only
123+
if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]:
124+
versions_to_build.append((version, buildinfo))
125+
else:
126+
versions_to_build.append((version, buildinfo))
127+
128+
# Build regular images
129+
if build_regular:
130+
print("Building regular images...")
131+
for version, buildinfo in versions_to_build:
132+
sha256 = buildinfo["sha256"]
133+
tags = buildinfo["tags"]
134+
build_and_push(sha256, version, tags, args.push_tags, args.multiarch)
135+
136+
# Build rootless images
137+
if build_rootless:
138+
print("Building rootless images...")
139+
# For rootless, only build stable and latest unless building both
140+
rootless_versions = []
141+
if not build_regular or args.only_stable_latest:
142+
for version, buildinfo in builddata.items():
143+
if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]:
144+
rootless_versions.append((version, buildinfo))
145+
else:
146+
rootless_versions = versions_to_build
147+
148+
for version, buildinfo in rootless_versions:
149+
sha256 = buildinfo["sha256"]
150+
original_tags = buildinfo["tags"]
151+
rootless_tags = generate_rootless_tags(original_tags)
152+
build_and_push(sha256, version, rootless_tags, args.push_tags, args.multiarch,
153+
dockerfile="Dockerfile.rootless", builder_suffix="-rootless")
99154

100155

101156
if __name__ == '__main__':
102-
push_tags = False
103-
multiarch = False
104-
for arg in sys.argv[1:]:
105-
if arg == "--push-tags":
106-
push_tags = True
107-
elif arg == "--multiarch":
108-
multiarch = True
109-
main(push_tags, multiarch)
157+
main()

docker/Dockerfile.rootless

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# build rcon client
2+
FROM debian:stable-slim AS rcon-builder
3+
RUN apt-get -q update \
4+
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install build-essential --no-install-recommends
5+
6+
WORKDIR /src
7+
COPY rcon/ /src
8+
RUN make
9+
10+
# build factorio image
11+
FROM debian:stable-slim
12+
LABEL maintainer="https://github.com/factoriotools/factorio-docker"
13+
14+
ARG BOX64_VERSION=v0.2.4
15+
16+
# optionally utilize a built-in map-gen-preset (see data/base/prototypes/map-gen-presets
17+
ARG PRESET
18+
19+
# number of retries that curl will use when pulling the headless server tarball
20+
ARG CURL_RETRIES=8
21+
22+
ENV PORT=34197 \
23+
RCON_PORT=27015 \
24+
SAVES=/factorio/saves \
25+
PRESET="$PRESET" \
26+
CONFIG=/factorio/config \
27+
MODS=/factorio/mods \
28+
SCENARIOS=/factorio/scenarios \
29+
SCRIPTOUTPUT=/factorio/script-output \
30+
DLC_SPACE_AGE="true"
31+
32+
SHELL ["/bin/bash", "-eo", "pipefail", "-c"]
33+
34+
RUN apt-get -q update \
35+
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install ca-certificates curl jq pwgen xz-utils procps gettext-base --no-install-recommends \
36+
&& if [[ "$(uname -m)" == "aarch64" ]]; then \
37+
echo "installing ARM compatability layer" \
38+
&& DEBIAN_FRONTEND=noninteractive apt-get -qy install unzip --no-install-recommends \
39+
&& curl -LO https://github.com/ptitSeb/box64/releases/download/${BOX64_VERSION}/box64-GENERIC_ARM-RelWithDebInfo.zip \
40+
&& unzip box64-GENERIC_ARM-RelWithDebInfo.zip -d /bin \
41+
&& rm -f box64-GENERIC_ARM-RelWithDebInfo.zip \
42+
&& chmod +x /bin/box64; \
43+
fi \
44+
&& rm -rf /var/lib/apt/lists/*
45+
46+
# version checksum of the archive to download
47+
ARG VERSION
48+
ARG SHA256
49+
50+
LABEL factorio.version=${VERSION}
51+
52+
ENV VERSION=${VERSION} \
53+
SHA256=${SHA256}
54+
55+
RUN set -ox pipefail \
56+
&& if [[ "${VERSION}" == "" ]]; then \
57+
echo "build-arg VERSION is required" \
58+
&& exit 1; \
59+
fi \
60+
&& if [[ "${SHA256}" == "" ]]; then \
61+
echo "build-arg SHA256 is required" \
62+
&& exit 1; \
63+
fi \
64+
&& archive="/tmp/factorio_headless_x64_$VERSION.tar.xz" \
65+
&& mkdir -p /opt /factorio \
66+
&& curl -sSL "https://www.factorio.com/get-download/$VERSION/headless/linux64" -o "$archive" --retry $CURL_RETRIES \
67+
&& echo "$SHA256 $archive" | sha256sum -c \
68+
|| (sha256sum "$archive" && file "$archive" && exit 1) \
69+
&& tar xf "$archive" --directory /opt \
70+
&& chmod ugo=rwx /opt/factorio \
71+
&& rm "$archive" \
72+
&& ln -s "$SCENARIOS" /opt/factorio/scenarios \
73+
&& ln -s "$SAVES" /opt/factorio/saves \
74+
&& mkdir -p /opt/factorio/config/
75+
76+
COPY files/*.sh /
77+
COPY files/docker-entrypoint-rootless.sh /docker-entrypoint.sh
78+
COPY files/config.ini /opt/factorio/config/config.ini
79+
COPY --from=rcon-builder /src/rcon /bin/rcon
80+
81+
# Make all scripts executable and set proper permissions for the factorio directory
82+
RUN chmod +x /*.sh \
83+
&& chmod -R 777 /opt/factorio /factorio
84+
85+
VOLUME /factorio
86+
EXPOSE $PORT/udp $RCON_PORT/tcp
87+
88+
# Run as non-root user (UID 1000 is common for the first user in rootless containers)
89+
USER 1000:1000
90+
91+
ENTRYPOINT ["/docker-entrypoint.sh"]

0 commit comments

Comments
 (0)