Factorio Admin RCON Source code

Installation

Getting the panel into production with Docker Compose, in a handful of commands.

Requirements#

Quick start, without cloning#

The images are published, so the repository is optional: one directory, the docker-compose.yml below pasted as it stands, and a generated .env next to it. Both the game server and the panel come up with docker compose up.

services:
  factorio:
    container_name: factorio-server
    image: factoriotools/factorio:stable
    restart: unless-stopped
    ports:
      # Game port only. RCON stays unpublished: the panel reaches it over the
      # compose network, and the port hands full server control to whoever
      # holds the password.
      - "34197:34197/udp"
    volumes:
      - ./data:/factorio
    environment:
      - UPDATE_MODS_ON_START=true

  factorio-admin:
    container_name: factorio-admin-panel
    # A minor tag: patches arrive, a major never lands on you by surprise.
    # In production, pin the exact version instead.
    image: williamnauroy/factorio-admin-rcon:1.4-distroless
    restart: unless-stopped
    depends_on:
      - factorio
    ports:
      # Loopback only: the panel grants full RCON access.
      - "127.0.0.1:3010:3000"
      - "[::1]:3010:3000"
    volumes:
      # The directory, not the file: a regenerated rconpw is picked up as is.
      - ./data/config:/factorio-config:ro
      # Sessions, audit log and metric series (SQLite).
      - factorio-admin-data:/data
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=16m
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    mem_limit: 256m
    pids_limit: 128
    environment:
      - RCON_HOST=factorio
      - RCON_PORT=27015
      - RCON_PASSWORD_FILE=/factorio-config/rconpw
      - ADMIN_PASSWORD=${ADMIN_PASSWORD:?set it in .env}
      - SESSION_SECRET=${SESSION_SECRET:?set it in .env, 32 chars minimum}
      # No docker-socket-proxy here, so the CPU and memory graphs are off.
      # Players and UPS keep working.
      - METRICS_DOCKER=false

volumes:
  factorio-admin-data:

The secrets are generated rather than typed — that is all setup-admin.sh does in the repository:

cat > .env <<EOF
ADMIN_PASSWORD=$(openssl rand -base64 18)
SESSION_SECRET=$(openssl rand -hex 32)
EOF

docker compose up -d
cat .env            # the password to log in with
                    # → http://127.0.0.1:3010

MODERATOR_PASSWORD and VIEWER_PASSWORD are optional: add them to .env and to the service's environment: to open the two read-only roles. A role without a password has no account at all.

The first start reports RCON as unavailable The game server is still creating its map, and data/config/rconpw does not exist yet. The panel re-reads that file on every connection attempt, so it recovers on its own after a few seconds — no restart needed.

Updating is a pull, since nothing is built locally: bump the tag, then docker compose pull && docker compose up -d.

Production setup#

setup-admin.sh writes a .env file next to the docker-compose.yml: one password per role and the session signing key.

./setup-admin.sh                # ADMIN_PASSWORD + SESSION_SECRET
./setup-admin.sh --all          # all three roles
./setup-admin.sh --force --yes  # full rotation, no questions asked

Then:

docker compose up -d --build factorio-admin
SESSION_SECRET is required Without it, Compose refuses to start the service. That is deliberate: the cookie signing key must not be derived from the passwords, otherwise changing one password signs everybody out. setup-admin.sh generates it.

What gets mounted#

MountPurpose
./data/config:/factorio-config:roThe RCON password (rconpw, regenerated by the game server) and, if you have one, your command catalogue. The directory is mounted rather than the file, so a recreated file is picked up without a restart.
factorio-admin-data:/dataNamed volume: sessions, audit log and metric series (SQLite). It is the only place the panel writes to.

The container runs with a read-only root filesystem, cap_drop: [ALL], no-new-privileges, and memory and PID caps. /tmp is a tmpfs, purely as insurance.

Hardened image (optional)#

Dockerfile.distroless produces a variant with no shell and no package manager, published under -distroless tags. It is the variant the docker-compose.yml uses by default. It shrinks the attack surface at the cost of less convenient debugging: no docker exec … sh.

Behind a reverse proxy#

The panel only listens on the loopback interface. To expose it, put a TLS reverse proxy in front and enable TRUST_PROXY:

TRUST_PROXY=true
Only enable TRUST_PROXY behind a proxy you control It makes the panel trust X-Forwarded-For for per-IP rate limiting. Exposed directly, anyone forges that header and resets their own counter on every attempt. While it is false the header is ignored and rate limiting falls back to a global bucket — coarser, but unforgeable.

The session cookie turns secure on its own as soon as the request arrives over HTTPS (COOKIE_SECURE=auto, the default).

Health probes#

RouteAnswersUse it for
/api/health200 as long as the process is aliveDocker's liveness probe. It does not depend on Factorio.
/api/ready200 when configuration, command catalogue, database and RCON all answer; 503 otherwiseThe readiness probe. Kept separate from the above so a Factorio outage does not restart the panel in a loop.

Updating#

git pull
docker compose up -d --build factorio-admin

Published images follow the versions semantic-release cuts (williamnauroy/factorio-admin-rcon). The database schema is upgraded at startup: columns added after the fact are caught up by an idempotent ALTER TABLE, so there is nothing to do by hand.