Deploying a full-stack web app with a database and authentication

This guide is designed for any fullstack app with a database and Auth0 authentication built in, e.g. jwt-auth or a personal/final group project.

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

The Dockerfile configures how your app will be pushed to a Docker container on the server. Create a Dockerfile at the root of the codebase, and add this code into it.

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

Note the three lines beginning with ARG. These keywords tell Dokku that we are going to be passing in some values named VITE_AUTH0_AUDIENCE, VITE_AUTH0_DOMAIN, and VITE_AUTH0_CLIENT_ID, so that Auth0 will work properly once your app is deployed. Don't add the actual contents of the values in here - we will add them to Dokku later in this guide using the terminal.

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

Pass build args into Dokku

Remember those ARG properties from earlier in our Dockerfile? It's time to pass the values for those via Dokku into our Docker container.

Run these three commands in the terminal, separately. Replace $ENV_VALUE with each of the values from your .env file. Make sure you get these right and check them over very carefully before executing the command.

dokku docker-options:add build '--build-arg VITE_AUTH0_AUDIENCE=$ENV_VALUE'

dokku docker-options:add build '--build-arg VITE_AUTH0_DOMAIN=$ENV_VALUE'

dokku docker-options:add build '--build-arg VITE_AUTH0_CLIENT_ID=$ENV_VALUE'

You won't get any output back from these commands, but that's okay. Under the hood, Dokku is passing each of these values to the Docker container the app will be deployed in, so that the app can use them once it is deployed.

Pass config values into Dokku

The last step added these values to Dokku for the frontend Auth0 functionality. To make sure Auth0 works on the backend too, we need to pass the values to the Dokku server using the config command.

Run these three commands in the terminal, separately. Replace $ENV_VALUE with each of the values from your .env file. Make sure you get these right and check them over very carefully before executing the command.

dokku config:set VITE_AUTH0_AUDIENCE=$ENV_VALUE

dokku config:set VITE_AUTH0_DOMAIN=$ENV_VALUE

dokku config:set VITE_AUTH0_CLIENT_ID=$ENV_VALUE

These commands won't return any output either when you run them. Under the hood, Dokku sets the config options to include these .env values so that Auth0 will work on our server as well.

Run this command to check all of your values are correctly configured:

dokku config:show

You'll notice that some other values appear alongside the ones you have entered. These are some of the other things Dokku automatically passes to our app's container, such as the PORT and PROXY.

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 it's recommended to try running a production version of your app using the command NODE_ENV=production npm run build && npm run start. This will run the built production version of your app locally (on localhost:5173).

Make sure you've committed all files and changes. 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

SSL certificate

To serve your website over the HTTPS protocol, and ensure your authentication works, 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

Allowed callback uris

Make sure you log into your Auth0 tenant settings and add your deployed url to the list of allowed callback uris. Don't forget to save these changes!

Extra handy terminal commands (optional but useful!)

This one lets you rebuild a fresh version of your app on Dokku, once it's already deployed dokku ps:rebuild

This command lets you remove the config values and start again, if you enter them incorrectly dokku config:clear

This command runs a clean install of your npm packages npm ci