Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions tutorials/install-peon-on-hetzner-cloud/01.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
---
SPDX-License-Identifier: MIT
path: "/tutorials/install-peon-on-hetzner-cloud"
slug: "install-peon-on-hetzner-cloud"
date: "2026-09-18"
title: "Install Peon on Hetzner Cloud (self-hosted Docker PaaS)"
short_description: "Install the free, open-source Peon control plane on Ubuntu in Hetzner Cloud, connect the host, and deploy an example Docker app."
tags: ["Ubuntu", "Docker", "Self-hosting"]
author: "PrinceAdvant"
author_link: "https://github.com/PrinceAdvant"
author_img: "https://github.com/PrinceAdvant.png"
author_description: "Cloud and self-hosting enthusiast focused on practical, reproducible deployments."
language: "en"
available_languages: ["en"]
header_img: "header-x"
cta: "product"
---

## Introduction

This tutorial shows how to run the free, open-source [Peon](https://peon.sh/) control plane on a fresh Ubuntu server in Hetzner Cloud. Peon is a self-hosted Docker platform that manages servers and deploys applications from a web dashboard.

By the end, you will have an Ubuntu CX server with Docker, a local PostgreSQL database, and Peon running on the server. You will add the server to Peon and deploy the official `nginx:alpine` image as a sample application. This uses the self-hosted path and does not require a Peon Cloud subscription.

See the official [Peon documentation](https://peon.sh/docs), the [self-hosting guide](https://peon.sh/docs/installation), and [Peon-sh/Peon on GitHub](https://github.com/Peon-sh/Peon).

**Prerequisites**

A Hetzner Cloud project, an SSH key in the [Cloud Console](https://console.hetzner.cloud/), and a local terminal with SSH access. A domain is optional, but recommended for HTTPS in production.

Use these placeholders and replace them with your own values: Server `<server_ip>`, hostname `<your_host>`, Linux user `holu`, and initial dashboard URL `http://<server_ip>:3000`.
## Step 1 - Create an Ubuntu server

Create a server in the [Hetzner Cloud Console](https://console.hetzner.cloud/). Select Ubuntu 24.04, an `CX` server type suitable for your workload, a location, and the SSH key you will use to log in. A server with at least 2 vCPUs and 4 GB of RAM gives the dashboard, database, and a small example container room to run together.

Set the hostname to `<your_host>` if you want to identify the server easily. Copy its public IPv4 address and connect as `root`:

```bash
ssh root@<server_ip>
```

Keep this root session open until you have tested the `holu` account.

## Step 2 - Configure the firewall

Update the system and install basic tools. The firewall allows SSH and the web ports used by the Peon gateway. Port 3000 is temporary so you can test the dashboard before putting it behind HTTPS.

```bash
apt update && apt upgrade -y
apt install -y ca-certificates curl git tmux ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw allow 3000/tcp comment 'Peon dashboard temporary'
ufw enable
ufw status verbose
```

Do not close the current SSH session until `ufw status` confirms port 22 is allowed. After configuring a domain and HTTPS, remove port 3000 with `ufw delete allow 3000/tcp`.

Create a non-root account for Peon:

```bash
adduser --disabled-password --gecos '' holu
usermod -aG sudo holu
install -d -m 700 -o holu -g holu /home/holu/.ssh
cp /root/.ssh/authorized_keys /home/holu/.ssh/authorized_keys
chown holu:holu /home/holu/.ssh/authorized_keys
chmod 600 /home/holu/.ssh/authorized_keys
```

Open a second terminal and test the account:

```bash
ssh holu@<server_ip>
sudo -v
```

## Step 3 - Install Docker

Install Docker from Ubuntu packages and enable it at boot. Add `holu` to the Docker group so Peon can manage containers over SSH.

```bash
sudo apt install -y docker.io docker-compose-v2
sudo systemctl enable --now docker
sudo usermod -aG docker holu
```

Log out of the second SSH session and reconnect so the group membership is active. Verify Docker with the official test image:

```bash
docker run --rm hello-world
```

The command should print a success message and remove its container.

## Step 4 - Install the Peon control plane

The Peon self-hosting guide requires Node.js 20 or newer, pnpm, and PostgreSQL. Install them with the build tools:

```bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs postgresql build-essential
sudo npm install --global pnpm
node --version
pnpm --version
```

Create a PostgreSQL database and user. Choose a long password and use the same value in the connection string below.

```bash
sudo -u postgres psql
```

At the `psql` prompt, replace the sample password before pressing Enter:

```sql
CREATE USER peon WITH PASSWORD 'replace-with-a-long-password';
CREATE DATABASE peon OWNER peon;
\q
```

Clone the official repository and install its dependencies:

```bash
sudo git clone https://github.com/Peon-sh/Peon.git /opt/Peon
sudo chown -R holu:holu /opt/Peon
cd /opt/Peon
cp .env.example .env
pnpm install
```

Open `.env` and set the database connection, dashboard URL, and every required secret documented in the file. For the initial test:

```dotenv
DATABASE_URL=postgresql://peon:replace-with-a-long-password@127.0.0.1:5432/peon
APP_URL=http://<server_ip>:3000
```

Do not commit `.env` or share its values. Use long, unique random values for the secrets in `.env.example`. If you use a domain, set `APP_URL` to its HTTPS URL and follow the [Peon domain and SSL documentation](https://peon.sh/docs/domains-and-ssl).

Apply migrations and build Peon:

```bash
pnpm prisma migrate deploy
pnpm build
```

Start the web process and worker in separate `tmux` sessions:

```bash
tmux new -s peon-web
pnpm start
```

Detach with `Ctrl+B`, then `D`. Start the worker:

```bash
tmux new -s peon-worker
cd /opt/Peon
pnpm worker
```

Detach again with `Ctrl+B`, then `D`. For production, run both processes under a service manager and put the dashboard behind HTTPS as described in the official [Peon self-hosting documentation](https://peon.sh/docs/installation).

## Step 5 - Open Peon and add the server

Open `http://<server_ip>:3000` in your browser. Register the first account if enabled, then create a workspace when prompted.

Open **Keys & Tokens** and create or import an SSH key that Peon can use. Next open **Infrastructure**, choose **Servers**, and select **Add server**. Use the name `<your_host>`, host `<server_ip>`, SSH port `22`, SSH user `holu`, and the key you added under **Keys & Tokens**.

Save the server and run validation. Docker is already installed and `holu` can use it, so validation should confirm the connection and Docker availability. If it fails, check the second SSH test, the key in `/home/holu/.ssh/authorized_keys`, and the Docker group in the SSH session.

See Peon's [Managing Servers documentation](https://peon.sh/docs/servers) for the current dashboard flow.

## Step 6 - Deploy a sample Docker application

Create a project in the Peon dashboard and add a service. Choose **Docker Image**, select the server, and use the trusted official image `nginx:alpine`. Set the container port to `80`.

For a public deployment, configure a domain that points to `<server_ip>` and add it to the service. Peon's gateway can route the domain to the container and obtain HTTPS when the DNS record is ready. Without a domain, deploy first and use the service logs and deployment status to verify that the container starts.

Click **Deploy** and wait for the deployment to finish. Open the service URL, or test from the server in a temporary local setup:

```bash
curl -I http://127.0.0.1
```

The response from Nginx confirms that Peon created and started the sample container. The **Deployments** and **Logs** views show the image pull, container status, and application output.

## Step 7 - Finish the setup securely

After confirming that the dashboard and sample service work, point a DNS record at the server and set an HTTPS `APP_URL` for the dashboard. Remove the temporary dashboard rule with `sudo ufw delete allow 3000/tcp` after HTTPS is configured. Replace the example PostgreSQL password if it was used elsewhere. Move the web and worker processes from `tmux` into a service manager so they restart after a reboot. Keep Ubuntu, Docker, Peon, and the sample image updated.

## Conclusion

You created an Ubuntu CX server, applied a restrictive firewall, installed Docker, and ran the self-hosted Peon control plane using its official open-source repository. You also added the server to Peon and deployed an Nginx container through the dashboard. From here, connect a Git source or use another Docker image by following the [Peon documentation](https://peon.sh/docs).

**Next steps:**

Peon [documentation](https://peon.sh/docs), the [self-hosting guide](https://peon.sh/docs/installation), [Peon-sh/Peon source code](https://github.com/Peon-sh/Peon), and the [Hetzner Cloud Console](https://console.hetzner.cloud/).

##### License: MIT

<!--

Contributor's Certificate of Origin

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I have
the right to submit it under the license indicated in the file; or

(b) The contribution is based upon previous work that, to the best of my
knowledge, is covered under an appropriate license and I have the
right under that license to submit that work with modifications,
whether created in whole or in part by me, under the same license
(unless I am permitted to submit under a different license), as
indicated in the file; or

(c) The contribution was provided directly to me by some other person
who certified (a), (b) or (c) and I have not modified it.

(d) I understand and agree that this project and the contribution are
public and that a record of the contribution (including all personal
information I submit with it, including my sign-off) is maintained
indefinitely and may be redistributed consistent with this project
or the license(s) involved.

Signed-off-by: PrinceAdvant <PrinceAdvant@users.noreply.github.com>

-->