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.
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.
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/id_rsa.pub
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 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):
ssh root@YOUR_IP_ADDRESS
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):
ssh USER_NAME@YOUR_IP_ADDRESS
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.
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 https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh sudo bash nodesource_setup.sh
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.
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:
#!/bin/bash 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" ]]; then # 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 else echo "Not master branch. Skipping." fi # We will add other steps here done
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 [email protected]:username/your-app.git 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 [email protected]:username/your-app.git
Many thanks to Alex for his helpful article on this. Now that we can deploy code, let’s run our application.
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
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 start
command. 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.
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.
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.
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; server_name example.com; 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.
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.
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 YOUR_DOMAIN.rocks
.
Once set up you will be prompted to add two name servers to your domain. Take note of these.
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!
You should now be able to access your app at your own domain name, using https://YOUR_DOMAIN.rocks
.
So what should you do if you see the dreaded 520
error? A few things to try:
npm run build
and that it is successfulpm2 list
to see the app is runningActive
in sudo service nginx status
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
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.
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!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
8 Replies to "Hosting your own Svelte / Sapper app"
Who’s doing these illustrations. They are amaze balls! 🤩
You can find him here: https://ahoymatej.com/
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;
Correct:
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!
Good catch, thanks for pointing that out
Matt. I like ypur posts. Are you in tweeter? Do you have a blog or repo in gitgub
Hi Gustavo, I’m a content editor for LogRocket — Donovan is the author of the post. You can find him on Twitter here: https://twitter.com/donovanh
Hey Donovan,
First thank you for the nice writeup!
There’s a typo in your post-receive hook, you used both GITDIR and GIT_DIR, this leads to a syntax error that took me quite a while to figure out. If you’re already in the process of fixing this, maybe also change from master to main for the branch check.
Best, Luca
It works !!! Excellent !!!