Self-Host Like a Pro: Dokku, Hetzner and Cloudflare

Setting up a Hetzner VPS

Let's start by creating a new Hetzner VPS.

If you don't have an account, you can use my referral link to sign up as a thank you.

The first step is to choose your machine's location and operating system. In my case, I picked:

  • Location: Nuremberg (eu-central)
  • OS: Debian

The reason for choosing Debian is the unattended upgrades. That means it keeps your machine up-to-date with the latest security updates automatically.

The next step involves choosing a machine type.

I picked the cheapest instance (CX22) since it's more than enough for the apps I intend to deploy. It gives you:

  • vCPUs: 2 (Intel)
  • RAM: 4 GB
  • SSD: 40 GB

You can always start with a modest VPS and upgrade later if needed.

For the networking part, leave the default selections.

Don't disable IPv4 if you don't want to run into issues with websites that don't support IPv6.

Now it's time to configure the SSH key for your server. Using an SSH key instead of credentials to log into your server is highly advisable since it's more secure.

Slight Detour: Generating SSH Keys

Generate an SSH key using the Ed22519 algorithm as follows:

ssh-keygen -t ed25519 -C "<your-email-address>"

Once you run this command, it'll prompt you to enter the file in which to save the key.

Using the default file id_ed25519 will overwrite the previous content, and you may run into trouble with SSH authentication for other clients.

To avoid that, I save my keys in different files:

Enter file in which to save the key (/Users/<your-username>/.ssh/id_ed25519): /Users/<your-username>/.ssh/vps_selfhosting

After specifying the file in which to save the key, you need to enter a passphrase. Although you can continue without one, I recommend that you add one.

Once you add it, the SSH setup is complete.

For more information about setting up SSH keys, check out this article from GitHub.

Slight Detour: Adding SSH Keys to a Hetzner VPS

You now need to add the public key to your Hetzner VPS. You can copy it as follows:

cat .ssh/vps_selfhosting.pub | pbcopy

Return to the VPS configuration page and paste the key.

You're almost done with the server configuration. Leave the "Volumes", "Firewalls", and other sections untouched. You can and will configure some of them later.

Important: Don't blindly copy my choices. Use the options that fit your needs and requirements.

SSH Into VPS

You can now SSH into your VPS as follows:

ssh root@<your-vps-ip> -i ~/.ssh/vps_selfhosting

However, having to provide the SSH key every time you want to access your VPS is annoying.

Add the key to the SSH agent:

ssh-add ~/.ssh/vps_selfhosting

Now you can run ssh root@<your-vps-ip.

Install and Configure Dokku on Hetzner

Most of the following commands and instructions are from the Dokku documentation. If you prefer it, follow the documentation.

SSH into your VPS:

ssh root@<your-vps-ip>

Get the latest updates and packages for your operating system:

sudo apt update -y
sudo apt upgrade -y

Then, install Dokku:

wget -NP . https://dokku.com/install/v0.35.18/bootstrap.sh
sudo DOKKU_TAG=v0.35.18 bash bootstrap.sh

Once the installation finishes, copy the SSH keys to the Dokku admin so you can push from your local machine:

cat ~/.ssh/authorized_keys | dokku ssh-keys:add admin

Global Domain

Optionally, you can set up a global domain for Dokku:

dokku domains:set-global <your-domain-or-subdomain>

In case you don't want to use a domain, you can also use the IP of your server:

dokku domains:set-global <your-ip>

If you set up a global domain, all your deployed apps will be created on a subdomain:

  • for example, an app named expense-tracker will be deployed on expense-tracker.global-domain.com

This is handy when hosting multiple applications under the same domain. You don't need to set up a global domain if you want to host only one app on your VPS.

Add Your Hetzner VPS to Cloudflare

Create an A record in your Cloudflare account that points to your server's IP.

Also, set the "Proxy status" to "Proxied". That proxies your traffic through Cloudflare, which provides benefits such as:

  • protecting your server from DDoS attacks
  • caching and optimizing the requests to your server

Cloudflare-Only Access on Port 80

You want to make sure that your server is accessible on port 80 only by Cloudflare. This makes your VPS more secure, since only Cloudflare can access port 80 on your VPS, which Dokku uses.

To add a Firewall, navigate to the "Firewalls" section on your VPS page and click the "Create Firewall" button.

Then add all the IPs from the "IP Ranges" page and click the "Create Firewall" button.

Important: Make sure that your server is selected in the "Apply to" section.

Note: my app wasn't accessible after setting up HTTPS with Let's Encrypt until I did the same thing for port 443.

You can take it further and add an Inbound rule for port 22 (SSH connections) too.

That means, only the specified IPs (your machine) can SSH into the VPS. Or even better, you can change the default SSH port from "22" to something else.

Your VPS is in good shape now, so it's time to deploy an app to Dokku.

Thanks to Ben for these tips on hardening your VPS.

Deploy App to Dokku with PostgreSQL Database

SSH into your VPS:

ssh root@<your-vps-ip>

Create the application with Dokku:

dokku apps:create <your-app-name>

To create and host a PostgreSQL database on Dokku, you need to install the PostgreSQL plugin:

sudo dokku plugin:install https://github.com/dokku/dokku-postgres.git

Then create the database:

dokku postgres:create <your-db-name>

A successful database creation outputs the database connection string. However, you can link a database and an app with a dokku command:

dokku postgres:link <your-db-name> <your-app-name>

Your app and database are now linked, but you still don't have a deployed app.

Local Machine Configuration

Navigate to the app directory on your local machine, and add a git remote for Dokku:

git remote add dokku dokku@<your-vps-ip>:<your-dokku-app-name>
Your pushes and deployments will fail if the remote username isn't dokku.

You're done. You can now deploy your app with the following command:

git push dokku main

As the image below shows, the deployment fails if the application requires environment variables to run.

You need to set all the required environment variables on Dokku as well.

Setting Environment Variables on Dokku

You can set multiple environment variables on Dokku using the config:set command. Here's an example of setting multiple env variables for my app:

dokku config:set <your-app-name> ENV=prod \
PORT=<your-app-port> \
DATABASE_URL=<your-db-url> \
NODE_ENV=production \
BETTER_AUTH_SECRET=<better-auth-secret> \
BETTER_AUTH_URL=<better-auth-url> \
GITHUB_CLIENT_ID=<github-client-id> \
GITHUB_CLIENT_SECRET=<github-client-secret> \
GOOGLE_CLIENT_ID=<github-client-id> \
GOOGLE_CLIENT_SECRET=<github-client-secret> \
POLAR_ACCESS_TOKEN=<polar-access-token> \
POLAR_PRODUCT_ID=<polar-product-id> \
POLAR_WEBHOOK_SECRET=<polar-webhook-secret> \
RESEND_KEY=<resend-key>

Once you run the command, Dokku restarts your application. If you want to avoid that, you can use the --no-restart flag.

Tip: To retrieve the PostgreSQL database connection string, run dokku postgres:info <your-db-name> on your VPS.

Set the App Domain

If you set up a global domain earlier, Dokku will create a subdomain on the global domain.

Whether you want that or not, it's up to you. In this case, we'll host only one app on this VPS, so we'll clear the global domain and use it for the app deployed.

Remove any domains set for your app and the global domain as follows:

dokku domains:clear <your-app-name>
dokku domains:clear-global  

Then, link the domain to the deployed app:

dokku domains:add <your-app-name> <your-domain>

Your app will now be accessible at the specified domain.

Optional: Set the App Port

When you deploy your apps with custom ports (e.g. 5678), Dokku doesn't automatically create and map the ports.

If you try to access the freshly deployed app via the URL returned by Dokku, you'll likely run into an error.

You can fix that by mapping the port from your VPS to a port inside your Docker container for the application:

dokku ports:set <you-app> http:80:<your-port>

This command sets up a rule so that all incoming HTTP requests to your server's port 80 are forwarded to port 5678 inside the <app-name> container.

Configure SSL with Let's Encrypt

Let's Encrypt offers free SSL certificates, so we'll use it to set up HTTPS for your app.

Install the Let's Encrypt plugin for Dokku:

sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

Then, set up the email address where you want to receive updates and notifications from Let's Encrypt:

dokku letsencrypt:set --global email <your-email>

Enable Let's Encrypt for your app:

dokku letsencrypt:enable <your-app-name>

Once the command finishes running, all that's left is to set up auto-renewal for the certificate:

dokku letsencrypt:cron-job --add

This command keeps your certificate up-to-date by setting up a cron job that renews it automatically.

Your app should now be accessible via HTTPS. Since you're using Cloudflare, HTTP should also automatically redirect to HTTPS.

Automatic Dokku Deployments via GitHub Actions

Deploying your app manually with the git push dokku main command can get tiring.

Thankfully, you can use GitHub Actions to automate the deployment of your apps. GitHub Actions will deploy your app on Dokku whenever you push to the main branch.

Create the .github/workflows/deploy-to-dokku.yml file in the root of your project and add the following code:

name: Deploy to Dokku

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-24.04
    steps:
      - name: Cloning repo
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Push to dokku
        uses: dokku/github-action@master
        with:
          branch: "main"
          git_remote_url: "ssh://dokku@${{ secrets.DOKKU_SERVER_IP }}:22/${{ secrets.DOKKU_APP_NAME }}"
          ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}

The branch property under with allows you to specify a custom deploy branch other than master.

Additionally, you can change the deploy branch with this dokku command:

# for a specific app
dokku git:set <your-app-name> deploy-branch <your-branch-name>

# globally
dokku git:set --global deploy-branch <your-branch-name>

If the branch names don't match, Dokku won't deploy your app. You'll likely get an error like the one in the screenshot below:

Going forward, you need to set up SSH authentication like you did for your personal machine previously for GitHub Actions to be able to deploy your app.

Generate an SSH key for GitHub Actions:

ssh-keygen -t ed25519 -C "<your-email-address>" -f ~/.ssh/dokku_gh_actions_rsa

Copy the private key:

cat ~/.ssh/dokku_gh_actions_rsa | pbcopy

Then, navigate to github.com/<your-username>/<your-app-name>/settings/secrets/actions.

On this page, add the following secrets:

  • SSH_DOKKU_PRIVATE_KEY - add the previously copied private key.
  • DOKKU_SERVER_IP - the IP of your Hetzner VPS
  • DOKKU_APP_NAME - your app name from Dokku

The last step is to add the public key to your Dokku server as follows:

cat ~/.ssh/dokku_gh_actions_rsa.pub | ssh root@<your-server-ip" "sudo dokku ssh-keys:add GITHUB_ACTIONS"

Note: If you add a passphrase for this key, you may need to do extra steps.

From now on, every time you push to master or the main branch (or whatever branch you chose), it'll trigger a Dokku deployment.

Extra Resources

I've used the documentation in combination with a couple of articles to put this guide together. They are very good and I recommend checking them out:

Pay what you like 🧡

If you like this content and it helped you, please consider supporting this blog.

Make a one-time donation 💫