Factorio Field guide

Factorio in Docker: keep worlds, mods, and settings when replacing a container

Keep Factorio saves, mods and settings across Docker container recreation and verify a data backup.

Factorio in Docker: keep worlds, mods, and settings when replacing a container

Treat the Factorio container as replaceable and give its data one explicit, durable location. Before an update, you should be able to identify the selected save, the complete mod directory, the configuration, and the image that last loaded them successfully.

This guide uses a Linux x86-64 Docker host with Compose and the community-maintained factoriotools/factorio image. It is not an official Wube image. Other images can use different paths, user IDs, and startup rules. The example imports an existing factory; it does not install Docker or create a new map.

Replacing a container preserves the world only when storage is mounted correctly; keep a separate backup.

Record the image and the data location together#

A running container is not the identity of a factory. Record the Compose project, image reference, host data path, save filename, and enabled mod versions before changing anything. For an existing container, inspect its mounts rather than guessing from the Compose file currently on disk:

bash
docker inspect factorio-factory --format '{{json .Mounts}}'

Replace factorio-factory with the actual container name. A bind mount should show the expected host source and destination /factorio. If the previous deployment used a named volume, preserve that exact volume name. Switching to a fresh bind directory does not transfer its contents.

Docker separates a container's writable layer from persistent mounts. A digest identifies image content more precisely than a moving tag. Choose an owner-reviewed image release compatible with the save and clients; review its Dockerfile and entrypoint. Pin that tag, preferably its resolved digest. Neither latest nor stable is a permanent version selection.

Prepare one dedicated data directory#

For this image, persist the whole /factorio directory. Its layout includes saves, mods, and config; additional state such as scenarios and script output can matter to your deployment. Copying only a save archive leaves you without the surrounding server and mod configuration.

The image's maintainer documentation describes its layout. Use a dedicated directory such as /srv/factorio-factory, never a shared parent containing other applications. The regular image's documented default account uses UID/GID 845; rootless variants differ. Check the selected image before assigning ownership, and grant write access only to this server's data.

Stop the original writer cleanly and copy its retained data into this new directory, preserving the original as a rollback source. Put the intended archive at /srv/factorio-factory/saves/factory.zip, retain the matching mod files and mod settings, and place server configuration under config. Protect configuration and backups: they can contain credentials.

Before first launch, verify that you copied the intended archive:

bash
test -s /srv/factorio-factory/saves/factory.zip

A successful result only confirms a nonempty file; compare it with the original and keep that original. The current entrypoint can generate a new map when no saves exist, even with GENERATE_NEW_SAVE=false. An empty or mistyped mount must be investigated before startup.

Make the mount and save explicit in Compose#

Set FACTORIO_IMAGE in the project's .env file to the reviewed image reference, for example a compatible version tag or factoriotools/factorio@sha256:… with the actual complete digest. The placeholder digest is not a runnable value.

Also set FACTORIO_DLC_SPACE_AGE explicitly in .env to match the original save and copied mods/mod-list.json: false for no expansion mods, true for all three, or a space-separated selection such as quality elevated-rails for that existing combination. Check the selected release's DLC wrapper: the image defaults to true and can change enabled DLC entries at startup. Leaving it unset does not preserve the old choice. Keep a copy of the original list and ensure each intended expansion mod is represented correctly. Then save this as compose.yaml:

yaml
services:
  factorio:
    image: "${FACTORIO_IMAGE:?Set a reviewed Factorio image reference}"
    restart: unless-stopped
    stop_grace_period: 2m
    ports:
      - "34197:34197/udp"
    environment:
      LOAD_LATEST_SAVE: "false"
      SAVE_NAME: "/factorio/saves/factory.zip"
      GENERATE_NEW_SAVE: "false"
      UPDATE_MODS_ON_START: "false"
      DLC_SPACE_AGE: "${FACTORIO_DLC_SPACE_AGE:?Set the original expansion mod selection}"
    volumes:
      - type: bind
        source: /srv/factorio-factory
        target: /factorio
        bind:
          create_host_path: false

The long mount syntax refuses to create a missing host directory. SAVE_NAME uses an absolute container path because this wrapper passes it to Factorio's save-loading argument. Preserve the existing base-game/Space Age choice for the selected image and mod list; do not enable DLC content as an incidental persistence change. RCON is not published here.

From the Compose directory, validate the model, start, and inspect the logs:

bash
docker compose config --quiet
docker compose up -d factorio
docker compose logs --tail=100 factorio

Check the actual image, mount, loaded archive, and mod set. Compare the enabled expansion entries with the retained original list. If the selection changed unexpectedly, stop this copy, correct the explicit DLC setting, and prepare another copy from the retained original before retrying. Join with a matching client and confirm the expected factory and player state. Publishing UDP in Compose does not configure the host or provider firewall, and a running container does not establish a successful join.

Verify recreation before trusting an update#

Make a recognizable, harmless change in the game and let a save complete. Stop the service gracefully:

bash
docker compose stop -t 120 factorio

Review the final logs. Docker can force-kill a process after its stop timeout, so elapsed time alone does not confirm a completed save. Once stopped, archive the entire dedicated data directory outside itself, together with Compose and the exact image reference. Keep an off-host copy.

Recreate the container with the same pinned image and same mount, then verify that the saved change survived. Only after that should you test a new image against a separate copy of the data. Never run two Factorio writers against the same writable data directory.

Recover the data and executable as a pair#

If an update fails, keep the failed data for diagnosis. Restore the pre-update backup into another dedicated directory and select the old image reference. A new executable may already have converted a save; changing the image alone does not reverse that conversion. Verify the restored copy by loading, joining, saving, and restarting before allowing normal play.

For permission failures, inspect the selected image's account and the exact mount source; do not apply chmod 777. For an unexpected new factory, check mount identity and selected archive before replacing files. Named volumes also persist independently of a container, but docker compose down -v can remove declared volumes. Container persistence is not a backup policy.

If you are choosing a central interface for your own infrastructure, Nexardo's product page describes that approach. Keep the image, data ownership, and restore procedure documented for whichever deployment you use.

Sources#