Quickstart Guide
Introduction
Architecture
Beszel consists of two main components:
- Hub: A web application that provides a dashboard for viewing and managing connected systems. Built on PocketBase.
- Agent: Runs on each system you want to monitor, creating a minimal SSH server to communicate system metrics to the hub.
Step 1: Create the Necessary Folders
If you have multiple nodes and a Docker Swarm environment, you can reference the GlusterFS guide for distributing folders across nodes.
mkdir -p /mnt/glustermount/data/beszel_data
Step 2: Docker Compose or Portainer for Initial Setup
You can either create a docker-compose.yaml
file manually or use Portainer to set up Beszel. Below is an example configuration that should work out of the box for a standalone Docker setup.
IMPORTANT: After you add a new system in the Beszel web UI, you must update the KEY
value with your public key (provided by the hub) and then restart the agent service. Also, use host.docker.internal
as the Host/IP when prompted, instead of localhost
or 127.0.0.1
.
Docker Standalone Example
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
extra_hosts:
- host.docker.internal:host-gateway
ports:
- 8090:8090
volumes:
- /mnt/glustermount/data/beszel_data:/beszel_data
beszel-agent:
image: henrygd/beszel-agent:latest
container_name: beszel-agent
restart: unless-stopped
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
# Do not remove quotes around the key
KEY: 'UPDATE WITH YOUR PUBLIC KEY (copy from "Add system" dialog)'
Docker Swarm + Traefik Example
version: "3.7"
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
networks:
- management_net
extra_hosts:
- host.docker.internal:host-gateway
ports:
- 8090:8090
volumes:
- /mnt/glustermount/data/beszel_data:/beszel_data
deploy:
mode: replicated
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.http.services.beszel-agent.loadbalancer.server.port=8090"
beszel-agent:
image: henrygd/beszel-agent:latest
container_name: beszel-agent
restart: unless-stopped
network_mode: host
depends_on:
- beszel_beszel
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
# Do not remove quotes around the key
KEY: 'UPDATE WITH YOUR PUBLIC KEY (copy from "Add system" dialog)'
deploy:
mode: replicated
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.http.services.beszel-agent.loadbalancer.server.port=45876"
networks:
management_net:
external: true
Why network_mode: host
?
The agent must use host network mode to access network interface metrics, which automatically exposes the port. If you do not need network statistics, you can remove network_mode: host
and map the port manually in the Compose file.
Step 3: Start the Containers
Once you've created your Compose file, you can deploy the services. The process differs slightly depending on your setup:
- Docker Swarm:
docker stack deploy -c docker-compose.yaml beszel
- Docker Standalone:
docker compose up -d
- Portainer: Use the Portainer UI to import the
docker-compose.yaml
file and start the stack.
Step 4: Create an Admin User
Open http://localhost:8090
(or your chosen URL/port) in your browser and follow the prompts to create an admin user.
Step 5: Adding Systems / Nodes
When you add a new system from the Beszel hub's web UI, you'll be given a snippet for the beszel-agent
configuration (Docker Compose or a binary install command). Copy the public key from the "Add system" dialog and place it in the KEY
variable of your agent container or service.
If you plan to monitor multiple nodes with Docker Swarm, you can create separate agent services, each running on a different node with constraints to ensure proper placement:
beszel-agent1:
image: henrygd/beszel-agent:latest
container_name: beszel-agent1
restart: unless-stopped
network_mode: host
depends_on:
- beszel_beszel
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
KEY: 'YOUR_PUBLIC_KEY_FROM_HUB'
deploy:
mode: replicated
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.http.services.beszel-agent1.loadbalancer.server.port=45876"
placement:
constraints:
- node.hostname == swarmpi1 # Only deploy on node 'swarmpi1'
beszel-agent2:
image: henrygd/beszel-agent:latest
container_name: beszel-agent2
restart: unless-stopped
network_mode: host
depends_on:
- beszel_beszel
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
KEY: 'YOUR_PUBLIC_KEY_FROM_HUB'
deploy:
mode: replicated
replicas: 1
labels:
- "traefik.enable=true"
- "traefik.http.services.beszel-agent2.loadbalancer.server.port=45876"
placement:
constraints:
- node.hostname == swarmpi2 # Only deploy on node 'swarmpi2'
Each agent service can then monitor the node on which it is running, allowing you to collect and consolidate metrics across your entire cluster.
start this wiki post with:
<h2 class="align-left"><a href="https://github.com/henrygd/beszel" target="_blank" rel="noopener"><img class="align-right" src="https://wiki.aeoneros.com/uploads/images/gallery/2025-01/scaled-1680-/814rhr5FohzOPEav-github-logo.png" alt="GitHub-logo.png" width="272" height="153"></a> <a href="https://hub.docker.com/r/henrygd/beszel-agent" target="_blank" rel="noopener"><img class="align-right" src="https://wiki.aeoneros.com/uploads/images/gallery/2024-09/scaled-1680-/rfuZed4CizAXYqTM-logo.png" alt="logo.png" width="187" height="167"></a></h2>
<h2 class="align-left"><a href="https://beszel.dev/" target="_blank" rel="noopener"><img src="https://wiki.aeoneros.com/uploads/images/gallery/2025-02/scaled-1680-/BLfnb6csoxA8Q8dk-2025-02-01-23-19-45-beszel-simple-lightweight-server-monitoring-brave.png" alt="2025-02-01 23_19_45-Beszel _ Simple, lightweight server monitoring - Brave.png" width="174" height="57"></a></h2>
<h2 id="bkmrk-live-demo" class="align-left">Introduction</h2>
<p id="bkmrk-beszel-summary"></p>
this is an formating example from a different setup guide step by step guide:
<p id="bkmrk-"><a href="https://wiki.aeoneros.com/ghcr.io/linkwarden/linkwarden:latest" target="_blank" rel="noopener"><img class="align-right" src="https://wiki.aeoneros.com/uploads/images/gallery/2024-09/scaled-1680-/rfuZed4CizAXYqTM-logo.png" alt="logo.png" width="163" height="145"></a></p>
<h2 id="bkmrk-introduction"><a href="https://github.com/linkwarden/linkwarden/" target="_blank" rel="noopener"><img class="align-right" src="https://wiki.aeoneros.com/uploads/images/gallery/2024-09/scaled-1680-/Ot77lQOoFS2b2cPK-github-logo.png" alt="GitHub-logo.png" width="231" height="130"></a>Introduction</h2>
<p id="bkmrk-in-this-guide%2C-we%27ll">In this guide, we'll walk through the process of installing Linkwarden, a self-hosted bookmark manager, on Docker Swarm. This installation will include setting up Traefik as the reverse proxy and configuring persistent storage using GlusterFS. Additionally, we'll cover how to securely set up environment variables, such as the NEXTAUTH_SECRET, and ensure proper file permissions using a custom Linux user.</p>
<p id="bkmrk-%C2%A0"></p>
<h4 id="bkmrk-step-1%3A-create-a-use">Step 1: Create a User for Linkwarden Folders</h4>
<p id="bkmrk-to-ensure-that-only-">To ensure that only one Linux user has the necessary rights for the Linkwarden folders, we'll create a custom user and group called <code>linkwardenuser</code>. This user will not have a home directory, and the shell will be set to <code>/usr/sbin/nologin</code> for security purposes.</p>
<ol id="bkmrk-create-the-custom-us">
<li>
<p>Create the custom user and group with the GID and UID set to 10002:<br></p>
<pre><code class="language-bash">sudo groupadd -g 10002 linkwardenuser
sudo useradd -u 10002 -g linkwardenuser -s /usr/sbin/nologin -M linkwardenuser</code></pre>
<ul>
<li>The <code>-s /usr/sbin/nologin</code> option ensures that this user cannot log in interactively, which is a security best practice for service users.</li>
<li>The <code>-M</code> option prevents the creation of a home directory, as this user will only be managing Linkwarden's folder permissions and not need a home directory for other purposes.</li>
</ul>
</li>
</ol>
<p id="bkmrk-%C2%A0-1"></p>
<h4 id="bkmrk-step-2%3A-create-folde">Step 2: Create Folders for Linkwarden</h4>
<p id="bkmrk-next%2C-we%E2%80%99ll-create-t">Next, we’ll create the necessary folders to store Linkwarden’s data on the GlusterFS mount.</p>
<ol id="bkmrk-create-the-folders-f">
<li>
<p>Create the folders for Linkwarden's data:<br></p>
<pre><code class="language-bash">mkdir -p /mnt/glustermount/data/linkwarden_data
mkdir -p /mnt/glustermount/data/linkwarden_data/pgdata
mkdir -p /mnt/glustermount/data/linkwarden_data/lwdata
mkdir -p /mnt/glustermount/data/linkwarden_data/storage</code></pre>
</li>
<li>Adjust ownership of these folders to the <code>linkwardenuser</code>, ensuring all files inside are accessible to this user:<br>This ensures that only the <code>linkwardenuser</code> has access to these folders and files, maintaining data security.<br>
<pre><code class="language-bash">sudo chown -R linkwardenuser:linkwardenuser /mnt/glustermount/data/linkwarden_data</code></pre>
<p><br></p>
</li>
<li><strong>Permissions</strong>: The following permissions ensure that the owner (user <code>10002</code>) has full access (read, write, execute) to the directories and files, and that no one else can modify the files.<br>
<pre><code class="language-bash">sudo chmod -R 750 /mnt/glustermount/data/linkwarden_data</code></pre>
<p class="callout info"><strong>750</strong> means:<br>- 7: Owner (user 10002) has read, write, and execute permissions.<br>- 5: Group has read and execute permissions (but not write).<br>- 0: Others have no permissions.<br></p>
<p data-pm-slice="0 0 []"><br></p>
<p><br></p>
</li>
</ol>
<h4 id="bkmrk-step-3%3A-create-docke">Step 3: Create <code>docker-compose.yaml</code></h4>
<p id="bkmrk-now%2C-we%E2%80%99ll-create-th">Now, we’ll create the Docker Compose file that will define two services: one for Linkwarden and another for its PostgreSQL database. We will also configure Traefik to route traffic to Linkwarden.</p>
<p id="bkmrk-to-configure-the-com" class="callout warning">To Configure the Composefile check out the <a href="https://wiki.aeoneros.com/books/linkwarden/page/enviroment-variables-list">ENVIROMENT-VARIABLES</a> Wiki Article.</p>
<pre id="bkmrk-version%3A-%223.5%22-servi"><code class="language-bash">version: "3.5"
services:
linkwarden:
image: ghcr.io/linkwarden/linkwarden:latest
environment:
- DATABASE_URL=postgresql://linkwarden:${POSTGRES_PASSWORD}@postgres:5432/linkwardendb
- NEXTAUTH_URL=http://localhost:3000/api/v1/auth
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- STORAGE_FOLDER=/mnt/glustermount/data/linkwarden_data/storage
- NEXT_PUBLIC_EMAIL_PROVIDER=${NEXT_PUBLIC_EMAIL_PROVIDER}
- EMAIL_FROM=${EMAIL_FROM}
- EMAIL_SERVER=${EMAIL_SERVER}
- BASE_URL=${BASE_URL}
- TZ=Europe/Zurich
- GID=10002
- UID=10002
restart: always
ports:
- 3000:3000
volumes:
- /mnt/glustermount/data/linkwarden_data/lwdata:/data/data
depends_on:
- postgres
networks:
- management_net
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
labels:
- "traefik.enable=true"
- "traefik.http.routers.linkwarden.rule=Host(`linkwarden.domain.tld`)"
- "traefik.http.services.linkwarden.loadbalancer.server.port=3000"
- "traefik.http.routers.linkwarden.entrypoints=websecure"
- "traefik.http.routers.linkwarden.tls.certresolver=leresolver"
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: linkwarden
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: linkwardendb
TZ: Europe/Zurich
GID: 10002
UID: 10002
restart: always
volumes:
- /mnt/glustermount/data/linkwarden_data/pgdata:/var/lib/postgresql/data
ports:
- 5432:5432
networks:
- management_net
deploy:
replicas: 1
placement:
constraints:
- node.role == manager
labels:
- "traefik.enable=true"
- "traefik.http.services.postgres.loadbalancer.server.port=5432"
networks:
management_net:
external: true
</code></pre>
<details id="bkmrk-smtp-settings-%23-the-">
<summary>SMTP Settings</summary>
<ul>
<li class="null"># The base URL of your Linkwarden installation (replace with your domain or local IP)<br>BASE_URL=https://linkwarden.domain.tld</li>
<li class="null"># Email provider for sending notification emails (example: SMTP settings)<br>NEXT_PUBLIC_EMAIL_PROVIDER=smtp</li>
<li class="null"># Email address that Linkwarden will use to send emails from (replace with your actual email)<br>[email protected]</li>
<li class="null"># SMTP server details (example: for Gmail's SMTP server)<br>EMAIL_SERVER=smtp://smtp.gmail.com:587<br></li>
</ul>
</details>
<p id="bkmrk-%C2%A0-2"></p>
<hr id="bkmrk--1">
<h4 id="bkmrk-step-4%3A-create-the-n">Step 4: Create the <code>NEXTAUTH_SECRET</code></h4>
<p id="bkmrk-the-nextauth_secret-">The <code>NEXTAUTH_SECRET</code> is used to sign authentication tokens securely. You need to generate a random string to be used as the <code>NEXTAUTH_SECRET</code>.</p>
<p id="bkmrk-you-can-generate-a-s">You can generate a secure <code>NEXTAUTH_SECRET</code> using the following command:<br></p>
<pre id="bkmrk-openssl-rand--base64"><code class="language-bash">openssl rand -base64 32</code></pre>
<p id="bkmrk-this-command-will-ge">This command will generate a 32-byte random string that you can add to your <code>.env</code> file as the <code>NEXTAUTH_SECRET</code>.</p>
<h5 id="bkmrk-what-does-nextauth_s">What Does <code>NEXTAUTH_SECRET</code> Do?</h5>
<p id="bkmrk-the-nextauth_secret--1">The <code>NEXTAUTH_SECRET</code> is critical for securing the authentication process in Linkwarden. It is used to sign and encrypt tokens, ensuring that user sessions are protected from tampering or unauthorized access.</p>
<p id="bkmrk-%C2%A0-3"></p>
<hr id="bkmrk--2">
<h4 id="bkmrk-step-5%3A-start-the-st">Step 5: Start the Stack</h4>
<p id="bkmrk-once-everything-is-c">Once everything is configured, you can start the Linkwarden stack either manually or through Portainer.</p>
<h5 id="bkmrk-start-with-docker-sw">Start with Docker Swarm</h5>
<p id="bkmrk-to-start-the-stack-m">To start the stack manually, run:<br></p>
<pre id="bkmrk-docker-stack-deploy-"><code class="language-bash">docker stack deploy -c docker-compose.yaml linkwarden</code></pre>
<h5 id="bkmrk-start-with-portainer">Start with Portainer</h5>
<p id="bkmrk-alternatively%2C-you-c">Alternatively, you can use Portainer’s graphical interface to import the <code>docker-compose.yaml</code> file and start the stack.</p>
<p id="bkmrk-once-the-stack-is-de" class="callout success">Once the stack is deployed, Linkwarden will be available at <code>https://linkwarden.domain.tld</code> (or whatever domain you've configured).</p>
<p id="bkmrk-%C2%A0-4"></p>
<hr id="bkmrk--3">
<h2 id="bkmrk-conclusion">Conclusion</h2>
<p id="bkmrk-this-guide-provides-">This guide provides a detailed walkthrough for setting up Linkwarden with Traefik on Docker Swarm. From user and folder management to Docker Compose configuration, these steps ensure a secure and scalable deployment of your self-hosted bookmark manager.</p>
now we write a guid on how to setup:
Introduction
Beszel consists of two main components: the hub and the agent.
Hub: A web application that provides a dashboard for viewing and managing connected systems. Built on PocketBase.
Agent: Runs on each system you want to monitor, creating a minimal SSH server to communicate system metrics to the hub.
https://pocketbase.io/
1. create necessary folders:
if you have multiple nodes and docker Swarm i reference here to my setup guide on how to setup GlusterFS https://wiki.aeoneros.com/books/glusterfs-keepalived-setup/chapter/glusterfs
mkdir /mnt/glustermount/data/beszel_data
2. go to Portainer or create a docker-compose.yaml for Initial setup
nano docker-compose.yaml
IMPORTANT
This configuration should work out of the box, but you must follow these steps when adding the system in the web UI:
Update the KEY value with your public key, then run docker compose up -d again to restart the agent.
Use host.docker.internal als the Host / IP. Do not use localhost or 127.0.0.1.
Docker Standalone:
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
extra_hosts:
- host.docker.internal:host-gateway
ports:
- 8090:8090
volumes:
- /mnt/glustermount/data/beszel_data:/beszel_data
beszel-agent:
image: henrygd/beszel-agent:latest
container_name: beszel-agent
restart: unless-stopped
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
# Do not remove quotes around the key
KEY: 'UPDATE WITH YOUR PUBLIC KEY (copy from "Add system" dialog)'
Docker Swarm + Traefik
version: "3.7"
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
networks:
- management_net
extra_hosts:
- host.docker.internal:host-gateway
ports:
- 8090:8090
volumes:
- /mnt/glustermount/data/beszel_data:/beszel_data
deploy:
mode: replicated
replicas: 1
labels:
- 'traefik.enable=true'
- 'traefik.http.services.beszel-agent.loadbalancer.server.port=8090'
beszel-agent:
image: henrygd/beszel-agent:latest
container_name: beszel-agent
restart: unless-stopped
network_mode: host
depends_on:
- beszel_beszel
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
# Do not remove quotes around the key
KEY: 'UPDATE WITH YOUR PUBLIC KEY (copy from "Add system" dialog)'
deploy:
mode: replicated
replicas: 1
labels:
- 'traefik.enable=true'
- 'traefik.http.services.beszel-agent.loadbalancer.server.port=45876'
networks:
management_net:
external: true
Why host network mode?
The agent must use host network mode to access network interface metrics, which automatically exposes the port. Change the port using an environment variable if needed.
If you don't need network stats, you can remove that line from the compose file and map the port manually.
3. start container with docker initial start
with Swarm:
docker stack deploy -c docker-compose.yaml beszel
or docker standalone:
docker compose up -d
or portainer start the stack
4. Open http://localhost:8090 and create an admin user.
5. Adding Systems / Nodes
The docker-compose.yml or binary install command is provided for copy/paste in the hub's web UI when adding a new system.
So if youre using docker swarm with traefik again you can just adjust your docker compose by the following example:
This is just the Service of the beszel-agent you Need to adjust:
Paste the SSH-Key that was generated in the Web-UI to the KEY Section.
The Deploy Mode Replicated 1 says that the container only should run on 1 node of our swarm cluster.
Now an Important one:
You want your different agents running on different nodes. so we Need to define them in the deploy section and create a constraint so this specific container only starts on this node.
Example:
beszel-agent1:
image: henrygd/beszel-agent:latest
container_name: beszel-agent1
restart: unless-stopped
network_mode: host
depends_on:
- beszel_beszel
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
# Do not remove quotes around the key
KEY: 'UPDATE WITH YOUR PUBLIC KEY (copy from "Add system" dialog)'
deploy:
mode: replicated
replicas: 1
labels:
- 'traefik.enable=true'
- 'traefik.http.services.beszel-agent1.loadbalancer.server.port=45876'
placement:
constraints:
- node.hostname == swarmpi1 # Ensures deployment on the node with the label swarmpi1
beszel-agent2:
image: henrygd/beszel-agent:latest
container_name: beszel-agent2
restart: unless-stopped
network_mode: host
depends_on:
- beszel_beszel
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
PORT: 45876
# Do not remove quotes around the key
KEY: 'UPDATE WITH YOUR PUBLIC KEY (copy from "Add system" dialog)'
deploy:
mode: replicated
replicas: 1
labels:
- 'traefik.enable=true'
- 'traefik.http.services.beszel-agent2.loadbalancer.server.port=45876'
placement:
constraints:
- node.hostname == swarmpi2 # Ensures deployment on the node with the label swarmpi2