No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-26 23:14:02 +02:00
campaigns Remove .env files 2026-08-26 23:11:38 +02:00
sftp First commit 2026-08-26 22:40:19 +02:00
shared Ignore zip files in container cache 2026-08-26 23:14:02 +02:00
README.md First commit 2026-08-26 22:40:19 +02:00

Foundry VTT — Two Campaign Instances

Two independent felddy/foundryvtt-docker instances, one per D&D campaign, each running as its own Docker Compose project so it can be started, stopped, upgraded, and restarted independently.

Layout

campaigns/
  aoraki/
    docker-compose.yml   # single "foundry" service
    .env.example          # per-instance template — copy to .env
    .env                  # per-instance overrides, gitignored
    data/                  # per-instance persistent volume (worlds, config), gitignored
  curse_of_stradh/
    ... same shape
shared/
  foundry.env.example     # common config template — copy to foundry.env
  foundry.env              # common config (admin key, cache, etc.), gitignored
  container_cache/        # shared cache of downloaded Foundry install packages, gitignored
sftp/
  docker-compose.yml       # single "sftp" service, one login for both campaigns
  .env.example              # template — copy to .env
  .env                       # real credentials, gitignored

Both instances mount shared/container_cache at /data/container_cache, so a Foundry version only has to be downloaded once and both campaigns reuse it, and both load shared/foundry.env for settings the two campaigns have in common (admin key, cache config, telemetry/performance flags, timezone).

Only three things vary per instance, and they live in each campaign's own .env: FOUNDRY_WORLD, the container hostname/container_name (set directly in that campaign's docker-compose.yml), and FOUNDRY_VERSION / FOUNDRY_PORT — kept per-instance on purpose so each campaign can be upgraded or restarted independently (see the Kestra section below).

Directory names (aoraki, curse_of_stradh) are arbitrary — nothing depends on them beyond the paths used in sftp/docker-compose.yml's volumes, since each stack is addressed by its own path/project name.

First-time setup

No Foundry account credentials are configured — the containers never try to download software themselves. Instead, populate the shared cache once with the install package(s) you download manually from your Foundry account's "Download Software" page:

# Foundry's admin page gives you a download URL; the resulting file must be
# renamed to "foundryvtt-<version>.zip", matching FOUNDRY_VERSION exactly, e.g.:
mv ~/Downloads/FoundryVTT-Node-13.348.zip shared/container_cache/foundryvtt-13.348.zip

Set up the shared config once:

cd shared
cp foundry.env.example foundry.env
# edit foundry.env: FOUNDRY_ADMIN_KEY at minimum

Then, per campaign:

cd campaigns/aoraki
cp .env.example .env
# edit .env: FOUNDRY_WORLD, FOUNDRY_VERSION (must match a cached package), FOUNDRY_PORT
docker compose up -d

Repeat for curse_of_stradh (it defaults to a different host port, 30000, so both can run at once on the same host). Both instances read from the same shared/container_cache and shared/foundry.env.

Finally, set up asset upload access:

cd sftp
cp .env.example .env
# edit .env: SFTP_PASSWORD at minimum
docker compose up -d

Log in with sftp -P 2223 ben@<host> and the two campaigns show up as aoraki/ and curse_of_stradh/, each backed by that instance's Foundry data/Data folder (worlds, systems, modules, assets) — Config and Logs aren't exposed over SFTP. If uploaded files come out unreadable by Foundry (or vice versa), align SFTP_UID in sftp/.env with the UID that owns the data/Data directories on the host.

Operating an instance

Run these from inside the campaign's directory (or add -f/--project-directory to target it from elsewhere):

docker compose up -d              # start
docker compose stop               # stop, keep container
docker compose down               # remove container (data/ volume persists)
docker compose logs -f            # tail logs

Changing the image version

The running version is controlled by FOUNDRY_VERSION in that campaign's .env (e.g. release, 13, or a pinned version like 12.331.0). To upgrade:

sed -i 's/^FOUNDRY_VERSION=.*/FOUNDRY_VERSION=13/' .env
docker compose up -d --pull always --force-recreate

Kestra integration (planned)

Each campaign is a self-contained Compose project, so a Kestra flow can target one instance at a time by:

  1. Taking the campaign (aoraki / curse_of_stradh) as a flow input.
  2. Writing/patching that campaign's .env (e.g. bumping FOUNDRY_VERSION).
  3. Running docker compose up -d --pull always --force-recreate inside campaigns/<campaign>/ via Kestra's Docker or Shell/Script task.

This keeps the two instances fully isolated — a flow run against one never touches the other's container, port, or data volume.