DEV Community

Matt Day
Matt Day

Posted on

Deploy SvelteKit on Dokku in 8 Sort-of Easy Steps

Disclaimer: I'm not being sponsored by anyone to write this, I'm just a really big fan of the tech in this stack ❤️

Have you ever wanted—or needed—to host a webapp, but for one reason or another couldn't use the serverless offerings of Netlify, Vercel, Heroku, Deta et al? Those platforms are undoubtedly awesome, but on occasion, they just don't quite fit the bill.

Maybe it's because you can't deploy in your region and need to. Maybe the infrastructure your app needs is prohibitively expensive on a serverless stack. Maybe it's because of DBaaS co-location issues, or strict data compliance constraints. (side note, Vercel + PlanetScale or Supabase = chef kiss)

Whatever your reasons, you find yourself in a situation where you need to explore self-hosting as an option.

Enter dokku.


"What the heck is dokku?? Like, Heroku or something?"

In short: yes. It is very much like Heroku. Except for the awesome fact that it's your very own DIY PaaS.

Spin up a VM, install Dokku, create your app, git push your code and...it's done: your app is containerised, launched, stable, and ready to receive traffic, and you can install as many apps as your server can vertically scale for. (Of course, there's a little more to it than that 😉)

There are even plugins for Postgres, Redis, Let's Encrypt and more, which helpfully abstract away the provisioning of those services, but not to the exclusion of retaining control if you want 👍

The real beauty of it, for me, is that each time I make code changes, deployment becomes literally as simple as a git push and dokku will provision the new container and hot-swap the new version of the app with zero downtime.

So all the preamble said and done, if you're still with me, here are the 8(ish) sort-of easy steps to follow for a pain-free installation of SvelteKit on dokku.


What we're going to do

We're going to deploy dokku onto a VM, and provision the SvelteKit demo app onto it.

Prerequisites

  • You'll need a private cloud VM with SSH access. There are so many options; two very popular ones are Digital Ocean and Vultr. Whatever box you choose, just make sure it's rocking Ubuntu 20.04, and it has at least 1GB RAM.
  • You'll need a domain, and a DNS provider who lets you actually modify your records. Personally, I use Netlify for my DNS management.
  • You need to be mostly unafraid of the linux commandline - dokku has no UI 😉

Ready?


Step 1: Provision your VM

Spin up your server and take note of the public IP address; we'll need that for step 2.

I'm going to use 10.11.12.13 in the examples

Step 2: Set up your DNS records

I'm going to use example.io for illustrative purposes: substitute your domain and IP as appropriate, and we do this step first because it takes a hot second for the records to propagate/cache.

In your DNS manager, create a wildcard A record.

  A   *.example.io  10.11.12.13
Enter fullscreen mode Exit fullscreen mode

This points all subdomain traffic to our dokku server.

Additionally, if you want to also point the root of your domain to the same server (which you totally can), create a root A record as well.

  A   example.io  10.11.12.13
Enter fullscreen mode Exit fullscreen mode

Step 3: SSH into your VM

Let's get installing! We've got 2 packages we need to install: dokku and pack. Pack is what dokku uses to convert your awesome app code into a container.

First up, go sudo if you aren't already.

sudo su
Enter fullscreen mode Exit fullscreen mode

Install pack.

add-apt-repository ppa:cncf-buildpacks/pack-cli
apt-get update
apt-get install pack-cli
Enter fullscreen mode Exit fullscreen mode

Now install dokku - at time of writing, v0.25.4 is the most recent stable version, but check the docs for a more recent version.

wget https://raw.githubusercontent.com/dokku/dokku/v0.25.4/bootstrap.sh
DOKKU_TAG=v0.25.4 bash bootstrap.sh
Enter fullscreen mode Exit fullscreen mode

This will take a few minutes, be patient. Grab a coffee or do some squats or something.

Step 4: Configure global configurables

First point of order is your root domain, which is easily set.

dokku domains:set-global example.io
Enter fullscreen mode Exit fullscreen mode

Next we nudge dokku to use main as the default branch name rather than master.

dokku git:set --global deploy-branch main
Enter fullscreen mode Exit fullscreen mode

Third is to configure the buildpack dokku will use to containerize your code. The awesome folks at packeto.io have put together a buildback that takes care of this for us.

dokku buildpacks:set-property --global stack paketobuildpacks/builder:base
Enter fullscreen mode Exit fullscreen mode

Finally, access: ideally you want to not have to input a password every time you push from your repo, so if you've configured public-private keypair access, run this to give dokku access.

cat ~/.ssh/authorized_keys | dokku ssh-keys:add admin
Enter fullscreen mode Exit fullscreen mode

Step 5: Install plugins

There's one plugin we definitely want—Let's Encrypt, so let's install and configure that.

dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
dokku config:set --global DOKKU_LETSENCRYPT_EMAIL=mywebmasterinbox@gmailorother.com
dokku letsencrypt:cron-job --add
Enter fullscreen mode Exit fullscreen mode

(The email address you put here doesn't have to be affiliated with your domain in any way, it just has to be an address that you own.)

Whether you need to install other plugins depends on what your apps need (check out the full list). For demonstration purposes, let's also install the Postgres plugin.

dokku plugin:install https://github.com/dokku/dokku-postgres.git
Enter fullscreen mode Exit fullscreen mode

Step 6: Prepare our app!

Before we can push our code to our server, we need a spot into which to push it!

dokku apps:create sveltekit-demo
Enter fullscreen mode Exit fullscreen mode

(Again, name it whatever you wish, this is just for demo purposes)

Enable HTTPS via Let's Encrypt:

dokku letsencrypt:enable sveltekit-demo
Enter fullscreen mode Exit fullscreen mode

For our demo app this next step is unnecessary, but it illustrates how to provision a postgres database, then link it to our app:

dokku postgres:create sveltekit-demo-pg
dokku postgres:link sveltekit-demo-pg sveltekit-demo
Enter fullscreen mode Exit fullscreen mode

How easy is that! You'll see in the output, an ENV variable for the database URL has been generated - you can now references that variable in your code, and it will be hooked up in production 👍

Step 7: Prepare to Deploy

There are a couple of dokku-specific things we need to do to our codebase in your local machine, before we push it.

If you haven't already cloned the svelte demo, let's do that.

npm init svelte@next sveltekit-demo
# choose Demo, not Skeleton
# select the other options as you wish
cd sveltekit-demo
git init
git remote add dokku dokku@example.io:sveltekit-demo
git add .
git commit -m "Initial commit"
git branch -M main
Enter fullscreen mode Exit fullscreen mode

Install @sveltejs/adapter-node.

npm i -D @sveltejs/adapter-node@next
Enter fullscreen mode Exit fullscreen mode

Modify svelte.config.js to use the node adapter.

 import preprocess from 'svelte-preprocess';
+import adapter from '@sveltejs/adapter-node';

 const config = {
   preprocess: preprocess(),
   kit: {
+    adapter: adapter({
+      out: 'build'
+    }),
     // hydrate the <div id="svelte"> element in src/app.html
     target: '#svelte'
   }
 };
Enter fullscreen mode Exit fullscreen mode

Modify the build script in package.json.

 // package.json
 {
   "scripts": {
-    "build": "svelte-kit build",
+    "build": "svelte-kit build && ln -s -f build/index.js server.js",
   }
 }
Enter fullscreen mode Exit fullscreen mode

We create this symlink because packeto expects there to be a launch script called server.js in the project root. (There is a config that's supposed to let you change it, but that didn't work for me—this does though!)

Finally, create a file called project.toml - this is how we provide pack environment variables and other info specific to the build.

# project.toml
[build]
exclude = [
  "README.md"
  #, and/or another other files that are not relevant
  # to the build
]

[[build.env]]
name = "BP_NODE_RUN_SCRIPTS"
value = "build"

[[build.env]]
name = "NODE_ENV"
value = "dev"
Enter fullscreen mode Exit fullscreen mode

The first environment variable, BP_NODE_RUN_SCRIPTS, tells pack to run npm run-script build after dependencies have been installed. You can run more than one command if you like, eg. "lint,build" - this var accepts a comma-delimited list of commands to run.

The second environment variable tells pack to set the build environment to "dev"—namely something other than "production". This is important, because without it, the dev dependencies don't get installed, which means the entire SvelteKit infrastructure is missing, which causes the build to fail.

Step 8: Time to Push

Here goes nothing! Commit all your changes and push.

git add .
git commit -m "Make dokku compatible"
git push dokku
# if your trunk branch is called something other than main,
# do this instead
git push dokku BRANCH_NAME:main
Enter fullscreen mode Exit fullscreen mode

Dokku will think for a moment, download the latest packer images from packeto, build your container, compile your svelte app, and launch it. 🚀 🎉

Step 9 [BONUS]: Admire your work

You should now be able to navigate to https://sveltekit-demo.[YOUR DOMAIN] and see your handiwork!


Official Docs

These are the ones I found helpful:

Dokku

BuildPacks

Packeto


Final Thoughts

There are a few ways in which this process could be better (I only learned Dokku even existed yesterday lol), the most prominent of which is deployment directly from Github. Thankfully, Dokku have you covered with a Github Action there, and there's a Gitlab option too 👍


The process should be reasonably adaptable to a React/Vue/Angular app - hopefully it helps someone out landing their first self-hosted PaaS!!

m@

Latest comments (1)

Collapse
 
jonasnobile profile image
Jonáš Nobile

Hi @emdienn , it should be enough to add "start": "node build" into your package.json instead of creating symlink.