| Filename | Latest commit message | Latest commit date |
|---|---|---|
| campaigns | ||
| sftp | ||
| shared | ||
| README.md | ||
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:
- Taking the campaign (
aoraki/curse_of_stradh) as a flow input. - Writing/patching that campaign's
.env(e.g. bumpingFOUNDRY_VERSION). - Running
docker compose up -d --pull always --force-recreateinsidecampaigns/<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.