{ martowen.com }

Running Foundry Virtual Table Top in a Local/Remote Docker Container

Mar 17, 2023
6 minutes

I bought the excellent web based virtual table top Foundry VTT last year in order to play the TTRPG Cyberpunk Red remotely, and also to learn how to run it.

The recommended way of making a Foundry instance available to other users is to port forward from your local machine, but there’s no way I’m exposing my personal machine in this way. Another option is running on Forge which is pretty cost-effective at $4 a month but I wanted the convenience of being able to manage my game worlds in an Azure Storage Account.

With this in mind I looked at options for running it in a Docker container so that I could put worlds together locally and then run them in the container when I wanted to share the game with others. My plan was to store all game data in an Azure File Share so that there would be a single source of the game data.

I use a few tools in this post, the main ones being Docker Compose and Terraform. The former should be pretty simple for you to pick up, but the latter will need you to do some research, but all you really need is to be able to terraform init/plan/apply.

Design

My plan was for a Windows File Share which would be used by both a local Docker Container running on my machine, and a container group running in Azure for when I was ready to play with others.

The design for this is described below:

C4Context
  title Foundry VTT Local and Remote with Shared Storage
    Boundary(users, "Users") {
      Person(player1, "Player 1")
      Person(gm, "Games Master")
    }

    Container_Boundary(local, "GM's Local Machine") {
      Container(localContainer, "Local Docker Container")
      Container(windowsFileShare, "Local File System")
    }

    Container_Boundary(azure, "Microsoft Azure") {
      Container(azureContainerGroup, "Azure Container Group")
      Container(azureFileShare, "Azure File Share")
    }

    BiRel(localContainer, windowsFileShare, "mounts")
    BiRel(windowsFileShare, azureFileShare, "manual sync")
    BiRel(azureContainerGroup, azureFileShare, "mounts")

    Rel(gm, localContainer, "browses", "http://localhost:30001")

    Rel(player1, azureContainerGroup, "browses", "https://foundry.martowen.com")
    Rel(gm, azureContainerGroup, "browses", "https://foundry.martowen.com")

    UpdateRelStyle(player1, azureContainerGroup, $offsetX="-70")
    UpdateRelStyle(gm, azureContainerGroup, $offsetX="20")
    UpdateRelStyle(azureContainerGroup, azureFileShare, $offsetX="-20")

I’m doing a manual sync between my local file system and the Azure File Share, because you can’t mount file shares in Docker containers. I may fix this using a CIFS volume in the future, but gave up for now.

A FoundryVTT Docker Image

There is a well maintained Docker image at https://hub.docker.com/r/felddy/foundryvtt that I have found to be great to work with, and it has worked for what I wanted to achieve. The image tag in the example below is release meaning the most recent stable release, so it will change over time. If you don’t want this behaviour then specify a specific version tag.

My docker-compose.yaml with the data being loaded from a local directory C:\Users\marti\AppData\Local\FoundryVTT\ (the default Foundry directory for my user):

---
version: "3.8"

secrets:
config_json:
file: secrets.json

services:
foundry:
image: felddy/foundryvtt:release
init: true
volumes:
- type: bind
source: C:\Users\marti\AppData\Local\FoundryVTT\
target: /data
ports:
- target: 30000
published: 30001
protocol: tcp
secrets:
- source: config_json
target: config.json
environment:
- CONTAINER_PRESERVE_CONFIG=true

My secrets.json:

{
"foundry_admin_key": "<redacted>",
"foundry_password": "<redacted>",
"foundry_username": "<redacted>",
"foundry_license_key": "<redacted>"
}

Put those in a directory with the appropriate secrets and run:

docker-compose up -d

And you will have a Foundry VTT instance running locally on http://localhost:30001/. This is great for taking the time to prepare your worlds offline.

Using Terraform to deploy the container

Beware: this deployment is to Microsoft Azure and will cost money. I am not responsible for costs incurred in your Azure Subscription.

Most of the data for your Foundry install resides in a storage account, so the container group should only be deployed when running a game with others, to minimise costs. Destroying resources with Terraform is easy to do, so be sure to do it when you’re finished.

The Terraform Deploy

I’ve written a separate blog post detailing the Terraform deploy, so won’t go into too much detail of it here. At a high level the Terraform deploy is split into two files:

  • The first main.tf for creating the Azure File Share for storing all our game data. This will persist in between our Foundry VTT sessions. I also create an Azure Key Vault in this step even for our Foundry license keys and passwords, though we don’t need it in between sessions, as it is not expensive and they take a relatively long time to provision.
  • The second foundry.tf for creating the actual Azure Container Group that will run the Foundry instance is the one that we will want to hide at the end of our gaming session so that it will be destroyed and save costs.

You can see the definition of these files here: https://github.com/mowen/foundryvtt-azure/tree/main/terraform

Foundry Container Definition

The main Foundry container runs the same Docker Container used locally above, specifies a few environment variables, and mounts the Azure File Share as a volume:

container {
name = "foundryvtt"
image = "felddy/foundryvtt:release"
cpu = "0.5"
memory = "1.5"

environment_variables = {
"CONTAINER_PRESERVE_CONFIG" = "true"
"FOUNDRY_PROXY_PORT" = "443"
"FOUNDRY_HOSTNAME" = local.dns_name
}

secure_environment_variables = {
"FOUNDRY_USERNAME" = var.FOUNDRY_USERNAME
"FOUNDRY_PASSWORD" = var.FOUNDRY_PASSWORD
"FOUNDRY_ADMIN_KEY" = var.FOUNDRY_ADMIN_KEY
"FOUNDRY_LICENSE_KEY" = var.FOUNDRY_LICENSE_KEY
}

volume {
name = "foundry-data-mount"
storage_account_name = azurerm_storage_account.foundry.name
storage_account_key = azurerm_storage_account.foundry.primary_access_key
share_name = azurerm_storage_share.foundry.name
mount_path = "/data"
}
}

Caddy Container Definition

The second container in the group is running Caddy as a reverse proxy and to provide HTTPS. This needs its own storage account, but that is destroyed along with the Container Group.

container {
name = "caddy"
image = "caddy"
cpu = "0.5"
memory = "0.5"

ports {
port = 443
protocol = "TCP"
}

ports {
port = 80
protocol = "TCP"
}

volume {
name = "foundry-caddy-data"
mount_path = "/data"
storage_account_name = azurerm_storage_account.caddy.name
storage_account_key = azurerm_storage_account.caddy.primary_access_key
share_name = azurerm_storage_share.caddy.name
}

commands = ["caddy", "reverse-proxy", "--from", local.dns_name, "--to", "localhost:30000"]
}

Mounting the Azure File Share on Windows

Once the Azure File Share is created it is simple to mount locally using the Connect option in the Azure Portal which generates a script: https://learn.microsoft.com/en-us/azure/storage/files/storage-how-to-use-files-windows

With this in place you can either copy and paste to that folder, or use Azure Storage Explorer to upload whole folders. I recommend Storage Explorer, or even a one line CLI call with AzCopy.

DNS

I added a CNAME record within my existing DNS host to point from a subdomain to the address of the Azure Container Group, and had to put this subdomain in the Caddy config in the Azure Container Group Terraform. It is loaded from the dns_name variable in my variables.tf.

Running in an App Service

You may wonder why I’m not using an App Service. I did spend time trying to get the container running in a Linux App Service but had issues with startup. I would like this option in the future and when Docker Compose for deployment matures I will revisit this.

Summary

I hope this gives provides some insight into hosting Foundry on Azure with Feldy’s Docker Container, as my experience of it has been pretty painless so far and I would highly recommend it.