Donovan Hutchinson JavaScript developer. Solving UX challenges, designing and building web things, teaching @cssanimation, and always learning.

Hosting your own Svelte / Sapper app

10 min read 3052

You’ve built an amazing app using Sapper and Svelte, but now what? Where do we host it to make it available to the world? This article will set out the steps involved in one possible approach, self-hosting your own Sapper application using DigitalOcean.

I recently went through this process having built an application and hosting it using Vercel. They have a slick deployment process, but as they better suit serverless applications I quickly realized I needed more than what they offered. So I rented a virtual server and moved the app there.

If you like you can see my Shop Ireland Sapper / Svelte project in action. It’s running on a DigitalOcean droplet as a Node app, with an Nginx reverse proxy. I also have a second app running alongside that acts as an API layer to get product information from Amazon.

What we cover in this article

In this article, I’ll walk through the steps I took to set up a server to run Node projects such as Sapper applications. I hope this can act as a good starting point for you if you’re interested in running your own Svelte / Sapper app.

Note: this is written based on Ubuntu version 18.04. Some specifics might have changed with newer versions.


  • Setting up your Digital Ocean droplet
  • Node and Nginx proxy server
  • Pushing your app code using Git
  • Running your app using PM2
  • Setting up Nginx server block with caching
  • Adding a domain name
  • Testing your site

Getting started with a virtual server

My site only serves around 500 page views per day, so it’s not in need of a powerful hosting plan.

There are lots of services to choose from that offer virtual servers. When you rent a virtual server you get a secure slice of a server that acts as a standalone machine. Generally, they don’t come with management tools like phpMyAdmin, but we won’t be needing anything like that. I’m also familiar with Ubuntu so I went with that as the platform.

To get started I chose an entry-level server for $5 per month, with a location close to my main audience. Even at this price, it’s plenty powerful for running a Svelte and Sapper application.

When registering and setting up your machine, it’s a good idea to choose to log in using SSH. You should be able to add the content of your public SSH key. This can be found with (Mac/Linux):

cat ~/.ssh/

If not, the “create RSA key pair” step in this guide should help.

Once you have your machine registered, we can log in and start setting up our server!

We made a custom demo for .
No really. Click here to check it out.

Log in and set up SSH, sudo, and firewall

We need to get some initial setup out of the way. Begin by logging in using ssh (YOUR-IP is the IP address given during setup):


Once logged in, set up a user by following this short guide. This should give you your own login using the same SSH key. You will then be able to log in to your server using (username is whatever name you chose during the above steps):


You’ll only be able to access the server via SSH as the firewall blocks other ports, but we will fix that when installing Nginx. You’ll also be able to use sudo!

Next, we will set up Node and the Nginx reverse proxy.

Node and Nginx proxy server

Since I’m setting up a Sapper application that uses Express, we need Node. Begin by adding some repository information to our system. The following downloads and runs a script that adds the necessary repo URLs.

You can find the scripts here if you want to see what they do:

cd ~ curl -sL -o sudo bash

With that set up, install Node:

sudo apt install nodejs

Once finished you should be able to see your Node version with nodejs -v. One last thing, be sure to add the build-essential package also as some packages will need it:

sudo apt install build-essential


Nginx (pronounced Engine-X) is a lightweight, fast webserver well suited to acting as a gateway to our Node application. It’s very powerful and can do a lot but we’ll mostly be leaning on it for our caching.

We begin by updating and then installing the package:

sudo apt update sudo apt install nginx

Before we can run it, we need to let the firewall know. For this guide, I’m not going to install SSL directly on Nginx, but if you want to do so this guide has more in-depth information.

Let’s add Nginx HTTP access to the firewall:

sudo ufw allow 'Nginx HTTP'

We can check the status of Nginx by running systemctl status nginx. If it says Active, you should be able to navigate to your IP address and see the success page.

Next, we’ll leave the Nginx configuration aside for a moment and set up our Sapper application and API code.

Pushing your app code using Git

There are many ways of building, testing, and deploying projects. For this project, I wanted something quite simple. I knew there would only be one person working on it at a time and I didn’t want to introduce lots of layers of complexity.

Back in the day, I’d have turned to a tool such as FTP to push files up to the server. Thankfully we have better means of deploying these days, and one such approach is to use a Git hook. With a Git hook, we can have our code copy automatically from our master branch to the server. In the following steps, I am reusing some of the code from this overview.

We get started by creating our directories to store the code. You can call them anything you like but I’m going to go with repo and www in this case. The repo represents the Git repository that we push our code to, and the www directory contains our live code:

cd ~/ mkdir -p repo/YOUR_APP mkdir -p www/YOUR_APP

The YOUR_APP part is your app name. Call it what you like but be sure to replace further references to YOUR_APP with the same name.

We navigate to our repo folder and set up a bare Git repo:

cd ~/repo/YOUR_APP git init --bare

Now we set up a Git hook to carry out the commands we want to run after we push to this repo. Now we create post-receive hook.

I’m using nano to edit files on the server, but replace the command as needed. I will create and save the hook using:

cd hooks nano post-receive

And adding the following:

GITDIR="/home/don/repo/YOURAPP" TARGET="/home/don/www/YOUR_APP"
while read oldrev newrev ref do BRANCH=$(git rev-parse --symbolic --abbrev-ref $ref)
if [[ $BRANCH == "master" ]];
   # Send a nice message to the machine pushing to this remote repository.
   echo "Push received! Deploying branch: ${BRANCH}..."

   # "Deploy" the branch we just pushed to a specific directory.
   git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f $BRANCH
   echo "Not master branch. Skipping."
# We will add other steps here

And save the file with Control+X then y.

Before we continue we need to make this file executable:

chmod +x post-receive

The above code sets up some variables (adjust the YOUR_APP part as needed!) and then within the do and done lines it runs code to apply our hook.

Currently, all it’s set up to do is copy our code, but only if we’ve pushed the master branch. Let’s try it. To use this we need to add a remote to our project. In our project folder on our local machine, add the following remote:

git remote add origin USER_NAME@YOUR_IP_ADDRESS:/home/USER_NAME/repo/YOUR_APP

For all the shouty bits above, be sure to replace them with the relevant username, IP address, and the name of your app directory.

Now when you push to master (you may need to make a change first) you should see something like:

remote: Push received! Deploying branch: master...

Along with some other Git noises. If you then return to your server and check in the www/YOUR_APP directory, you should see your app files!

Before we move on, let’s make the Git deploy process easier by adding multiple origins. Adjust the following commands to include your Git repo location.

git remote set-url origin git remote set-url --add --push origin USER_NAME@YOUR_IP_ADDRESS:/home/USER_NAME/repo/YOUR_APP git remote set-url --add --push origin

Many thanks to Alex for his helpful article on this. Now that we can deploy code, let’s run our application.

Running your app using PM2

I usually run my Node applications using npm start (or sometimes npm run dev when working locally). On the server, we could certainly do the same, but unless we use some kind of service to monitor our application, it could crash and become unresponsive.

It’s good to have some kind of tool to automatically restart our app, as well as starting up any apps when our server restarts. PM2 is a useful tool that manages our applications and ensures they stay up.

Begin by installing it globally so that it can be used wherever:

sudo npm install pm2 -g

Running a Sapper app

Let’s get the app running. First, we need to install dependencies:

cd ~/www/YOUR_APP npm install

Before we run our Sapper app we need to build it. While working in a dev environment we don’t need the build step as it compiles for it, we do need this step when running in production. So before we start running our app, build it with:

npm run build

This should output a lot of lines of content as it builds all the scoped JavaScript fragments of your app. We can then run the app using PM2.

While we can set up PM2 for most Node apps with a simple pm2 start app.js --name App, we need to use our npm startcommand. We can do it like this:

pm2 start npm --name "AppName" -- start

With that running, we save the current state of the PM2 system with:

pm2 save

You can also check the status of your running apps any time with pm2 list. See pm2 examples for more.

Lastly, we want to make sure the app loads when the server restarts. Set this up with:

sudo env PATH=$PATH:/usr/local/bin pm2 startup -u USER_NAME

Be sure to replace USER_NAME with your actual chosen username as before.

You can now test your app. If all went to plan, it should be running on port 3000:

curl http://localhost:3000

If you get an error here, check your code is all in place and run pm2 restart AppName to restart it.

Improving the Git hook

With the app running, we can now improve our Git hook to have it handle the build step, run npm install, and restart PM2 on every deploy. Update our hook by adding the following before the done line:

cd /home/don/www/YOUR_APP npm install --production npm run build pm2 restart AppName

Again be careful to replace YOUR_APP and AppName with your values.

Now when you push to master, it should install dependencies, build the app, and restart the app using PM2.

Setting up Nginx server block with caching

We have our app running and we get the expected output at localhost:3000 so the next step is to let Nginx act as a reverse proxy. This will catch any web requests to port 80, and direct them to our app (or a cached version).


Before we set up our server block to tell Nginx where to find our app, we will quickly set up caching. Nginx has a cache option that looks at the headers sent back from our app and saves a copy of the request to disk. It then returns this saved version to each new request until the cache expires.

Begin by creating a directory for our cache to be stored:

sudo mkdir /var/cache/nginx

We will then configure the cache settings in our server block.

Adding the server block

We begin by setting up an entry in our sites-available directory.

sudo nano /etc/nginx/sites-available/YOUR_SITE.vhost

In this file we add some directives to tell Nginx how to serve up our app:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=480m use_temp_path=off;
server { listen 80; listen [::]:80;

  location / {
     proxy_cache my_cache;
     proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
     proxy_cache_revalidate on;
     proxy_pass http://localhost:3000;
     proxy_http_version 1.1;
     proxy_set_header Upgrade $http_upgrade;
     proxy_set_header Connection 'upgrade';
     proxy_set_header Host $host;
     proxy_cache_bypass $http_upgrade;

Exit and save using Control + X, y.

At the start, the proxy_cache_path sets a location for the cache along with how many levels to store (this can be tweaked if you think you’ll be storing millions of requests). The keys_zone part sets a named zone and how big it is, in memory, for storing metadata about the cached content. We set max_size as the maximum amount of space the cache can take up. In this case, it’s a generous 10g.

We set an optional inactive value to 8 hours in this case, and set use_temp_path to off as this saves a little bit of performance as it no longer writes content twice.

Next up is the server block itself. In this we set the port, set up a location and tell the proxy_pass to pass requests through to our app on http://localhost:3000.

We also tell the server where to find the proxy_cache that we defined above, and we use a very interesting optional setting proxy_cache_use_stale. This tells the server to use old cached entries if for some reason the file returns an error.

There are a lot of settings available, and you can find out more in this article containing the full Nginx caching options.

Activating the server block

We now have a file in sites-available but we need to activate it by creating a link to it from sites-enabled.

sudo ln -s /etc/nginx/sites-available/YOUR_SITE.vhost /etc/nginx/sites-enabled/

There could be a “default” file or similar already in the sites-enabled directory. We don’t need that so you can delete it.

Now that we have the file linked fromsites-enabled we can test to check our config is ok using:

sudo nginx -t

If all is not well, check for typos or missing semi-colons! If it is, restart Nginx to activate the changes:

sudo service nginx restart

We should now be able to access our content on port 80 via this Nginx reverse proxy:

curl http://localhost:80

If all is well, let’s move on to setting up our public URL and HTTPS.

Adding a domain name

With an app running on port 80 and an IP address, it’s time to give this app a home in the form of a URL.

There are many ways to handle domain names but something I like to do is use CloudFlare and make use of their free “flexible” SSL service. It’s not 100% secure, but can be upgraded to provide end to end encryption for sites or apps that deal in sensitive data.

In this case, my app has no log in component and doesn’t send any sensitive data to or from the browser so I’m content with their most simple SSL setting. If you want to add more you can set up your own SSL or use another service.

So if we’re using CloudFlare, head over and register. Then add a new site and select the Flexible SSL option.

If you have a domain already, it will prompt you to move the records. You might not want to keep the old records but instead take this opportunity to add in an A record pointing to your server’s IP address, and a CNAME for www pointing to

Once set up you will be prompted to add two name servers to your domain. Take note of these.

Setting up your nameservers

Log in to your registrar and navigate to the section where you set the name servers. Update the list to contain just the two given by CloudFlare.

Then, return to CloudFlare and press the button to continue. It may take a few hours for the updated nameservers to be detected. Once it detects the changes it will email you, and your site is live!

Testing your site

You should now be able to access your app at your own domain name, using


So what should you do if you see the dreaded 520 error? A few things to try:

  • Check your app has been built with npm run build and that it is successful
  • Check pm2 list to see the app is running
  • Look for Active in sudo service nginx status
  • Double-check the location server block to ensure it’s looking for localhost:3000

Lastly, you can also check logs for both at their default locations using:

sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

Wrapping up and next steps

We should now have our app running on our own managed server. From here we can think about adding services such as databases, setting up more location blocks for other apps and APIs we might build, and so much more.

See it yourself

You can see the final result online at Shop Ireland. It’s a Svelte application running on Sapper on the backend, plus there’s an API running on the same server to serve up the product data.

The site is fast thanks to the light Svelte frontend, the Nginx cache, and I can easily deploy changes using Git.

I hope this article is useful and helps you get your awesome Sapper projects onto the web!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Donovan Hutchinson JavaScript developer. Solving UX challenges, designing and building web things, teaching @cssanimation, and always learning.

6 Replies to “Hosting your own Svelte / Sapper app”

  1. Very useful info – Thank you!

    One error. it appears that something the system doesn’t like underscores ‘_’. as a result, the top line in the file listing contains errors.

    As listed:
    proxycachepath /var/cache/nginx levels=1:2 keyszone=mycache:10m maxsize=10g inactive=480m usetemp_path=off;

    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=480m use_temp_path=off;

    Still, your article really helped me out.

    Thanks again!

Leave a Reply