Deploying a full-stack web app with a database

This guide is specifically for deploying node apps that have a react/SPA frontend and an API express backend with an sqlite database, e.g. dreamfest

Make sure you have completed dokku user setup first.

Remove hard-coded values

Dokku sets the environment variable PORT to tell the web app what port to listen on.

So, instead of just hard-coding 3000, we want to do something like this:

const port = process.env.PORT || 3000
server.listen(port, () => {
console.log(`listening on port ${port}`)
})

App contains no references to localhost

If your api-client is making requests to http://localhost, this will not work in production. Requests to your api should start with / to be relative to the host serving your static assets.

Packages you need while the app is running should be in dependencies

Ensure that all required packages are in the dependencies part of your package.json. Dokku will remove everything in devDependencies before it runs your app.

If a package is working globally on your machine you may have forgotten to add it to your project explicitly with npm install <package name>, which means it will not be installed for the deployed version.

The start script calls node directly

Dokku will use npm run start to start your app, so it should typically call node on your pre-built backend code.

"start": "node dist/server.js",

The build script compiles both the frontend and the backend

Dokku will use the build script i.e. npm run build, to compile your application.

Our default build scripts use vite to build the client and esbuild for the server.

"build": "run-s build:client build:server",
"build:client": "vite build",
"build:server": "esbuild --packages=external --platform=node --format=esm --outfile=dist/server.js --bundle server/index.ts",

make sure these packages are in your devDependencies: vite, esbuild, and npm-run-all

Commit your package-lock.json

Dokku will use npm ci, this only works if you have a package-lock.json

If you don't have a package-lock file already you can generate one with npm install --package-lock-only.

Make sure this file is committed.

Check that all build products are ignored by git

You should have a .gitignore in the root of your project, this should include the node_modules folder and the dist folder where your assets will be built to

/node_modules
/dist

If you have accidentally committed either of these folder, git rm -r them and commit the change.

Serve static assets in production

We use vite to serve static assets in development, in production we'll use our express app. These lines will set us up to do exactly that, but only in production.

Since we have a wildcard (*) serving index.html this if-block must appear after your API routes.

import * as Path from 'node:path'
if (process.env.NODE_ENV === 'production') {
server.use(express.static(Path.resolve('public')))
server.use('/assets', express.static(Path.resolve('./dist/assets')))
server.get('*', (req, res) => {
res.sendFile(Path.resolve('./dist/index.html'))
})
}

Add a Procfile

A Procfile tells Dokku how to start different types of process. In our case we're defining two proc types, web to start our app and release to run our migrations

web: npm run start
release: npm run knex migrate:latest

Add a Dockerfile

Dokku can use docker to set up a virtual machine to run your application. To configure this create a file in the root of your repo called Dockerfile with these contents.

This needs to be called exactly Dockerfile with a capital "D"

FROM node:20-alpine
WORKDIR /app
COPY ["package.json", "package-lock.json*", "./"]
RUN npm ci
COPY . .
ENV NODE_ENV=production
RUN npm run build --if-present
RUN npm prune --omit=dev

Configuring your production database

In your knexfile, you can configure the production to use a location in /app/storage.

It should look like this.

production: {
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: '/app/storage/prod.sqlite3',
},
pool: {
afterCreate: (conn, cb) => conn.run('PRAGMA foreign_keys = ON', cb),
},
},

Creating your dokku app

Choose a name that is likely to be unique, e.g. you can put your name and cohort at the start gerard-kahu24-worldwide-routing

Note that the name should only be hyphens and lowercase letters, and cannot include any underscores ('_').

Run the apps:create command like this, replacing $APP_NAME with the name you've chosen.

dokku apps:create $APP_NAME

This will create an app on the Dokku serve and automatically add it as a remote in your local git repo.

Check that this remote has been added with git remote -v

Keep in mind your origin remote will typically be the github repo, and pushing to the origin will not update your deployed website, and pushing to the dokku remote will not update your github repo.

Mounting storage for our database

To keep our data between deploys, we need to have our database file outside of the application container, but we need to mount it to the container so that our application can use it.

Run this command to ensure that a storage directory exists for your app (don't forget to to replace $APP_NAME with the full name of your app).

dokku storage:ensure-directory $APP_NAME-storage

... then run this command to mount it for your app's use

dokku storage:mount /var/lib/dokku/data/storage/$APP_NAME-storage:/app/storage

Lastly, check the list of storage folders mounted for your the app. There should be only one item in the list returned.

dokku storage:list

Secrets

If you're using environment variables to pass api keys or other secrets to the backend, you might be using a .env file during development.

In production we will set the values with dokku config:set, for example:

dokku config:set API_KEY='klnaksdfu12831123a' CLIENT_SECRET='1234'

Deploying your app

NOTE: Dokku only has a main branch. so if you're deploying a local branch other than main, you must specify which branch you're deploying with:

Before you push, make sure that you've committed all the new files and changes you've made.

If you run git status, it should look like this (you might not be on the main branch)

On branch main
nothing to commit, working tree clean

Run this command, replacing $LOCAL_BRANCH with the name of the branch you want to deploy

git push dokku $LOCAL_BRANCH:main

After the usual git messages, you'll see the output of dokku trying to build and deploy your app.

Finally, it will log out the url to your deployed web app!

Running your seeds

Your migrations will run as part of the release phase (in your Procfile) however you will need to run your seeds manually.

You can use dokku run to run commands in your app container.

dokku run npm run knex seed:run

Optional: SSL certificate

To serve your website over the HTTPS protocol, you need an SSL certificate. Let's Encrypt is a service that provides certificates for free. We can use them in dokku with a plugin

Run this command in your repo:

dokku letsencrypt:enable