foundryvtt-docker: a container image that installs and licenses Foundry
An easy-to-deploy containerized Foundry Virtual Tabletop server.
At a glance
- What is it?
- felddy/foundryvtt-docker packages Foundry Virtual Tabletop as an OCI image that downloads, installs and licenses the server at startup, at the cost of binding the license to a container hostname and regenerating configuration on every boot.
- Who is it for?
- This image is a good fit for a self hosted Foundry server where an operator already owns a FoundryVTT.com licence and wants the install, the update cycle and the configuration surface reduced to environment variables and a data volume.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Shell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The image installs and licenses Foundry for you at startup
Foundry Virtual Tabletop is licensed software, and installing it is the part of running a server that most people want to avoid repeating on every host and every update. foundryvtt-docker takes that work into the image. You give it credentials or a download URL, and the container installs and licenses the server on your behalf.
The project describes itself as an easy to deploy containerized Foundry Virtual Tabletop server, and the README states plainly that the image is designed to be secure, reliable, compact and simple to use. The scope is deliberate: the container covers the install, the license binding and the configuration plumbing, and leaves the rest of your infrastructure to you. There are deployment guides for Kubernetes, Podman, Docker Compose, and reverse proxies with Caddy, and the image itself is the same on each of them.
Two prerequisites set the boundary for who this is for. You need an OCI compatible runtime, whether that is Kubernetes, Podman or Docker, and you need a FoundryVTT.com account with a purchased software license. That is the point of the project, and it also means the image is not a trial of the software. The image is the distribution mechanism for people who already pay for the licence.
Configuration is regenerated on every start unless you preserve it
This is the design decision most likely to surprise someone running Foundry elsewhere. Configuration options are supplied through environment variables, and each time the container starts it generates the configuration files Foundry needs from the values of those variables. The consequence, stated in the README, is that changes made in the in-application configuration GUI do not persist between container restarts.
The guidance that follows is therefore not a preference but a requirement: manage configuration through the runtime's environment settings, whether that is a compose.yaml file, a Kubernetes manifest or something similar. If you need the generated files to survive edits made in the GUI, you set CONTAINER_PRESERVE_CONFIG to true, which disables the regeneration step.
The trade-off is worth naming. The default gives you a declarative, reproducible server whose configuration can live in version control next to the rest of your deployment, and gives up the convenience of clicking through a settings screen. Preserving the files gives you the convenience and gives up the guarantee that the running server matches the environment you declared. You pick one, and the container will not quietly do both.
The hostname becomes part of the license contract
The README marks this as important, and it deserves its own attention. Foundry binds its software license to the container hostname. If no hostname is set, the runtime assigns a random container ID on each start, and license verification then fails after every restart.
That failure mode is nasty in practice, because it appears on the second boot rather than the first. A container that starts cleanly, serves a game, and then refuses to start again after a daemon restart or a node drain is an incident, not a bug report. The fix is cheap, but it has to be applied in three different places depending on how you launch things:
docker run \
--env FOUNDRY_USERNAME='<your_username>' \
--env FOUNDRY_PASSWORD='<your_password>' \
--hostname my_foundry_host \
--publish 30000:30000/tcp \
--volume <your_data_dir>:/data \
ghcr.io/felddy/foundryvtt:14The hostname key in a compose.yaml file, the hostname flag for docker and podman, and the hostname field in a pod spec are the three forms the README names. Pick one name and keep it stable for the lifetime of the licence.
The first run is one command and one published port
The quick start in the README is the shortest useful description of the whole workflow. You pass credentials as environment variables, set a hostname, publish a single port, mount a data directory, and point at the image. The container then installs and licenses the server, and you open http://localhost:30000 in a browser to reach it.
Two details in that command are doing more work than they look. The published port is 30000 on the container side, mapped to the same port on the host, and it is the only port you need to forward for a single instance. The volume mount to /data is where the server keeps its data, and because the licence and the world data live there, that mount is the thing your backup policy has to cover.
The image reference in the command uses the 14 tag, which tracks the current major release line. The repository also ships a release tag, and the compose.yaml file in the repository uses that form instead:
foundry:
image: ghcr.io/felddy/foundryvtt:release
hostname: my_foundry_host
volumes:
- type: bind
source: ./data
target: /dataFor a production deployment, prefer a pinned version tag over a floating one, so that a restart cannot silently change the server version underneath your players.
compose.yaml is the reference for the environment variable surface
Because configuration is environment driven, the list of variables is effectively the API of the image. The compose.yaml file in the repository shows the shape of that surface, with a handful of variables set and the rest present as commented lines so the options are visible without being active:
environment:
- FOUNDRY_PASSWORD=<your_password>
- FOUNDRY_USERNAME=<your_username>
- FOUNDRY_ADMIN_KEY=atropos
- FOUNDRY_TELEMETRY=trueFurther down the same file the commented block lists further switches, including CONTAINER_PRESERVE_CONFIG, CONTAINER_CACHE, CONTAINER_PATCHES, CONTAINER_PATCH_URLS, CONTAINER_UMASK, CONTAINER_URL_FETCH_RETRY and CONTAINER_VERBOSE, along with a set of FOUNDRY prefixed options for the server itself, such as FOUNDRY_COMPRESS_WEBSOCKET, FOUNDRY_CSS_THEME, FOUNDRY_DELETE_NEDB, FOUNDRY_DEMO_CONFIG, FOUNDRY_HOSTNAME, FOUNDRY_HOT_RELOAD, FOUNDRY_LANGUAGE, FOUNDRY_LICENSE_KEY, FOUNDRY_NO_BACKUPS, FOUNDRY_PROTOCOL, FOUNDRY_PROXY_PORT, FOUNDRY_PROXY_SSL, FOUNDRY_RELEASE_URL, FOUNDRY_ROUTE_PREFIX, FOUNDRY_SSL_CERT, FOUNDRY_SSL_KEY, FOUNDRY_TEMP_DIR and FOUNDRY_UNIX_SOCKET.
There is a second channel as well. Sensitive values, meaning your credentials, the admin key or the licence key, can be supplied through a secret file instead of environment variables. The file may have any name, but it must be presented to the container as config.json. For a cluster, that is a Kubernetes secret mounted at the right path, and for a single host it is a file on disk with tighter permissions than a compose file.
The image is built from a pinned node and Debian base
The Dockerfile in the repository shows a multi stage build whose base coordinates are controlled by build arguments, and the comments explain why the ordering matters:
ARG NODE_MAJOR_VERSION=24
ARG DEBIAN_SUITE=trixie
ARG NODE_IMAGE_VERSION=${NODE_MAJOR_VERSION}-${DEBIAN_SUITE}-slim
ARG NPM_VERSION=11.12.1
ARG FOUNDRYVTT_CLI_VERSION=3.0.5The stage named archive-state is the interesting part. It exists to track the state of the Debian archives that carry security fixes, using a scratch stage that fetches the InRelease files for the security and updates distributions. The stated reason is that BuildKit revalidates remote ADD sources on every build and derives its cache key from the fetched content, so the downstream stage is invalidated exactly when those archives are republished. Without that stage, the layer applying security updates would be cached indefinitely and would silently stop applying them. The upstream node image is built from the main archive only, so fixes published to the security suite are absent until a Debian point release folds them into main.
Around the image sit the usual signs of a carefully run project: a Build workflow badge, CodeQL analysis, an OpenSSF Scorecard badge, a CII Best Practices badge, and an SLSA 3 build level badge. The tools inside the image come from a small Node package that the package.json calls foundryvtt-utils, whose main entry point is authenticate.js and whose dependencies include docopt, node-fetch, fetch-cookie, tough-cookie-file-store, proxy-agent, cheerio and winston. That is the code that performs the authenticated download of the distribution, which is why the credentials are needed at all.
Where the container is the wrong shape of solution
Three cases stand out.
First, evaluation. The image installs a licensed distribution, so without a purchased FoundryVTT.com licence and account there is nothing to install and the container has no purpose. If you want to look at the software first, the container cannot help you, and the licence purchase is the real dependency rather than the image.
Second, the configuration model. If your operators are used to configuring Foundry in the application itself and expect those settings to persist, this image works against them by default. You can preserve the files, but then the environment driven promise weakens, and you are effectively maintaining the configuration in two places.
Third, the licence binding to the hostname makes the image a poor fit for platforms that generate ephemeral hostnames, such as a serverless or autoscaling deployment that spins up a pod with a new name each time. A long lived server with a stable name is the case the design assumes.
The alternative worth weighing is building the image yourself from the Dockerfile rather than pulling a published tag. You can then set the build arguments for the container version, the Foundry version and the release URL, and bake in a base image you trust, at the cost of maintaining your own build and update cycle. Between the two, the published image gives you a maintained update path tied to Foundry releases, and self building gives you control over the base and the ability to patch the Debian archive handling yourself.
Editorial conclusion
This image is a good fit for a self hosted Foundry server where an operator already owns a FoundryVTT.com licence and wants the install, the update cycle and the configuration surface reduced to environment variables and a data volume. It is the wrong tool for evaluating Foundry without a purchased licence, for a deployment that cannot promise a stable hostname, or for a team that intends to change settings in the in-application configuration GUI and expect them to survive a restart. Before you run it, set the hostname explicitly, decide whether configuration is generated or preserved with CONTAINER_PRESERVE_CONFIG, and pin the image tag you deploy rather than tracking the floating 14 tag.
Frequently asked questions
How do I run Foundry VTT with foundryvtt-docker?
The README gives a single docker run command that passes FOUNDRY_USERNAME and FOUNDRY_PASSWORD, sets the hostname, publishes port 30000, mounts a data directory to /data and starts the image ghcr.io/felddy/foundryvtt:14. You then open http://localhost:30000.
Why does my Foundry container fail to start after a restart?
Foundry binds its software license to the container hostname, and a runtime without an explicit hostname assigns a random container ID on each start, which makes license verification fail. Set the hostname explicitly, with the hostname key in compose.yaml, the --hostname flag, or the hostname field in a pod spec.
How do I keep changes made in the Foundry configuration GUI?
By default the container regenerates the configuration files on every start from the environment variables, so GUI changes do not persist between restarts. Set CONTAINER_PRESERVE_CONFIG to true to disable that regeneration.
Can I avoid putting my Foundry password in the container?
Yes. You can acquire a temporary download URL from the Purchased Software Licenses page and pass it as FOUNDRY_RELEASE_URL instead of the username and password. Sensitive values can also be supplied through a secret file, which may have any name as long as it is presented to the container as config.json.
Which runtimes does foundryvtt-docker support?
The README lists Kubernetes, Podman and Docker as the OCI compatible runtimes, and the deployment guides cover each one in depth, including a Docker Compose setup and reverse proxy configurations with Caddy. The image is the same on every runtime.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/felddy-foundryvtt-docker)Was this useful?