Cloud-Hosting FoundryVTT in Lightsail

Plus CloudFlare and proper LetsEncrypt Certs

I got bored and decided to play with a cloud-hosted Foundry VTT server. They have some great guides for getting started with some of these. I could get a little more bang for my buck though if I went with Amazon Lightsail. Lightsail is basically the same as EC2, but simplified a bit (so not as many options), and about half the price. I'm not going to write a guide on how to stand up one of those, but for my needs, I went with an Ubuntu 20.04 instance with 1 GB RAM, 1 vCPU, 40 GB SSD. Snag a static IP address for your instance, and you probably want firewall rules that only allow SSH from certain locations, but allow HTTP and HTTPS from anywhere. Once you're done and SSH'ed in, the fun begins.

Update 22 Mar 2021: I've been asked why I didn't use the usual NGINX reverse proxy. Mostly it's because this evolved as I was standing up the server. I didn't originally see NGINX as a requirement, and wanted to limit my dependencies. It was only when I realized I needed an 80->443 redirect that I added NGINX. Things work this way, SSL certificate renewal works, Cloudflare is handing some/most of the static caching, there's no vital need for the reverse proxy. That said, I host a few sites for fun, and don't do this for a living; this isn't my "thing". The primary reason I've read for it's need is performance. So if you're running a large game or seeing poor performance, you might want to implement this. I probably will add this in the future, and if I do so, I'll add a note here and an update at the very bottom.

Update 22 Mar 2021: NGINX Proxy Added, See Bottom

Initial Setup

Almost everything below was performed as root.
First, run updates and install some necessary packages.

apt update
apt upgrade
apt install unzip
apt install authbind
apt install nginx
apt install certbot
curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash - 
sudo apt-get install -y nodejs

unzip and nodejs are both necessary for Foundry. However, Ubuntu still ships with node 10 appearingly, and Foundry requires 12+. Here's where I got the instructions for installing the latest version: https://github.com/nodesource/distributions/blob/master/README.md#debinstall

The rest of the packages, authbind, nginx, and certbot, we'll get to.

Now I'm going to create a user to run Foundry with, as I don't want it running with my default user or root, or anything with more permissions than it needs. I'm going to remove the password for that user, nobody will ever need to log in with that account.

adduser foundryvtt
passwd -d foundryvtt

Next I'm going to make some directories to hold the Foundry data. This is almost straight out of the DigitalOcean playbook that I liked above.

su foundryvtt
cd ~
mkdir foundrycore
mkdir foundrydata
exit

Now I'm going to grab the foundry install package. It's currently version 0.7.9. I had a bit of trouble using the temporary AWS link, I'm not sure why, so you may have to download it on a local host, and then SCP it to your remote host. Sorry that's not a lot of help, but once you get it there, move it into the proper place, and make sure your unprivileged user will have write access to it. SCP sample thanks to Przemkolote on Discord scp -i path_to_pem_file -r foundryvtt-0.7.9.zip ubuntu@public_ip_address:home/ubuntu

mv /home/ubuntu/foundryvtt-0.7.9.zip /home/foundryvtt/foundrycore
chmod 777 /home/foundryvtt/foundrycore/foundryvtt-0.7.9.zip

Before we start things up, lets sort a few network items. Well-known ports (0-1023) "are reserved for the operating system and core services." Foundry isn't one of these, so I used an old trick to allow it to run on port 80. You can read more in the link, but here's the steps.

cd /etc/authbind/byport
touch 80
chown foundryvtt 80
chmod 500 80

We'll end up un-doing some of this later, but for now we need it and we want to make sure everything is running correctly. We're going to change back to our unprivileged user for a bit, unzip the bundle, and try starting the service.

su foundryvtt
cd ~/foundrycore
unzip foundryvtt-0.7.9.zip
rm foundryvtt-0.7.9.zip
screen -s Foundry
authbind --deep node resources/app/main.js --port=80 --dataPath=/home/foundryvtt/foundrydata

CTRL + A + D (detach from screen)
If you ever want to reattach to the Screen, use screen -r

You should be able to access you Foundry server at this point. It may be helpful to leave this screen up so you can start/stop your service as we test, and start a new SSH session in a new window. Note the "authbind --deep" that we added in front of the usual node command.

SSL/TLS from Lets Encrypt

First, get a domain name, you can't register a certificate with just an IP address unless you're https://1.1.1.1 for some reason... Get DNS setup with CloudFlare. Again, actually registering the domain name and fixing DNS are outside the scope of this guide. I'll wait while you get there.

One important note for CloudFlare: For some reason it seems that under SSL/TLS settings, the encryption mode MUST be set to Full or Full (strict). Without this I either got 521 errors (if HTTP was not redirecting to HTTPS), or Too Many Redirect errors if it was. I'm not sure why.

Stop your Foundry server for a bit, we'll need to use port 80 when we get our cert. Certbot should be able to automatically grab your cert, and then you need to move it into the correct location for Foundry and set permissions that allows the user to read that cert. Replace server.name with your domain name.

certbot certonly --standalone -d server.name,www.server.name
cp /etc/letsencrypt/live/server.name/fullchain.pem /home/foundryvtt/foundrydata/Config/
cp /etc/letsencrypt/live/server.name/privkey.pem /home/foundryvtt/foundrydata/Config/
chown foundryvtt:foundryvtt /home/foundryvtt/foundrydata/Config/*.pem

Lets create another authbind port so Foundry can start using SSL/TLS (443 is still below 1023).

cd /etc/authbind/byport
touch 443
chown foundryvtt 443
chmod 500 443

Now restart Foundry, but this time using --port=443. At this point you may also want to try accessing it directly via IP, as well as hostname. Remember that CloudFlare caches, and may not be showing you all the problems.

Right now nothing is listening on HTTP, requests there will hit a dead end. Lets get NGINX to forward all of our HTTP traffic to HTTPS.

rm /var/www/html/index.nginx-debian.html
touch /var/www/html/index.html

cd /etc/nginx/sites-available
mv default backup_of_default.bak
vim default

In default we're going to put the following:

server {
    listen 80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
}

Now we can restart systemctl and remove our earlier port 80 authbind (nginx handles this just fine on it's own).

systemctl restart nginx.service
rm -rf /etc/authbind/byport/80

Last thing for the SSL/TLS settings. Lets make sure certbot has no issues renewing the certificate. We're going to make sure NGINX gets shut down when it needs to use port 80, make sure the new cert gets copied to the Foundry directory, and make sure Foundry gets restarted.

Edit LetsEncrypt's default configuration to add a pre- and post-hook action

cd /etc/letsencrypt
vim cli.ini

To the bottom add:

pre-hook =  systemctl stop nginx.service
post-hook = systemctl start nginx.service

Now to make a quick script to handle the Foundry stuff. I could have done the pre/post hook here as well, but I wanted to keep everything separated.

cd /etc/letsencrypt/renewal-hooks/post 
vim move.sh

Copy the following into move.sh (replace server.name with your proper path):

cp /etc/letsencrypt/live/server.name/fullchain.pem /home/foundryvtt/foundrydata/Config/
cp /etc/letsencrypt/live/server.name/privkey.pem /home/foundryvtt/foundrydata/Config/
chown foundryvtt:foundryvtt /home/foundryvtt/foundrydata/Config/*.pem
systemctl restart foundryvtt.service

Now make it executable with chmod 755 move.sh. Don't worry about that systemclt thing, we're getting there.

You should be able to test this by checking the current cert's date/time, running certbot renew --force-renewal, and then checking for a new cert. Remember, hit the IP, Cloudflair provides their own certs.

Create a service for Foundry

This will let us start/stop/restart the server much easier, including allowing us to restart it via our script above, and start Foundry automatically on boot. Big thanks to Ryan Himmelwright for doing 90% of the work. Mine is just a bit different (note, I don't use the UPnP port)

start by creating a service vim /lib/systemd/system/foundryvtt.service and inside that we're going to put:

[Unit]
Description=A service to run the Foundry VTT node app
Documentation=https://foundryvtt.com
After=network.target

[Service]
Type=simple
User=foundryvtt
ExecStart=/usr/bin/authbind --deep /usr/bin/node /home/foundryvtt/foundrycore/resources/app/main.js --port=443 --dataPath=/home/foundryvtt/foundrydata
Restart=on-failure

[Install]
WantedBy=multi-user.target

Now run systemctl daemon-reload and we should be able to do all the usual service commands, including systemctl enable foundryvtt.service to make it start on boot.

If your service is still running on your screen session. Kill that and use systemctl start foundryvtt.service to start Foundry, and systemctl status foundryvtt.service to see if it's running correctly

Random Other Stuff

I do like to have my firewall running, here's how I took care of that:

ufw allow OpenSSH
ufw allow http
ufw allow https
ufw enable

I also like unattended upgrades (security patches). They -might- cause problems, but it's rare that this happens to me. I would rather be patched that not. If you want, you can also turn on nightly snapshots in Lightsail for a backup. I'm not sure if this incurs additional cost though.

dpkg-reconfigure --priority=low unattended-upgrades
vim /etc/apt/apt.conf.d/20auto-upgrades

inside 20auto-upgrades, I have:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
        "${distro_id}:${distro_codename}-updates";
};
// Do "apt-get autoclean" every n-days (0=disable).
APT::Periodic::AutocleanInterval "7";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "06:38";
// Enable logging to syslog. Default is False
Unattended-Upgrade::SyslogEnable "true";
// Specify syslog facility. Default is daemon
Unattended-Upgrade::SyslogFacility "unattend-daemon";

NGINX Proxy

I've done some light reading on this, and there's a number of reasons I've seen to use an NGINX proxy. Here's Foundry's documentation. Lets talk through this a bit before I get to the how-to.

  1. It makes it easier to auto-renew SSL certs
    1. I'm already doing this, Foundry handles SSL just fine, I don't see that as an argument here.
  2. It provides compression (gzip).
    1. I'm not sure this is the case, per NGINX docs "By default, NGINX does not compress responses to proxied requests". Even if this were the case, CloudFlare is serving a lot of my content.
  3. It caches content.
    1. See CloudFlare.
  4. It provides threading
    1. As of Node.js 10, there was experimental worker thread support. I don't know what the current status is in 15, but I suspect it's there. I just don't know if Foundry devs are taking advantage of it.
  5. Routing
    1. Look, I don't have 10 Foundry nodes behind some enterprise frontend. Non-issue.
  6. CPU Speed/memory/resources
    1. This one I can buy. SSL and (if you are using it) gzip compression use a lot of CPU. NGINX is better at handling this than Node. Some of the material/benchmarks I'm looking at are a bit dated (see threading/v10 reference), but I would still believe this is the case. That said, I'm not running huge campaigns and my server is keeping up just fine as it is.
  7. It provides greater access control/security
    1. Neither Node nor NGINX is some clunky software from a mom and pop. Want to take bets on which one will have the next unauthenticated RCE? Regarding access control, maybe, but not that I'm using or that would impact me.

So how about metrics? I ran the before (Node) and after (NGINX) through Google's Pagespeed. The results were, not much of a change. On Node I got a 46 for Mobile and 85 for Desktop. On NGINX I got a 50 for mobile and 80 for Desktop. Really they're about dead even.

Look, as I said, this isn't my daily thing. I'm sure there's a reason it's advised, but I like to understand -why- I'm doing something. I can't see a really great reason here. However, there are a few upsides and no downside that I can see, so I say go for it. If nothing else, you've stopped serving content over both Node and NGINX, so you've reduced your footprint at least somewhat.

Implimentation / Here's the fun part
Stop both the NGINX and Foundry service. systemctl stop <service>
Edit your Foundry service file to serve content over port 30000 (or whatever you want) with vim /lib/systemd/system/foundryvtt.service. Once you've changed the port there, don't forget to do a systemctl daemon-reload.

Edit the Foundry config file with vim /home/foundryvtt/foundrydata/Config/options.json. We are going to set both our sslCert and sslKey back to null (don't forget to remove the "'s). Set proxySSL to true and proxyPort to 443.

Edit the NGINX config with vim /etc/nginx/sites-available. Here's the config I used (don't forget to change your server.name).

server {

    # Enter your fully qualified domain name or leave blank
    server_name _;

    # Listen on port 443 using SSL certificates
    listen                  443 ssl;
    ssl_certificate         "/etc/letsencrypt/live/server.name/fullchain.pem";
    ssl_certificate_key     "/etc/letsencrypt/live/server.name/privkey.pem";

    # Sets the Max Upload size to 300 MB
    client_max_body_size 300M;

    # Proxy Requests to Foundry VTT
    location / {

        # Set proxy headers
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # These are important to support WebSockets
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";

        # Make sure to set your Foundry VTT port number
        proxy_pass http://localhost:30000;
    }
}

server {
    listen 80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
}

Now restart the Foundry and NGINX services. Your site should be running behind NGINX now! I left my previous certbot settings in place, we could remove some extra material there, but there's really no need to.