├── foundry-docker-swag.png ├── LICENSE └── README.md /foundry-docker-swag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChefsSlaad/foundry_swag_docker/HEAD/foundry-docker-swag.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foundry Swag Docker 2 | 3 | 4 | ![foundry_swag_docker](foundry-docker-swag.png) 5 | 6 | version 0.08 - Archon 7 | 8 | This is a how-to on running [foundry-vtt](https://foundryvtt.com/) on your home server 24/7 in a docker container and securing the connection using nginx and letsencrypt. If that does not mean anything to you, this is basically a how-to on running a reasonably secure version of foundry. It is: 9 | - **containered** - even if someone is able to hijack your foundry system through a vulnerability or by guessing your password, they cannot go any further and damage your system or network. They're basically stuck in your container. We will use [Docker](https://www.docker.com/resources/what-container) to achieve this. 10 | - **encrypted** - the connection between your player's PC and your server is encrypted, which means that other users cannot easily steal your password or hijack your connection. We will use [Secure Web Application Gateway aka SWAG](https://hub.docker.com/r/linuxserver/swag) to achieve this. 11 | 12 | # Disclaimer 13 | * This guide will make you more secure hosting foundry than doing nothing. However, there is no such thing as an unhackable system. So please **choose a strong password** and **update your system**. Also, don't go around daring other people to hack you. That's just stupid. 14 | * This guide is geared towards users who want to host foundry 24/7. That assumes you have a home server or some other dedicated hardware (even a raspberry pi) that player and the GM can always access from the internet. If you are only running foundry when you have a game, or if you only need your players to access the game from your LAN, this may be overkill. Then again, you're not paranoid if they're *really* out to get you. 15 | * This guide is written by me, based on my own experience self-hosting foundryVTT. There are bound to be mistakes in this guide. Please contact me if I missed anything or if you feel this guide could be improved. Or, you know, make a pull request. This is Github after all. 16 | * I'm assuming you have a passing familiarity with Linux, the terminal, and how a web server works in general. I may forego or adjust these assumptions in the future, but right now it is what it is. 17 | 18 | # Overview 19 | This guide is set up in 4 steps: 20 | * [Preparations](#preparations) 21 | * [Setting up the host](#setting-up-the-host) (the system or server you will be using to run foundry vtt) 22 | * [Setting up the containers](#containers) 23 | * [Wrapping up](#wrapping-up) 24 | 25 | 26 | # Preparations 27 | The preparations are about making sure you have everything ready to install and run foundry. 28 | 29 | ## Hardware selection 30 | Here's a secret: [You dont need powerfull hardware to host a foundry-vtt server](https://foundryvtt.com/article/requirements/). A raspberry pi (4B) will do, as will a NAS or NUC. Or, if you dont mind, use an old desktop or laptop as a server. 31 | 32 | What you need is: 33 | * one or two core CPU's (anythng over 1Ghz will do) 34 | * 1GB ram 35 | * at least 1GB disk space 36 | 37 | In my experience, the biggest bottleneck for a smooth running game is for your server to serve all your assets quickly. For best results: 38 | * Make sure you are using an SSD (SD Cards or other Flash memory will be quickly destroyed by foundry's frequent read/writes) 39 | * Have a good, wired connection 40 | * If you are going with a raspberry pi, make sure it's a Pi 4B as you will want usb 3.0 and gigabit ethernet 41 | 42 | ## Getting Foundry-VTT 43 | Before you get started you'll need to have [bought foundry-vtt](https://foundryvtt.com/purchase/) and have received your license. 44 | 45 | ## Getting a domain name 46 | You basically have two choices: a fancy pancy full domain like `AgeOfWorms.com` or `TheCityOfSharn.org` (or whatever you campaign or setting is called), or a free subdomain like `mycampaign.duckdns.org`. The `.org`, `.com` and `.net` domains are paid and usually start at $8 per year, while duckdns.org is completely free. If you want a full domain name, shop arround a bit. Prices vary and sometimes you can get the domain at a discount. 47 | 48 | The rest of this guide breaks down into two paths based on whether you have bought a full domain name or are using duckdns. 49 | 50 | ### Option 1: full domain name (like godaddy.com or domains.com) 51 | * register the domain name 52 | * add (or replace) your IP adrress to the dns A record. The details depend on your provider, but the result should be something like this: 53 | 54 | name type content ttl 55 | www A your.ext.ip.adr 24H 56 | @ A your.ext.ip.adr 24H 57 | 58 | 59 | What this does is tell your provider that you would like to forward traffic for www.yourdomain.com and yourdomain.com to the ip address your.ext.ip.adr (e.g 66.102.13.99). The provider will tell other DNS servers that whenever someone asks for the addresses you specified, they should forward likewise. Those providers tell other providers, etc. etc. This is called propagation. This is quite fast, but the internet is a big place, so it still takes a couple of hours before all DNS servers are aware of your new domain name. 60 | 61 | ### Option 2: duckdns (the free and easy option) 62 | * log in to [duckdns.org](https://www.duckdns.org/) however you want 63 | * enter the domain name you want to register 64 | * duckdns will look up your public IP and fill that in. 65 | 66 | You do **not** need to [set up a cron job to keep your IP address updated](https://www.duckdns.org/install.jsp), we use a docker container which will do this for us. This prevents us from having to make our router's public IP address public. 67 | 68 | ### Test 69 | Test that the domain resolves to the correct IP 70 | * go to www.mxtoolbox.com/DNSLookup.aspx 71 | * enter your domain name 72 | * verify that it resolves to your IP address. 73 | 74 | If it doesn't, wait a bit and try again. 75 | If it still doesn't, [verify your public IP was set correctly](https://whatismyipaddress.com/) 76 | 77 | # Setting up the host 78 | This is about configuring your host machine (which we discussed in the hardware selection section previously) as a server. If this is not a [NAS](https://en.wikipedia.org/wiki/Network-attached_storage), I recommend doing a fresh install of your OS of choice. I assume this will be some sort of BSD or Linux system; such as Raspbian, Debian, or perhaps Proxmox or similar. I won't go into the details of doing this. The rest of this tutorial assumes you have Debian installed (mostly because that's what I'm running). 79 | 80 | Note: only install operating systems compatible with your storage device. I.e., don't install Ubuntu onto a SD Card or you will burn the SD out quickly. 81 | 82 | 83 | ## Update your server 84 | For the best security you should always keep your system as up to date as possible. Updates are often security updates which patch known vulnerabilities. 85 | 86 | sudo apt update 87 | sudo apt upgrade 88 | 89 | 90 | ## Install Docker 91 | Docker has a really good install guide for multiple systems. 92 | 93 | [Guide for Debian](https://docs.docker.com/engine/install/debian/) 94 | 95 | There are also some recommended [post-install steps.](https://docs.docker.com/engine/install/linux-postinstall/) 96 | For added security, I configure docker to be managed as a non-root user. For ease of use, I configure docker to start on boot. 97 | 98 | 99 | ## Setting up a data folder for your resources 100 | It's a good idea to create a folder where you will store all your art assets in one easily searchable place. Your library will probably grow (god knows it never shrinks) so it's a good idea to put some thought into the organization now, as it's a pain to change it later. You can put this is your home directory (/home/user/resources) or any place else that makes sense for you. 101 | 102 | I personally use a structure: 103 | 104 | - resources 105 | - assets 106 | - maps 107 | - tokens 108 | - campaign specific stuff. 109 | 110 | For the rest of the guide, I am assuming you have a folder called resources that contains all this stuff. 111 | 112 | ## Set up a static IP address for the host machine 113 | You need to make your router always assign the same internal network IP address to the host machine. The steps to do this will depend on your router, but are generalised [in this guide](https://au.pcmag.com/networking/65062/how-to-set-up-a-static-ip-address). Having a static IP for the host machine ensures that you don't have to update port forwarding rules whenever it changes. 114 | 115 | Please note this is different from making your router's public IP static. 116 | 117 | Make a note of the ip address of your host. If you are using Debian run: 118 | 119 | ip addr 120 | 121 | ## Set up Port Forwarding 122 | You need to configure your router to port forward port 80 (http) port 443 (https) to your host. Unfortunately, different routers do this is in different ways. [This guide](https://www.noip.com/support/knowledgebase/general-port-forwarding-guide/) has some help for different brands of routers. 123 | 124 | 125 | # Containers 126 | This section is going to be about selecting and configuring your containers. We are going to combine foundry and Swag into a single stack, with docker taking care of most of the plumbing (such as the networking between the containers). 127 | 128 | ## Preparing Foundry 129 | There is currently no official foundry-vtt container, but there are plenty of options created by fans. Which one is the best is going to vary over time. Have a look on [foundryvtt.com](https://foundryvtt.wiki/en/setup/hosting/Docker) for some of the more popular options. Two of my favorites: 130 | - https://hub.docker.com/r/felddy/foundryvtt is quite popular and seems easy to set-up and configure. If you go this route, make sure you use `secrets.json` to store your password and key 131 | - https://github.com/BenjaminPrice/fvtt-docker (aka direckthit) strikes a happy medium (for me) between convenience and security. Basically, you download the zipfile yourself and a script in the container does the rest. No credentials to store, no credentials to accidentally leak. 132 | 133 | For this guide, I'm using felddy. 134 | 135 | 136 | ## SWAG 137 | Linuxserver.io has made an excellent set of containers. I personally have a bunch of them running on my home server. One of the best ones is [SWAG](https://docs.linuxserver.io/general/swag), a container that combines Letsencrypt, nginx, a reverse proxy and fail2ban. Trust me, it's cool. 138 | 139 | What is does, handle your incoming connections and directs them to the correct server, while keeping the bad stuff out. 140 | 141 | ### Setting up the folders for your containers 142 | Create a folder to store the container data. Where it should go depends on your system configuration. If you are using a raspberry pi, the best place may be your home directory. 143 | 144 | mkdir -p ~/swag-foundry/foundry 145 | mkdir -p ~/swag-foundry/swag 146 | cd ~/swag-foundry 147 | 148 | 149 | ## Get a timed url 150 | go to https://foundryvtt.com/community/me/licenses, select Linux/NodeJS under Operating system and click **timed url** 151 | a temporary url file will be copied to your clipboard. 152 | 153 | *note* 154 | the next steps may take more time than you have for your timed url. Dont worry about it. You can request a new timed url as often as you like. Just keep the tab open and be ready to copy it again when needed. 155 | 156 | 157 | ## Deploy the stack 158 | We are going to create a configuration file for docker that tells it how to run our swag/foundry stack: 159 | 160 | nano docker-compose.yaml 161 | 162 | copy the following into the file: 163 | 164 | ### Option 1: if you are using a full domain name 165 | ```yml 166 | version: "3.8" 167 | 168 | services: 169 | foundry: 170 | container_name: foundry 171 | hostname: foundry 172 | image: felddy/foundryvtt:release 173 | environment: 174 | - FOUNDRY_RELEASE_URL= # replace with the timed url you copied earlier 175 | ports: 176 | - 30000:30000 177 | volumes: 178 | - path/to/your/foundry/data/directory:/data/foundryvtt # replace with the correct path to your foundry config dir 179 | - /path/to/your/resources:/data/foundryvtt/Data/resources # replace with the correct path to your resources 180 | restart: unless-stopped 181 | 182 | 183 | swag: 184 | image: ghcr.io/linuxserver/swag 185 | container_name: swag 186 | cap_add: 187 | - NET_ADMIN 188 | environment: 189 | - PUID=1000 190 | - PGID=1000 191 | - TZ=Europe/Amsterdam # replace with your timezone https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 192 | - URL=yourdomain.com # replace with your domain name 193 | - SUBDOMAINS=www 194 | - VALIDATION=http 195 | - EMAIL=address@example.com # replace with your email address here (optional) 196 | volumes: 197 | - /path/to/your/swag/directory:/config # replace with the correct path to your swag config dir 198 | ports: 199 | - 80:80 200 | - 443:443 201 | restart: unless-stopped 202 | ``` 203 | 204 | ### Option 2: if you are using using duckdns 205 | ```yml 206 | version: "3.8" 207 | 208 | services: 209 | duckdns: 210 | image: ghcr.io/linuxserver/duckdns 211 | container_name: duckdns 212 | environment: 213 | - PUID=1000 214 | - PGID=1000 215 | - TZ=Europe/Amsterdam # replace with your local timezone https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 216 | - SUBDOMAINS=mysubdomain # replace with to your DuckDNS subdomain name. 217 | - TOKEN=blah-blah-blah # replace with your DuckDNS token here 218 | restart: unless-stopped 219 | 220 | foundry: 221 | container_name: foundry 222 | hostname: foundry 223 | image: felddy/foundryvtt:release 224 | environment: 225 | - FOUNDRY_RELEASE_URL= # replace with the timed url you copied earlier 226 | ports: 227 | - 30000:30000 228 | volumes: 229 | - /path/to/your/foundry/data/directory:/data/foundryvtt # replace with the correct path to your foundry config dir 230 | - /path/to/your/resources:/data/foundryvtt/Data/resources # replace with the correct path to your resources 231 | restart: unless-stopped 232 | 233 | swag: 234 | image: ghcr.io/linuxserver/swag 235 | container_name: swag 236 | cap_add: 237 | - NET_ADMIN 238 | environment: 239 | - PUID=1000 240 | - PGID=1000 241 | - TZ=Europe/Amsterdam # replace with your timezone 242 | - URL=subdomain.duckdns.org # replace with this to your DuckDNS hostname 243 | - VALIDATION=http 244 | - DUCKDNSTOKEN=blah-blah-blah # replace with your DuckDNS token here 245 | - EMAIL=address@example.com # replace with your email address (optional). 246 | volumes: 247 | - /path/to/your/swag/directory:/config # replace with the correct path to your resources 248 | ports: 249 | - 80:80 250 | - 443:443 251 | restart: unless-stopped 252 | ``` 253 | 254 | Modify the config on each line where there is a `# replace with` comment. 255 | this is also where you need the **timed_url** you copied to your clipboard. 256 | Save the docker-compose.yaml file. 257 | 258 | run: 259 | 260 | docker-compose up -d 261 | 262 | This will configure the container and run in detached or daemon mode. 263 | 264 | ## Configure reverse proxy 265 | 266 | Look into the swag config files. 267 | 268 | cd ~/swag-foundry/swag/config/nginx/site-confs/ 269 | 270 | You should add an entry for foundry: 271 | 272 | nano foundryvtt.conf 273 | 274 | Add the following content to the file: 275 | 276 | # only serve https 277 | map $http_upgrade $connection_upgrade { 278 | default upgrade; 279 | '' close; 280 | } 281 | 282 | server { 283 | listen 443 ssl http2; 284 | server_name yourdomain.com www.yourdomain.com; # add your domain name here. if you want to use both with and without www add both here. 285 | 286 | # make sure ssl is enabled 287 | include /config/nginx/ssl.conf; 288 | 289 | client_max_body_size 0; 290 | ssl_session_cache shared:SSL:10m; 291 | proxy_buffering off; 292 | 293 | location / { 294 | include /config/nginx/proxy.conf; 295 | 296 | resolver 127.0.0.11 valid=30s; 297 | 298 | set $upstream_proto http; 299 | set $upstream_app foundry; 300 | set $upstream_port 30000; 301 | proxy_pass $upstream_proto://$upstream_app:$upstream_port; 302 | } 303 | } 304 | 305 | 306 | Add your domain name on the indicated line. 307 | 308 | Save and close. 309 | 310 | Restart swag, so that the new config is loaded. 311 | 312 | docker-compose restart 313 | 314 | Verify everything works by going to www.yourdomain.com. You should see the foundry login screen. 315 | 316 | 317 | ## Updating 318 | Updating is done by stopping, removing and redeploying the stack. Before you do this, **shut down your game world.** You may want to **create a backup** as well. 319 | to update your server, you will need to get a new timed url and replace the existing one in docker-compose.yaml 320 | 321 | see: 322 | * [Get a timed url](#get-a-timed-url) 323 | * [Deploy the stack](#deploy-the-stack) 324 | 325 | 326 | Run: 327 | 328 | cd ~/swag-foundry 329 | docker-compose rm --stop 330 | nano docker-compose.yaml 331 | 332 | edit the file 333 | 334 | now restart your containers 335 | 336 | docker-compose up -d 337 | 338 | Again **close your world** and **back up your data** 339 | 340 | 341 | # Wrapping up 342 | Some things not covered here, but which may be useful: 343 | 344 | * **backups** make sure you backup your world regularly. I personally have a script that creates a backup every morning using rsync. I may add a how-to later if people are interested 345 | * **extra options in felddy's docker container** this guide is long enough as it is. however, Felddy has added a great number of options to run scripts, configure your server, have a certain world be ready, even a custom login screen. Check out his options [here](https://github.com/felddy/foundryvtt-docker) 346 | * **searchable resources** Foundry's search function, quite frankly, sucks. My workaround is to have a separate container running [pigallery](https://github.com/bpatrik/pigallery2) which is a free photo album (like google photos) that allows me to search different photos based on keywords. So if I'm looking for a chest, or a candle or a bridge to plop into my game I can easily do that. 347 | * **HTTP Basic auth** an extra authentication step that limits people's access to your foundry server, even before they hit the logon screen. I may upgrade this to a best practice, but I want to test it out for myself first. A How-to guide can be found [here](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/). 348 | * **portainer** portainer adds some features to docker that look good and may be helpful. This includes a web interface and some cool tools to manage your containers. There's a short guide below: 349 | 350 | ## (optional) Install portainer 351 | Portainer is a container management system. It basically adds a web interface to docker and gives you some handy tools. You can absolutely do without. It just makes life that little bit easier. 352 | 353 | As portainer itself runs in docker, deploying it is as simple as running two commands 354 | 355 | docker volume create portainer_data 356 | 357 | This creates a persistent place to store some of the container's data. Usually containers will lose all data when you restart the container. This is a feature that makes containers more predictable and more secure. But sometimes you need certain data, such as config files to remain after you have restarted a container. That is where volumes come in. Basically you are telling docker to reserve a place called portainer_data where this data can be stored. 358 | 359 | docker run -d --name=Portainer --hostname=Portainer -p 8000:8000 -p 9000:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data -e TZ='Europe/Amsterdam' portainer/portainer-ce 360 | 361 | This tells docker to start portainer. The variables are: 362 | 363 | docker run --> tell docker to run a container 364 | -d --> run in daemon or detached mode. basically run in the background 365 | --name=Portainer --> the name that docker uses to identify this container 366 | --hostname=Portainer --> the name other computers use to identify this portainer on the network 367 | -p 8000:8000 --> map port 8000 on your host to the same port in the container. Port 8000 is used mostly for managing other portainer instances, so I'm not sure if you need this. 368 | -p 9000:9000 --> map port 9000 on your host to the same port in the container. This means that users that visit http://:9000 will be served the portainer web interface. 369 | --restart=allways --> allways restart (recover) the container after a crash. 370 | -v var/run/..... --> this maps (shares) what is going on with docker on your host to the container. The container needs this to monitor and manage other containers on your network 371 | -v portainer_data:/.. --> this maps (shares) the persistent volume you created to your container, so that your configurations remain persistent between restarts 372 | -e TZ='Europe/Amsterdam' --> set the timezone to where you live. You can change it to where you live. If you remove this part entirely, the container will default to UTC 373 | portainer/portainer-ce --> the name of the base image. Docker will look up this container on your host system, or download it from the docker repository if it is not present. 374 | 375 | Test if portainer is working by visiting http://hostip:9000 376 | 377 | You should see a registration screen. register and press +create user 378 | 379 | next, choose the install type: LOCAL 380 | 381 | you should see a dashboard 382 | --> click on local 383 | --> click on containers 384 | --> you should see 1 container active; you can inspect it using portainer, restart it, stop it or kill it (dont do those last two!).. oh, and maybe next time I should put the warning before the command that will destroy your pretty web interface... 385 | 386 | 387 | 388 | 389 | # Thanks 390 | Thank you to all the wonderful people on reddit for helping me to improve this guide. Shout out specifically to u/PriorProject for his help on HSTS and HTTP basic and to u/WindyMiller2006 for his help on integrating SWAG and foundry into a single stack. 391 | 392 | 393 | That's it. Happy gaming, and please give some feedback, either by raising an issue, or making a pull request. 394 | --------------------------------------------------------------------------------