Homelab Post Tag - TechOpt.io Programming, servers, Linux, Windows, macOS & more Tue, 10 Nov 2026 01:44:16 +0000 en-US hourly 1 https://wordpress.org/?v=7.1.4 https://www.techopt.io/wp-content/uploads/2024/07/cropped-logo-1-32x32.png Homelab Post Tag - TechOpt.io 32 32 My iCloud Photos Alternative: Setting Up Immich on Proxmox https://www.techopt.io/servers-networking/my-icloud-photos-alternative-setting-up-immich-on-proxmox https://www.techopt.io/servers-networking/my-icloud-photos-alternative-setting-up-immich-on-proxmox#respond Tue, 10 Nov 2026 01:43:12 +0000 https://www.techopt.io/?p=1543 I was tired of paying Apple every month just to keep storing my photos. I already had a home server, so I started looking for an iCloud Photos alternative that would automatically back up pictures from my iPhone without sending everything to another paid cloud service. That’s how I ended up using Immich. Immich now […]

The post My iCloud Photos Alternative: Setting Up Immich on Proxmox appeared first on TechOpt.

]]>
I was tired of paying Apple every month just to keep storing my photos. I already had a home server, so I started looking for an iCloud Photos alternative that would automatically back up pictures from my iPhone without sending everything to another paid cloud service. That’s how I ended up using Immich.

Immich now fills that role for me: its iOS app uploads photos to my server, and I browse them from my phone or a browser. Here’s how I set it up on Proxmox, without getting buried in infrastructure details.

TL;DR: I use Immich to automatically back up my iPhone photos to a NAS through a Debian container on Proxmox. It’s a strong iCloud alternative if you already manage a home server. The official Docker Compose guide is a good starting point.

Why I Wanted an Alternative to iCloud Photos

iCloud worked fine for me. Its iPhone integration and syncing across Apple devices are convenient. I just didn’t want to keep paying for storage when I already owned hardware capable of keeping my photo library.

I also didn’t want to manually export photos. Automatic iPhone photo backup was essential, not just a nice web gallery.

Immich combines that backup functionality with a familiar timeline, albums, search, sharing, and facial recognition. More importantly, the photos can live on storage I control.

Immich desktop web interface showing the photo library and timeline
Immich’s desktop interface offers a familiar photo library experience, accessible directly from any web browser.

That doesn’t mean self-hosting is free. The server, disks, electricity, maintenance, and reliable backups all have costs. The appeal for me is using infrastructure I already have and retaining more control over the data.

Using Immich as My iPhone Photo Backup App

This is the part that makes Immich feel like a genuine iCloud Photos replacement in day-to-day use.

I use the Immich iOS app as my photo backup app. After connecting it to my server and enabling backup, the app uploads photos and videos from the albums I’ve selected. When I open it later, I can see the pictures in a date-based timeline. I can also access the same server-side library from a web browser.

Immich iOS app displaying a photo timeline as an iCloud Photos alternative
The Immich iOS app lets me browse and automatically back up my iPhone photos to my own server.

It also supports search, facial recognition, albums, and Live Photos, as described on the Immich features page. But automatic uploads are what make it useful for me.

Immich desktop map view displaying photos organized by their GPS locations
Immich’s interactive map lets me explore my photo library by location, using GPS data embedded in my photos.

One Important Difference From iCloud

Immich’s mobile backup is not identical to Apple’s two-way iCloud Photos sync. Immich uploads selected photos and videos to your server; it doesn’t replace the built-in iOS Photos system or reproduce every Apple integration. Its optional album synchronization is also one-way, from the phone to the server.

iOS controls when third-party apps run in the background. Immich uploads when opened and periodically in the background, but it can’t promise immediate uploads. Background App Refresh and opening the app regularly help.

In other words, it’s the alternative I use for backing up my iPhone photos, not a claim that the two products behave identically. Immich explains the behavior in its mobile backup documentation.

My Immich Setup on Proxmox

Since I already use Proxmox in my homelab, I didn’t need a dedicated computer for Immich. I gave it its own Debian LXC container and installed the official Docker Compose deployment inside it.

My configuration is the following:

ComponentMy setup
Host platformProxmox
Guest operating systemDebian 13 LXC container
Resources4 CPU cores and 8 GB RAM
Container disk64 GB ZFS-backed root filesystem
Immich deploymentOfficial Docker Compose files in /opt/immich
Photo and video storageNAS share mounted at /mnt/photos using NFS
DatabasePostgreSQL files stored locally at /opt/immich/postgres
Secure accessHTTPS handled by HAProxy on my firewall

Photos stay on the NAS, while PostgreSQL stays on the container’s disk. Immich explicitly advises against putting the database on a network share.

Proxmox LXC container running Immich with four CPU cores and 8 GB RAM
My Immich server running in a Debian LXC container on Proxmox, with 4 CPU cores and 8 GB of RAM.

One caveat: my container is privileged, and Docker inside Proxmox LXC is an advanced setup. Although this arrangement runs in my homelab, Immich recommends a full virtual machine rather than Docker in LXC. If you’re starting from scratch and want the more straightforward supported route, use a Debian VM. I’ve also written about the differences between LXC containers and virtual machines in Proxmox.

How to Set Up Immich on Proxmox

Here’s the simplified setup, assuming you can create a Debian guest in Proxmox and access its shell.

1. Create a Debian Guest and Install Docker

Create a network-connected Debian guest. My instance has four CPU cores and 8 GB RAM, matching Immich’s recommended resources.

Install Docker Engine and Compose using the official Debian instructions. I prefer the official Compose deployment to a community installation script.

Docker in LXC may require extra Proxmox configuration; a VM is simpler.

2. Decide Where Your Photos Will Live

You can use local storage, but I mounted my NAS share at /mnt/photos.

For an illustrative NAS at 10.0.0.100, the steps look like this:

sudo apt update
sudo apt install nfs-common
sudo mkdir -p /mnt/photos
sudo mount -t nfs 10.0.0.100:/srv/photos /mnt/photos
findmnt /mnt/photos

These addresses are placeholders. Ensure Immich can write to the mount. To persist it, add this to /etc/fstab:

10.0.0.100:/srv/photos /mnt/photos nfs defaults 0 0

Verify the NAS is mounted before starting Immich. Otherwise, uploads could land in the local directory rather than on the NAS. NFS is optional.

3. Download the Official Immich Compose Files

Download the official Compose files into /opt/immich:

sudo mkdir -p /opt/immich
cd /opt/immich
sudo wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
sudo wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env

Then edit .env. The important settings from my deployment are:

UPLOAD_LOCATION=/mnt/photos
DB_DATA_LOCATION=./postgres
IMMICH_VERSION=v3
DB_PASSWORD=<your-random-alphanumeric-password>
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

Replace the password placeholder with a unique random alphanumeric value. My deployment uses the v3 image tag; check the official instructions before installing.

UPLOAD_LOCATION points to the media directory and DB_DATA_LOCATION keeps PostgreSQL local. Without a NAS, use a suitable local media directory.

4. Start Immich

From /opt/immich, start the services:

sudo docker compose up -d
sudo docker compose ps

Once the services are healthy, open:

http://<your-debian-guest-ip>:2283

Create the administrator account, then upload a test photo to confirm that storage works.

5. Set Up HTTPS Access

For my installation, HAProxy on my OPNsense firewall accepts HTTPS requests for my photo domain and forwards them to the Immich server on port 2283. This gives me the same HTTPS address on my local network and from outside my home. PostgreSQL and the cache aren’t exposed as separate public services.

For remote access, use a secure HTTPS reverse proxy or VPN. See Immich’s reverse proxy guidance for the required headers and upload limits. Don’t expose plain HTTP to the internet.

How I Set Up Automatic iPhone Photo Backup

Once the server is online, this is the part that turns it into a practical iCloud Photos alternative.

  1. Install the Immich iOS app and sign in using the address of your server. On my setup, that’s the HTTPS domain configured through HAProxy.
  2. Give Immich access to your photos. Select the photos and albums you want it to back up. To cover your library, make sure the appropriate camera-roll albums are included, rather than assuming every album is selected automatically.
  3. Open the backup screen using the cloud icon, select the relevant albums, and tap Enable Backup. Immich will begin uploading eligible photos and videos.
  4. Enable Background App Refresh for Immich in iOS Settings under General > Background App Refresh. By default, Immich uses Wi-Fi for uploads, although you can change that in its backup settings.
  5. Verify the result in the web interface or on another device. For the first large upload, I recommend opening Immich and allowing enough time for it to work through the library before depending on the server as your only copy.

If you use Optimize iPhone Storage, some originals may still be in iCloud. Immich can temporarily fetch those assets for upload, which can use significant bandwidth and local storage.

Before reducing your iCloud plan, verify all originals are uploaded and independently backed up. Turning on the app doesn’t mean migration is complete.

How I Handle Storage and Backups

Owning the storage also means owning the recovery plan.

My NAS holds Immich’s media directories, while PostgreSQL stays on the container’s disk.

My instance also has Immich’s automatic database dumps, stored under the photo location’s backups directory. The configuration documented for November 10, 2026 retained 14 daily dumps. I additionally have a scheduled database export that creates a compressed archive and sends it to a backup account on the NAS, where those archives are retained for up to 30 days.

Those are database backups, not complete photo backups. The database holds the library’s records and metadata; the actual photos and videos must be backed up separately. A second archive on the same NAS also isn’t protection against losing that NAS. For a dependable replacement for paid cloud storage, plan an independent backup of the entire Immich media location as well as the database, ideally with another copy offsite. Immich’s backup and restore documentation explains what needs to be preserved.

Using IMMICH_VERSION=v3 doesn’t automatically update running containers; I still need to pull and deploy updated images. Over time, unused images can also take space. I’ve covered freeing extra disk space used by Docker separately, but don’t delete data volumes without understanding what’s stored in them.

Is Immich a Good iCloud Photos Alternative?

For me, yes. I wanted my iPhone photos to back up automatically without committing to another monthly photo-storage subscription, and Immich gives me that workflow on hardware I already operate. The iOS app is the part I interact with; Proxmox, Docker, and the NAS are simply what make it possible behind the scenes.

Apple offers deeper iOS integration. With Immich, I have to maintain the server, apply updates, and maintain backups. It’s not for someone who wants zero administration.

But if you already run Proxmox or a small homelab, Immich is worth considering as an iCloud Photos alternative. You get the convenience of a dedicated photo app, automatic uploads, and access to your library, while keeping the actual files on infrastructure you control.

For me, the important part isn’t the server configuration. It’s dependable iPhone uploads, an accessible library, and a backup plan if something breaks.

The post My iCloud Photos Alternative: Setting Up Immich on Proxmox appeared first on TechOpt.

]]>
https://www.techopt.io/servers-networking/my-icloud-photos-alternative-setting-up-immich-on-proxmox/feed 0
Host an openSUSE RPM Repository With Gitea and zypper https://www.techopt.io/linux/host-an-opensuse-rpm-repository-with-gitea-and-zypper https://www.techopt.io/linux/host-an-opensuse-rpm-repository-with-gitea-and-zypper#respond Fri, 23 Oct 2026 01:19:20 +0000 https://www.techopt.io/?p=1498 I run a self-hosted openSUSE RPM repository using my existing Gitea instance. This allows me to install and update third-party software through zypper on both openSUSE Leap and Tumbleweed, without manually downloading RPM files on every machine. The repository uses Gitea’s built-in package registry to store and sign RPM packages. An hourly systemd job checks […]

The post Host an openSUSE RPM Repository With Gitea and zypper appeared first on TechOpt.

]]>
I run a self-hosted openSUSE RPM repository using my existing Gitea instance. This allows me to install and update third-party software through zypper on both openSUSE Leap and Tumbleweed, without manually downloading RPM files on every machine.

The repository uses Gitea’s built-in package registry to store and sign RPM packages. An hourly systemd job checks upstream for newer releases, publishes them to Gitea, and removes older versions automatically.

Currently, I’m using this setup to distribute Cursor, but it can work with other software distributed as RPM packages as well.

In this guide, I’ll explain how the repository works, how to configure Gitea and zypper, and how I keep the packages up to date.

How the openSUSE RPM Repository Works

Gitea 1.20 and newer includes a built-in RPM package registry that supports the rpm-md repository format used by package managers such as yum, dnf, and zypper.

This means you can host an RPM repository directly from Gitea without running a separate package server or generating repository metadata yourself.

When I upload an RPM, Gitea stores the package in its package registry and automatically regenerates the repository’s repodata.

The general setup is straightforward:

  1. An automated job checks whether a newer upstream RPM is available.
  2. If a new build exists, the job downloads the RPM and uploads it to Gitea.
  3. Gitea signs the package and updates the repository metadata.
  4. openSUSE clients access the repository using zypper.
  5. The publisher keeps the two newest versions of each package and deletes older ones.

The important distinction is that I’m not compiling software specifically for openSUSE.

Instead, I’m mirroring upstream RPMs that are already compatible with the distributions I’m using. This is not an openSUSE Build Service project, and it doesn’t involve rebuilding packages from RPM spec files.

One Repository for Leap and Tumbleweed

My Gitea RPM registry uses a single group named opensuse.

Both Leap and Tumbleweed access that same group, so I don’t need separate repositories for each distribution.

For this guide, I’ll use the following example configuration:

SettingValue
Gitea URLhttps://git.example.com
Package ownerpackages
Repository groupopensuse
Repository typerpm-md
Architecturex86_64
Packagecursor

The resulting repository URL is:

https://git.example.com/api/packages/packages/rpm/opensuse

Notice that packages appears twice in the URL. The first occurrence belongs to Gitea’s package API path, while the second is the name of the package owner.

All hostnames, usernames, and credentials shown in this guide are placeholders.

Is a Fedora RPM Compatible With openSUSE?

Cursor distributes an RPM intended for RPM-based Linux distributions, including Fedora-compatible systems.

At the time I configured this repository on September 26, 2026, its upstream x86_64 RPM was:

cursor-3.22.7-1790228250.el8.x86_64

The file was approximately 290 MB.

Despite the .el8 suffix, this particular RPM works on both openSUSE Leap and Tumbleweed.

Its dependencies consist primarily of shared library sonames, along with ca-certificates and xdg-utils. There are no Fedora-specific package names in its RPM Requires metadata.

Additionally, its highest required glibc symbol is GLIBC_2.28, which is available on openSUSE Leap 15.3 and newer, as well as Tumbleweed.

I’ve used the same RPM successfully on Leap 15.6 and Tumbleweed, including its GTK and NSS dependencies.

Of course, this doesn’t mean every Fedora RPM will work on openSUSE. You still need to inspect the dependencies of any additional packages you mirror.

1. Configure Gitea to Host RPM Packages

First, you’ll need an existing Gitea installation with its package registry enabled.

For RPM publishing, Gitea 1.20 or newer is required. However, you’ll need Gitea 1.23 or newer to use its RPM signing functionality.

You can find more details about the supported endpoints in the official Gitea RPM package registry documentation.

Configure the Reverse Proxy Upload Limit

One important consideration is the upload size.

Since Cursor’s RPM was approximately 290 MB at the time of setup, the reverse proxy in front of Gitea needs to accept request bodies larger than the package.

For example, if you’re using nginx, you can configure:

client_max_body_size 512m;

Adjust this value to accommodate the RPMs you intend to publish.

If the proxy rejects the upload with HTTP 413 Request Entity Too Large, the request body limit is probably too low.

You’ll also want enough storage for multiple package versions. Keeping two copies of Cursor requires approximately 600 MB, excluding additional registry overhead.

Create a Package Access Token

The publisher needs a Gitea personal access token with the following permissions:

  • read:package
  • write:package

This token allows the publishing job to upload packages, inspect existing versions, and delete older ones.

The publishing token should remain on the machine running the synchronization job. It doesn’t need to be distributed to openSUSE clients.

If the Gitea package owner is public, clients can access the RPM repository without authentication.

For private package owners, I’ll cover how to configure a separate read-only token later.

2. Upload and Sign an RPM Package

With Gitea configured, the next step is publishing an RPM.

Gitea’s RPM registry accepts HTTP PUT requests containing the RPM file.

For example:

curl --user USER:TOKEN --upload-file cursor.rpm \
  "https://git.example.com/api/packages/packages/rpm/opensuse/upload?sign=true"

The --upload-file option tells curl to upload the RPM using an HTTP PUT request.

The sign=true query parameter tells Gitea to sign the RPM as part of the publishing process.

This is particularly important for Cursor because its upstream RPM is not signed.

Gitea’s generated repository configuration enables gpgcheck=1, meaning zypper expects a valid package signature. Without signing, installation can fail during package verification.

Enable RPM Signing by Default

Alternatively, you can enable automatic RPM signing globally through Gitea’s app.ini.

Under an uncommented [packages] section, add:

[packages]
DEFAULT_RPM_SIGN_ENABLED = true

With this enabled, Gitea signs RPM uploads by default instead of requiring sign=true on every request.

I use signing so clients can verify packages against the key associated with the Gitea package owner.

Understanding Upload Responses

The RPM upload endpoint returns different HTTP status codes depending on the result.

StatusMeaning
201 CreatedRPM published successfully
409 ConflictThat exact package version already exists
400 Bad RequestThe submitted package is invalid

When Gitea returns 201, it also regenerates the group’s repository metadata.

A 409 isn’t necessarily a problem. In this setup, it simply means the same package version already exists, so there’s nothing to replace.

There is no reason to delete and reupload an identical version.

3. Import the Gitea RPM Signing Key on openSUSE

Before installing packages from the repository, clients need to trust the public key used by Gitea to sign them.

Each Gitea package owner has its own signing key.

For the example owner, the public key URL is:

https://git.example.com/api/packages/packages/rpm/repository.key

However, there’s an important detail: Gitea doesn’t serve this key in the ASCII-armored format expected by the usual rpm --import workflow.

Instead, I import the key with GnuPG, export an armored copy, and then import that copy into RPM.

Run the following commands in a root shell, such as one opened with sudo -i:

curl -fsS -o repository.key \
  "https://git.example.com/api/packages/packages/rpm/repository.key"

gpg --batch --import repository.key

gpg --batch --export --armor > /etc/pki/rpm-gpg/gitea-packages-opensuse.asc

rpm --import /etc/pki/rpm-gpg/gitea-packages-opensuse.asc

The result is an armored public key at:

/etc/pki/rpm-gpg/gitea-packages-opensuse.asc

I’ll reference this file in the zypper repository configuration.

If you publish RPMs under another Gitea owner, you’ll need to import that owner’s signing key instead.

4. Configure the Repository in zypper

The client configuration is almost identical on openSUSE Leap and Tumbleweed.

Before adding the repository, there’s one setting I recommend applying to avoid problems when downloading large RPMs.

Configure zypper’s Temporary Directory

On some openSUSE installations, /tmp resides on a separate tmpfs filesystem.

When zypper downloads a package into that temporary location and attempts to rename it into /var, the operation can fail if the directories reside on different filesystems.

To avoid this, I use /var/tmp as the temporary directory.

export TMPDIR=/var/tmp
mkdir -p "$TMPDIR"

Run these commands before invoking zypper in the same shell. If you’re using a root shell, keep that shell open for the installation commands.

This avoids the cross-filesystem rename problem without disabling package verification or changing the repository itself.

Option A: Configure a Private Gitea Repository

My Gitea package owner is private, so the openSUSE clients need authentication to download repository metadata and packages.

I don’t put the access token directly into the repository URL.

Instead, I store a separate read-only token in zypper’s credentials directory.

Create the directory and credentials file:

install -d -m 700 /etc/zypp/credentials.d

umask 077

cat > /etc/zypp/credentials.d/gitea-packages-opensuse <<'EOF'
username=USER
password=READ_TOKEN
EOF

chmod 600 /etc/zypp/credentials.d/gitea-packages-opensuse

Replace USER and READ_TOKEN with a Gitea user and a token authorized to read packages from that owner.

This should not be the same token used by the publisher, since clients don’t need permission to upload or delete packages.

Next, create the repository configuration file:

/etc/zypp/repos.d/gitea-packages-opensuse.repo

Add the following:

[gitea-packages-opensuse]
name=packages openSUSE
baseurl=https://git.example.com/api/packages/packages/rpm/opensuse?credentials=gitea-packages-opensuse
enabled=1
autorefresh=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/gitea-packages-opensuse.asc
type=rpm-md

Notice the ?credentials=gitea-packages-opensuse parameter in the baseurl.

This tells libzypp to load credentials from the matching file in /etc/zypp/credentials.d, rather than embedding them directly in the repository configuration.

You can read more about this feature in the official libzypp configuration documentation.

Now refresh the repository:

zypper --non-interactive --gpg-auto-import-keys refresh gitea-packages-opensuse

Then install Cursor:

zypper --non-interactive install --allow-vendor-change cursor

The --allow-vendor-change flag permits zypper to install or replace the package from this repository even when the package vendor differs from another installed version.

Once installed, I can update Cursor using:

zypper refresh && zypper update cursor

That’s all the client needs. It can now use the private Gitea package registry like another RPM repository.

Option B: Configure a Public Gitea Repository

If your Gitea package owner is public, the setup is simpler.

You don’t need to create a credentials file.

After importing the signing key from the previous section, you can add the repository directly:

zypper addrepo https://git.example.com/api/packages/packages/rpm/opensuse.repo

Then refresh and install:

zypper refresh
zypper install cursor

Remember to set TMPDIR=/var/tmp before installing.

If the generated repository file points directly to Gitea’s raw signing key, replace its gpgkey value with the local armored key:

gpgkey=file:///etc/pki/rpm-gpg/gitea-packages-opensuse.asc

After that, the public and private configurations behave the same way from the client’s perspective.

Both Leap and Tumbleweed use the same Gitea repository and receive the same RPM.

screenshot of cursor rpm package in opensuse rpm gitea repository
A screenshot of the Cursor RPM package in the Gitea packages interface.

5. Automatically Detect and Publish Cursor Updates

Manually uploading RPMs works, but I want the repository to stay current without having to check for releases myself.

I use a publishing job that runs once per hour.

The important part is detecting when a new RPM is available.

Find Cursor’s Latest RPM

Cursor exposes a download endpoint for its latest Linux x86_64 RPM:

https://api2.cursor.sh/updates/download/golden/linux-x64-rpm/cursor/latest

This endpoint redirects to the actual RPM download.

I use the redirect’s Location header to identify a new build.

You can inspect the redirect using:

curl -fsSI \
  "https://api2.cursor.sh/updates/download/golden/linux-x64-rpm/cursor/latest"

The resulting download URL contains a production commit identifier in a path resembling:

production/<commit>/

For example, on September 26, 2026, the latest download pointed to a build associated with this commit:

37076c6c3f9e253c0fa2305197e45befd13a2268

My publisher saves that commit identifier in a state file.

The next time it runs, it compares the current redirect against the saved identifier. If nothing changed, the job exits without downloading the entire RPM again.

This is especially useful for a package of approximately 290 MB.

Why I Use the Golden Download URL

Cursor also has a JSON-based update API, but it doesn’t accept the linux-x64-rpm platform identifier.

Instead of polling an unsupported platform through that API, I use the golden download endpoint.

It’s also important to use the URL ending in /cursor/latest, rather than one pinned to a specific release line such as /cursor/3.22.

A version-specific URL might receive updates within the 3.22 series but miss the next minor release.

The latest endpoint is what allows the publisher to keep following new versions.

How the Publishing Job Works

My hourly job follows five steps.

1. Check the upstream redirect

It sends a HEAD request to the golden download URL and extracts the commit identifier from the Location header.

2. Compare against the saved state

Each package has its own state file containing the identifier of its last successfully published build.

If the identifier matches, there’s nothing new to publish.

3. Download and inspect the RPM

When the identifier changes, the job downloads the RPM and reads the package metadata using rpm.

For example:

rpm -qp --nosignature \
  --queryformat '%{NAME}\n%{VERSION}-%{RELEASE}\n%{ARCH}\n' \
  cursor.rpm

This provides the package name, version-release, and architecture.

For the example Cursor RPM from September 2026, those values correspond to:

cursor
3.22.7-1790228250.el8
x86_64

4. Upload the new build

The job publishes the RPM to the opensuse group using Gitea’s upload endpoint with sign=true.

curl --user USER:TOKEN --upload-file cursor.rpm \
  "https://git.example.com/api/packages/packages/rpm/opensuse/upload?sign=true"

A successful upload returns 201, and Gitea regenerates the repository metadata.

After a successful publication, the job saves the new commit identifier to that package’s state file.

5. Remove older package versions

Finally, the publisher checks which versions are already stored in Gitea and keeps the two newest.

This allows me to retain one previous build without storing an unlimited number of RPMs.

Keep Only the Two Newest RPM Versions

Gitea exposes a REST API for retrieving the existing package versions.

For Cursor, the endpoint is:

GET /api/v1/packages/packages/rpm/cursor?limit=50

The publisher retrieves the version list and sorts the entries by created_at, newest first.

It keeps the first two versions and deletes everything older.

The delete endpoint uses the package name, version-release, and architecture:

curl --user USER:TOKEN -X DELETE \
  "https://git.example.com/api/packages/packages/rpm/opensuse/package/cursor/VERSION-RELEASE/x86_64"

For example, the version-release of the September 2026 package was:

3.22.7-1790228250.el8

Gitea returns 204 No Content when the deletion succeeds.

A 404 means the package version was already removed, while other unexpected responses indicate a failure that the job should report.

Keeping only two versions limits Cursor’s RPM storage to roughly 600 MB at the package sizes I was seeing when I configured this setup.

6. Schedule Automatic Updates With a systemd Timer

To run the publisher automatically, I use a systemd user service and timer on an always-on Linux machine.

This avoids needing a Gitea Actions runner or another CI system just to keep an RPM repository updated.

Configure the Publisher Environment

The publisher reads its Gitea credentials and repository settings from an environment file.

The file contains the following values:

GITEA_URL=https://git.example.com
GITEA_OWNER=packages
GITEA_USER=USER
GITEA_TOKEN=TOKEN
GITEA_GROUP=opensuse
KEEP=2

GITEA_GROUP defaults to opensuse, while KEEP defaults to 2.

I keep this file private using permissions of 600, since it contains the token with package publishing access.

The client-side read token is separate and isn’t required by the publisher.

Create the systemd Service

Create the following user service:

~/.config/systemd/user/opensuse-rpm-sync.service

Add:

[Unit]
Description=Publish vendor RPMs to the Gitea openSUSE repository
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
EnvironmentFile=%h/.config/opensuse-rpm/env
ExecStart=%h/bin/sync-opensuse-rpm.sh
Nice=10

The ExecStart value points to the local publishing script. Adjust it to wherever you keep your own publisher.

The service uses Type=oneshot, meaning systemd runs the synchronization and considers the service complete when the script exits.

I’ve also set Nice=10 to give the job a lower scheduling priority than normal interactive processes.

Create the Hourly Timer

Next, create:

~/.config/systemd/user/opensuse-rpm-sync.timer

With the following configuration:

[Unit]
Description=Hourly openSUSE RPM sync

[Timer]
OnCalendar=hourly
Persistent=true
RandomizedDelaySec=5min

[Install]
WantedBy=timers.target

There are two useful settings here.

Persistent=true tells systemd to trigger a missed scheduled run after the machine becomes available again.

Meanwhile, RandomizedDelaySec=5min introduces a small random delay so the job doesn’t necessarily start at the exact beginning of every hour.

Hourly checks are sufficient for the software I’m mirroring.

Enable the Timer

Reload the user systemd configuration and enable the timer:

systemctl --user daemon-reload
systemctl --user enable --now opensuse-rpm-sync.timer

I also enable user lingering:

loginctl enable-linger "$USER"

This allows the user systemd instance to continue running even when the account isn’t actively logged in.

At this point, the timer runs the publishing job automatically without requiring a desktop session or an interactive terminal.

7. Add More RPM Packages to the Same Repository

Although I currently use this repository for Cursor, the same approach works for additional vendor RPMs.

There’s no need to create a new Gitea package group or add another repository on the openSUSE clients.

Instead, the publisher uses a separate source definition for each package.

Every published RPM appears in the existing opensuse group.

Cursor’s Source Configuration

For Cursor, the source definition looks like this:

NAME=cursor
KIND=redirect
URL=https://api2.cursor.sh/updates/download/golden/linux-x64-rpm/cursor/latest
STATE_REGEX=.*/production/\([^/]*\)/.*
KEEP=2

The redirect source type sends a HEAD request to the URL and uses the redirect destination as the identity.

The optional STATE_REGEX extracts a specific identifier from that URL.

In Cursor’s case, it extracts the production commit.

If the regular expression doesn’t match the redirect, the publisher treats that as an error rather than blindly downloading and publishing an unexpected file.

Supported Source Types

My publisher handles three different types of package sources.

Source typeHow changes are detected
redirectHEAD request and redirect Location, optionally processed by STATE_REGEX
urlETag, Last-Modified, and Content-Length headers, with SHA256 fallback
fileSHA256 checksum of a local RPM

For a direct URL, the publisher combines the available HTTP metadata into an identity.

If the server provides none of those headers, the publisher downloads the file and computes its SHA256 checksum instead.

For a local file, the checksum serves as the identity from the beginning. If the file doesn’t exist, the job reports a failure.

Example: Add a Second Package

Suppose I have another application distributed through a direct RPM download.

Its source definition could look like this:

NAME=example
KIND=url
URL=https://example.com/example-latest.x86_64.rpm
KEEP=2

The next sync checks this source along with Cursor.

Each package maintains a separate state file, using its package name as the filename. For example, the files could live under a state directory such as:

~/.local/state/opensuse-rpm/

Keeping separate state files prevents an update to one package from affecting change detection for another.

The publisher also processes sources independently. If one source fails, it continues processing the others, but the overall job still exits with a failure status.

Once the additional package is uploaded, it becomes available through the same Gitea repository.

On an openSUSE client, I can install it using:

zypper install example

No additional .repo file is necessary.

Adding the repository and refreshing its metadata does not automatically install every package it contains. Clients still choose which packages to install.

8. Troubleshooting Common Problems

There are a few issues worth checking if the repository doesn’t behave as expected.

HTTP 413 When Uploading an RPM

If the upload returns:

413 Request Entity Too Large

Check the request body size limit on the reverse proxy in front of Gitea.

For nginx, this is controlled by client_max_body_size.

Remember that the limit must accommodate the full RPM file, not just a small metadata request.

zypper Rejects the Package Signature

Check that Gitea signed the RPM during upload.

Cursor’s upstream RPM is unsigned, while the generated repository enables gpgcheck=1.

Make sure you’re using sign=true or have enabled DEFAULT_RPM_SIGN_ENABLED in Gitea.

Also verify that you’ve imported the public key belonging to the correct Gitea package owner.

If rpm --import rejects the raw repository.key, convert it to an ASCII-armored key using the GnuPG steps earlier in this guide.

Don’t disable package signature verification just to work around an incorrectly configured repository.

Private Repository Refresh Fails

If zypper refresh can’t read the repository metadata, verify the credentials file.

Check that:

  • The token has package read access.
  • The ?credentials= value matches the credentials filename.
  • The file is stored under /etc/zypp/credentials.d.
  • The file contains the correct username and password fields.

For private owners, an anonymous refresh won’t have permission to retrieve the package metadata.

Downloads Fail When Moving the RPM

If zypper reports an error while renaming or moving a downloaded package, check whether /tmp and /var reside on different filesystems.

Use:

export TMPDIR=/var/tmp
mkdir -p "$TMPDIR"

Then retry the zypper operation from the same shell.

Cursor Updates Aren’t Being Detected

First, make sure the publisher checks the golden URL ending in:

/cursor/latest

A URL pinned to a specific release line can miss newer minor releases.

Next, inspect the redirect using curl -fsSI and confirm that the Location header contains the expected production commit.

Also check the package’s saved state file and the systemd service logs to confirm the hourly job actually ran.

Multiple Repositories Provide Cursor

If you’ve installed dnf, check whether Cursor’s RPM created:

/etc/yum.repos.d/cursor.repo

I remove this file to avoid maintaining a second Cursor package source through its upstream yum repository.

You can also inspect your enabled zypper repositories using:

zypper lr

And verify the installed Cursor RPM with:

rpm -q cursor

The installed package name and version should correspond to the RPM published through Gitea.

Conclusion

Using Gitea as an openSUSE RPM repository gives me a straightforward way to distribute third-party packages across multiple Linux machines without maintaining a separate package server.

Gitea already handles RPM storage, package signing, and repository metadata, while zypper takes care of installation and dependency resolution on the clients.

The hourly publisher handles the remaining work: detecting upstream releases, downloading new RPMs, publishing signed packages, and keeping the number of stored versions under control.

The result is a single repository shared between Leap and Tumbleweed, with no need to manually download Cursor’s RPM every time a new build becomes available.

If you’re maintaining several openSUSE installations, I’ve also written guides covering upgrading openSUSE Leap from 15.5 to 15.6 and upgrading openSUSE Leap 15.6 to 16.0.

For additional package management references, you can also check out the tools available on TechOpt.

The post Host an openSUSE RPM Repository With Gitea and zypper appeared first on TechOpt.

]]>
https://www.techopt.io/linux/host-an-opensuse-rpm-repository-with-gitea-and-zypper/feed 0
How to Install Home Assistant on Proxmox Step by Step https://www.techopt.io/smart-home/how-to-install-home-assistant-on-proxmox-step-by-step https://www.techopt.io/smart-home/how-to-install-home-assistant-on-proxmox-step-by-step#respond Sat, 15 Aug 2026 01:44:01 +0000 https://www.techopt.io/?p=1346 I decided to install Home Assistant on Proxmox instead of running it on a dedicated Raspberry Pi or another physical computer. Since I already had a Proxmox server running in my homelab, giving Home Assistant OS its own virtual machine made more sense and let me manage it alongside my other VMs. These are the […]

The post How to Install Home Assistant on Proxmox Step by Step appeared first on TechOpt.

]]>
I decided to install Home Assistant on Proxmox instead of running it on a dedicated Raspberry Pi or another physical computer. Since I already had a Proxmox server running in my homelab, giving Home Assistant OS its own virtual machine made more sense and let me manage it alongside my other VMs.

These are the steps I used to install Home Assistant on Proxmox. I chose to install the full Home Assistant Operating System (HAOS) rather than running Home Assistant in a regular Linux VM or LXC container.

Home Assistant provides an official KVM/Proxmox .qcow2 disk image specifically for this type of installation. The process is slightly different from installing a normal operating system from an ISO because we import this prebuilt disk image into Proxmox instead.

I based part of my process on this community guide rather than using one of the automated helper scripts.

Before Installing Home Assistant on Proxmox

For my Home Assistant VM, I used:

  • 2 CPU cores
  • 4096 MB (4 GB) RAM
  • q35 machine type
  • OVMF (UEFI) BIOS
  • Home Assistant’s KVM/Proxmox QCOW2 image
  • The default Proxmox network interface

Home Assistant’s official documentation currently lists a minimum of 2 vCPUs and 2 GB of RAM for a virtual machine. I decided to give mine 4 GB of RAM instead. You can always increase the resources later if necessary.

You will also need access to the Proxmox web interface and shell (either via web or SSH) for the Proxmox node where you want Home Assistant to run.

Steps to Install Home Assistant on Proxmox

1. Get the Home Assistant KVM/Proxmox Image

First, go to the Home Assistant alternative installation page and find the download for KVM/Proxmox.

KVM/Proxmox download link on Home Assistant alternative downloads page

Home Assistant provides this image in QCOW2 format, which is exactly what we need for Proxmox.

Instead of downloading the image to my computer and uploading it again to Proxmox, I downloaded it directly from the Proxmox shell.

Right-click the KVM/Proxmox download link on the Home Assistant website and copy the link address.

copy link on Home Assistant download page

Then, open the Proxmox web interface, select your Proxmox node and open Shell.

Download the file using wget:

wget <HOME-ASSISTANT-QCOW2-UL>

For example, your command will look similar to:

wget https://github.com/home-assistant/operating-system/releases/download/<version>/haos_ova-<version>.qcow2.xz

downloading Home Assistant qcow2 on Proxmox via web shell

Press Enter after typing the command.

Alternatively, you can SSH into the Proxmox node to run these commands.

I recommend copying the current URL directly from Home Assistant instead of copying an old version number from another guide. That way, you will download the current Home Assistant OS image.

2. Decompress the Home Assistant QCOW2 Image

The downloaded QCOW2 image is compressed using XZ, so we need to decompress it before importing it into Proxmox.

Run:

unxz haos_ova-<version>.qcow2.xz

Replace the filename with the actual filename you downloaded.

For example:

unxz haos_ova-18.2.qcow2.xz

The version above is only an example. Use the filename you actually downloaded.

downloading and unzipping home assistant image for proxmox

After extraction, you should have a file ending in:

.qcow2

If you aren’t sure which directory you downloaded the file into, run:

pwd

You can also list the files in the current directory with:

ls

If you opened the shell through the Proxmox web interface, you will commonly be working from /root.

Show current directory when installing home assistant on proxmox with pwd and ls commands

The Proxmox forum guide uses the same wget and unxz process to obtain and extract the Home Assistant image.

3. Create a New Home Assistant VM in Proxmox

Next, go back to the Proxmox web interface and click Create VM.

We are going to create the VM without an installation ISO because the Home Assistant QCOW2 file will become the VM’s boot disk.

General

Give the VM a name such as:

HomeAssistant

Take note of the VM ID. We will need this number when importing the Home Assistant disk later. In my case, this was 103.

In Advanced settings I also enabled:

Start at boot

This way, Home Assistant will automatically start when my Proxmox server starts.

general tab settings creating vm for proxmox

OS

Under the OS tab, select:

Do not use any media

We don’t need an ISO because Home Assistant is already installed inside the QCOW2 image.

os tab settings creating vm for proxmox

System

This part is important.

For Machine, select:

q35

For BIOS, select:

OVMF (UEFI)

Select a storage location for the EFI disk. In many installations this will be:

local-lvm

In my case it was actually a storage location called VMs.

I also unchecked:

Pre-Enroll keys

Home Assistant requires UEFI when running as a virtual machine, and the referenced Proxmox configuration uses q35 with OVMF and Secure Boot keys disabled.

system tab settings creating vm for proxmox

Disks

Proxmox normally creates an empty disk when you create a VM.

We don’t need it.

Delete the automatically created SCSI disk from the VM configuration. The Home Assistant QCOW2 image that we downloaded earlier will become our disk instead.

disks tab settings creating vm for proxmox

CPU

I assigned:

2 cores

cpu tab settings creating vm for proxmox

You can give Home Assistant more CPU resources later if your installation grows.

Memory

I assigned:

4096 MB

That gives the VM 4 GB of RAM.

Although Home Assistant currently specifies 2 GB as the minimum for a VM, I preferred giving it 4 GB from the beginning.

memory tab settings creating vm for proxmox

Network

I left the network configuration at the Proxmox defaults.

For a typical installation, the VM should be connected to your normal Proxmox bridge so Home Assistant appears as another device on your LAN.

network tab settings creating vm for proxmox

If you’re using VLANs or a more complicated network configuration, adjust this accordingly.

Finish creating the VM.

Do not start it yet.

finish tab settings creating vm for proxmox

4. Import the Home Assistant QCOW2 Disk into Proxmox

Now we need to import the Home Assistant disk into the VM we just created.

Go back to the Proxmox node’s Shell.

The general syntax for importing a QCOW2 image is:

qm importdisk <VM-ID> <QCOW2-FILE> <STORAGE>

For example, if:

  • Your Home Assistant VM ID is 103
  • Your Home Assistant QCOW2 file is in /root
  • Your VM storage is local-lvm

You would run something similar to:

qm importdisk 103 /root/haos_ova-<version>.qcow2 local-lvm

Make sure you replace the VM ID, filename and storage with the values from your own Proxmox installation.

qm importdisk with haos qcow2 to storage

Let the import finish before continuing.

5. Attach the Imported Home Assistant Disk

Once the import completes, return to the Proxmox web interface.

Select your Home Assistant VM and open:

Hardware

You should now see something similar to:

Unused Disk 0

This is the Home Assistant disk we just imported.

Select the unused disk and click Edit.

set discard bit home assistant disk proxmox

If your Proxmox storage backing this VM is on an SSD, you should enable Discard.

Then, click Add.

set discard bit home assistant disk proxmox

The disk should now be attached to the Home Assistant VM, commonly as something similar to:

scsi0

showing attached drive proxmox home assistant vm

6. Set the Home Assistant Disk as the Boot Disk

Before starting the VM, we need to make sure Proxmox actually boots from the imported Home Assistant disk.

Select the VM and go to:

Options > Boot Order

Click Edit.

Enable the imported Home Assistant disk. In my case, this was the scsi0 disk.

Disable any unnecessary boot devices so the Home Assistant disk is the VM’s primary boot device.

check home assistant drive and uncheck unnecessary boot devices proxmox

7. Start the Home Assistant VM

We can finally start Home Assistant.

Click Start.

Then, open the VM’s Console.

Home Assistant OS should begin booting!

If everything is configured correctly, Home Assistant will eventually display network information in the console, including the address you can use to access the web interface.

home assistant running in proxmox

8. Open the Home Assistant Web Interface

Once Home Assistant has booted, open a web browser from another computer on your network.

Home Assistant may be available at:

http://homeassistant.local:8123

Alternatively, you can use the IP address shown in the Home Assistant console:

http://<HOME-ASSISTANT-IP>:8123

For example:

http://192.168.1.100:8123

Home Assistant’s documentation recommends using the IP address directly if hostname discovery doesn’t work on your network.

You should now see the Home Assistant onboarding page.

home assistant onboarding page

From here, you can create your Home Assistant account and continue with the normal initial setup.

That’s it! Home Assistant OS is now running as a virtual machine in Proxmox.

Troubleshooting Home Assistant on Proxmox

Home Assistant VM Says There Is No Bootable Disk

First, go to:

VM > Options > Boot Order

Make sure the imported Home Assistant disk is enabled.

It will commonly appear as something such as:

scsi0

The imported QCOW2 disk needs to be selected as a bootable device.

Also confirm that you actually attached the Unused Disk after running qm importdisk.

VM Fails to Boot or Shows a UEFI/Secure Boot Error

Check the VM’s system configuration.

I used:

  • Machine: q35
  • BIOS: OVMF (UEFI)
  • Pre-Enroll Keys: Disabled

Home Assistant requires UEFI for the virtual machine, while the Proxmox forum procedure specifically disables the pre-enrolled Secure Boot keys.

There are also reports in the referenced forum thread of Secure Boot preventing the Home Assistant image from booting until it was disabled.

qm importdisk Says the QCOW2 File Does Not Exist

Check your current directory:

pwd

Then list your files:

ls

You can also specify the complete path to the file:

qm importdisk 103 /root/haos_ova-<version>.qcow2 local-lvm

Also double-check the filename. The extension should be:

.qcow2

Make sure you have already decompressed the .qcow2.xz file with unxz.

homeassistant.local:8123 Does Not Work

Try accessing Home Assistant by IP address instead:

http://<HOME-ASSISTANT-IP>:8123

The Home Assistant documentation specifically recommends using the VM’s IP address when local hostname resolution does not work.

You can find the assigned address from the Home Assistant VM console or from your router/DHCP server.

What About Zigbee, Z-Wave and Other USB Devices?

If you use Home Assistant with a Zigbee, Z-Wave or other USB adapter, the physical USB device will need to be made available to the Home Assistant VM.

Home Assistant’s virtualization documentation specifically accounts for passing USB hardware such as Zigbee and Z-Wave controllers through to a VM.

I would get the basic Home Assistant VM installed and working first before configuring any USB passthrough. This makes it much easier to troubleshoot because you know the base Home Assistant installation is working before adding additional hardware.

If you’re planning to use a Zigbee coordinator with Home Assistant, you’ll also need to decide whether to use ZHA or Zigbee2MQTT. I’ve compared both options separately, along with the best Zigbee channels for Home Assistant if you’re trying to avoid Wi-Fi interference.

Why I Installed Home Assistant Manually

There are community scripts available that can automate much of the process of installing Home Assistant on Proxmox.

However, I wanted to install it manually.

Doing it this way makes the VM configuration much more obvious. You know exactly which machine type, BIOS, CPU, memory, storage and boot disk the Home Assistant VM is using.

It also makes troubleshooting easier later because the installation isn’t hidden behind a script.

The process really isn’t complicated once you understand that the Home Assistant download is a preinstalled QCOW2 disk rather than a conventional installation ISO.

The basic process is simply:

  1. Download the Home Assistant QCOW2 image.
  2. Decompress it.
  3. Create an empty Proxmox VM.
  4. Import the QCOW2 image using qm importdisk.
  5. Attach the imported disk.
  6. Make it bootable.
  7. Start the VM.
  8. Open Home Assistant on port 8123 at http://homeassistant.local:8123.

Conclusion

Installing Home Assistant on Proxmox is a little different from creating a typical Linux VM because there isn’t an installer ISO involved. Instead, Home Assistant provides a ready-to-use QCOW2 image that we import directly into the virtual machine.

These are the steps I used to install Home Assistant on Proxmox, and once the VM configuration and disk import were complete, Home Assistant OS booted like any other virtual machine.

For my setup, 2 CPU cores, 4 GB of RAM, q35 and OVMF worked well as a straightforward starting configuration.

Running Home Assistant this way also keeps it nicely contained in its own VM while letting Proxmox handle the underlying virtual hardware and startup of the system.

Remarks

  • Home Assistant provides an official KVM/Proxmox QCOW2 image, so make sure you download that image to install Home Assistant on Proxmox rather than one intended for VirtualBox or VMware.
  • I used 2 CPU cores and 4 GB of RAM. Home Assistant currently lists 2 vCPUs and 2 GB of RAM as the minimum VM requirements.
  • Make sure the VM uses OVMF/UEFI. Home Assistant requires UEFI to boot as a virtual machine.
  • If the VM refuses to boot, double-check that Pre-Enroll Keys/Secure Boot is disabled and that the imported Home Assistant disk is selected in the boot order.
  • If homeassistant.local doesn’t resolve on your network, access Home Assistant directly using its IP address and port 8123, such as http://192.168.1.100:8123.

The post How to Install Home Assistant on Proxmox Step by Step appeared first on TechOpt.

]]>
https://www.techopt.io/smart-home/how-to-install-home-assistant-on-proxmox-step-by-step/feed 0
GPU Passthrough to Proxmox LXC Container for Plex (or Jellyfin) https://www.techopt.io/servers-networking/gpu-passthrough-to-proxmox-lxc-container-for-plex-or-jellyfin https://www.techopt.io/servers-networking/gpu-passthrough-to-proxmox-lxc-container-for-plex-or-jellyfin#respond Sun, 24 Aug 2025 20:40:06 +0000 https://www.techopt.io/?p=1059 When I added an Intel Arc GPU to my Proxmox server for Plex hardware transcoding, I quickly realized there isn’t much solid documentation on how to properly passthrough a GPU to a Proxmox LXC container. Most guides focus on VMs, not LXCs. After some trial and error, I figured out a process that works. These […]

The post GPU Passthrough to Proxmox LXC Container for Plex (or Jellyfin) appeared first on TechOpt.

]]>
When I added an Intel Arc GPU to my Proxmox server for Plex hardware transcoding, I quickly realized there isn’t much solid documentation on how to properly passthrough a GPU to a Proxmox LXC container. Most guides focus on VMs, not LXCs. After some trial and error, I figured out a process that works. These same steps can also apply if you’re running Jellyfin inside an LXC.

In this guide, I’ll walk you through enabling GPU passthrough in Proxmox (or Jellyfin) LXC step by step.

Step 1: Enabling PCI(e) Passthrough in Proxmox

The first step is enabling PCI passthrough at the Proxmox host level. I mostly followed the official documentation here: Proxmox PCI Passthrough Wiki.

I’ll summarize what should be done below.

Enable IOMMU in BIOS

Before you continue, enable IOMMU (Intel VT-d / AMD-Vi) in your system’s BIOS. This setting lets GPUs pass directly through to containers or VMs.

Each BIOS Is different, so if you’re not sure you should check your motherboard’s instruction manual.

Enable IOMMU Passthrough Mode

Not all hardware supports IOMMU passthrough, but if yours does you’ll see a big performance boost. Even if your system doesn’t, enabling it won’t cause problems, so it’s worth turning on.

Edit the GRUB configuration with:

nano /etc/default/grub

Locate the GRUB_CMDLINE_LINUX_DEFAULT line and add:

iommu=pt

Intel vs AMD Notes

  • On Intel CPUs with Proxmox older than v9 (kernel <6.8), also add: intel_iommu=on
  • On AMD CPUs, IOMMU is enabled by default.
  • On Intel CPUs with kernel 6.8 or newer (Proxmox 8 updated or Proxmox 9+), you don’t need the intel_iommu=on parameter.

With an AMD CPU on Proxmox 9, my file looked like this in the end:

grub config to passthrough gpu to proxmox lxc with iommu

Save the file by typing CTRL+X, typing Y to confirm and then Enter to save.

Then update grub by running:

update-grub

Load VFIO Kernel Modules

Next, we need to load the VFIO modules so the GPU can be bound for passthrough. Edit the modules file with:

nano /etc/modules

Add the following lines:

vfio
vfio_iommu_type1
vfio_pci

My file looked like this in the end:

added kernel modules needed for gpu passthrough to lxc in proxmox

Save and exit, then update initramfs:

update-initramfs -u -k all

Reboot and Verify

Reboot your Proxmox host and verify the modules are loaded:

lsmod | grep vfio

You should see the vfio modules listed. This is what my output looks like:

vfio kernel modules are loaded

If you don’t get any output, the kernel modules have not loaded correctly and you probably forgot to run the update-initramfs command above.

To double-check IOMMU is active, run:

dmesg | grep -e DMAR -e IOMMU -e AMD-Vi

Depending on your hardware, you should see confirmation that IOMMU or Directed I/O is enabled:

output when IOMMU is active (or similar)

Step 2: Finding the GPU Renderer Device Path

Now that PCI passthrough support is enabled, the next step is to figure out the device path of the renderer for the GPU you want to pass through.

Run the following on your Proxmox host:

ls /dev/dri

This will print all the detected GPUs. For example, my output looked like this:

by-path  card0  card1  renderD128  renderD129

If you only have a single GPU, you can usually assume it will be something like renderD128. But if you have multiple GPUs (as I did), you’ll need to identify which renderer belongs to your Intel Arc card.

First, run:

lspci

This will list all PCI devices. From there, I found my Intel Arc GPU at 0b:00.0:

lspci output showing my intel arc GPU card

Next, run:

ls -l /dev/dri/by-path/

My output looked like this:

lrwxrwxrwx 1 root root  8 Aug 24 11:54 pci-0000:04:00.0-card -> ../card0
lrwxrwxrwx 1 root root 13 Aug 24 11:54 pci-0000:04:00.0-render -> ../renderD129
lrwxrwxrwx 1 root root  8 Aug 24 11:54 pci-0000:0b:00.0-card -> ../card1
lrwxrwxrwx 1 root root 13 Aug 24 11:54 pci-0000:0b:00.0-render -> ../renderD128

From this, I confirmed that my Intel Arc GPU was associated with renderD128. That means the full device path I need to pass to my LXC container is:

/dev/dri/renderD128

Step 3: Passthrough GPU Device to the Plex LXC Container

Now that we know the correct device path, we can pass it through to the LXC container.

  1. Stop the container you want to pass the GPU into.
  2. In the Proxmox web UI, select your Plex LXC container.
  3. Go to Resources.
  4. At the top, click Add → Device Passthrough.
    steps to get to device passthrough menu
  5. In the popup, set Device Path to the GPU renderer path you identified in Step 2 (in my case, /dev/dri/renderD128).
  6. In the Access mode in CT field, type:0666
    add passthrough device to lxc config
  7. Click Add.
  8. Once added, you can start the container again.

A Note About Permissions

Normally, you would configure a proper UID or GID in CT for the render group inside the container so only that group has access. However, in my testing I wasn’t able to get the GPU working correctly with that method.

Using 0666 permissions allows read/write access for everyone. Since this is a GPU device node and not a directory containing files, I’m not too concerned, but it’s worth noting for anyone who takes their Linux permissions very seriously.

Step 4: Installing GPU Drivers Inside the LXC Container

With the GPU device passed through, the container now needs the proper drivers installed. This step varies depending on the Linux distribution you’re running inside the container.

Some distros may already include GPU drivers by default. But if you reach Step 5 and don’t see your GPU as an option in Plex (or Jellyfin), chances are you’re missing the driver inside your container.

In my case, I’m using a Debian container, which does not include Intel Arc drivers by default. Here’s what I did:

  1. Edit your apt sources to enable the non-free repo: nano /etc/apt/sources.list
  2. Add non-free to the end of each Debian repository line:
    my sources.list after adding the non-free Debian repository
  3. Refresh apt sources: apt update
  4. Install the Intel Arc driver package: apt install intel-media-va-driver-non-free
  5. Restart the container.

For Debian, I followed the official documentation here: Debian Hardware Video Acceleration Wiki.

Note: These steps will vary depending on your GPU make, model and container distribution. Make sure to check the official documentation for your hardware and distro.

Step 5: Enabling Hardware Rendering in Plex

Now that your GPU and drivers are ready, the final step is to enable hardware transcoding inside Plex.

  1. Open the Plex Web UI.
  2. Go to Settings (top-right corner).
  3. In the left sidebar, scroll down and click Transcoder under Settings.
  4. Make sure the following options are checked:
    • Use hardware acceleration when available
    • Use hardware-accelerated video encoding
  5. Under Hardware transcoding device, you should now see your GPU. If not, double-check Step 4. In my case, it showed up as Intel DG2 [Arc A380]
  6. Select your GPU and click Save Changes.
Steps to enable hardware encoding in plex ui

Testing Hardware Transcoding

To verify that hardware transcoding is working:

  1. Play back any movie or TV show.
  2. In the playback settings, change the quality to a lower resolution that forces Plex to transcode:
    Force Plex to transcode by selecting a lower quality
  3. While it’s playing, go to Settings → Dashboard.
  4. If the GPU is handling transcoding, you’ll see (hw) beside the stream being transcoded:
    The GPU passthrough to the proxmox lxc for plex can be confirmed working from Plex dashboard: here are the steps

That’s it! You’ve successfully set up GPU passthrough in Proxmox LXC for Plex. These same steps should also work for Jellyfin with minor adjustments for the Jellyfin UI.

The post GPU Passthrough to Proxmox LXC Container for Plex (or Jellyfin) appeared first on TechOpt.

]]>
https://www.techopt.io/servers-networking/gpu-passthrough-to-proxmox-lxc-container-for-plex-or-jellyfin/feed 0
Run Virtual Machines on OPNsense with bhyve https://www.techopt.io/servers-networking/run-virtual-machines-on-opnsense-with-bhyve https://www.techopt.io/servers-networking/run-virtual-machines-on-opnsense-with-bhyve#respond Wed, 30 Apr 2025 02:00:16 +0000 https://www.techopt.io/?p=892 If you’ve ever looked at your OPNsense box and thought, “this thing is barely breaking a sweat,” you’re not alone. Many users with overpowered hardware are now looking for ways to run virtual machines on OPNsense to take full advantage of those idle resources with additional software and services. In my case, I had 8GB […]

The post Run Virtual Machines on OPNsense with bhyve appeared first on TechOpt.

]]>
If you’ve ever looked at your OPNsense box and thought, “this thing is barely breaking a sweat,” you’re not alone. Many users with overpowered hardware are now looking for ways to run virtual machines on OPNsense to take full advantage of those idle resources with additional software and services. In my case, I had 8GB of RAM and 120GB of storage sitting mostly idle, with CPU usage rarely spiking beyond a modest blip.

Instead of virtualizing OPNsense itself using something like Proxmox (which is a common suggestion), I wanted to keep OPNsense on bare metal for reliability and stability. But I also wanted to move some VMs directly onto my router box, such as my Ubuntu Server VM running the TP-Link Omada software that controls my WiFi. That led me down the rabbit hole of running a virtual machine on OPNsense—and the tool that makes this possible is bhyve, the native FreeBSD hypervisor.

This is definitely not officially supported and could break with any OPNsense update, so proceed with caution. My setup is loosely based on a 2023 forum post I found in the OPNsense community forums.

Step 1: Installing bhyve

To get bhyve running, we first need to enable the FreeBSD repository temporarily. OPNsense locks its package manager to prevent upgrades from mismatched repos, so we need to handle this carefully.

Lock the pkg tool

pkg lock -y pkg

Enable the FreeBSD repository

sed -i '' 's/enabled: no/enabled: yes/' /usr/local/etc/pkg/repos/FreeBSD.conf

Install bhyve and required packages

pkg install -y vm-bhyve grub2-bhyve bhyve-firmware

Disable the FreeBSD repository again

sed -i '' 's/enabled: yes/enabled: no/' /usr/local/etc/pkg/repos/FreeBSD.conf

Unlock pkg

pkg unlock -y pkg

⚠ Leaving the FreeBSD repo enabled may interfere with future OPNsense updates. Disabling it again helps maintain system stability, but means bhyve won’t update automatically. If you want to update bhyve later, you’ll need to repeat this process.

Step 2: Configuring the Firewall for Virtual Machines on OPNsense

Next, we need to create a virtual bridge to let our bhyve virtual machines talk to each other and the rest of the network.

This part is all done from the OPNsense UI.

Create a bridge interface

  • Navigate to Interfaces → Devices → Bridge
  • Add a new bridge with your LAN interface as a member
  • Enable link-local IPv6 if you use IPv6 on your network
  • Note the name of your bridge (e.g., bridge0)
Creating the LAN bridge switch for virtual machines on OPNsense

Assign and configure the bridge interface

  • Go to Interfaces → Assignments
  • Assign bridge0 to a new interface (give it a description like bhyve_switch_public)
  • Enable the interface
  • Check Lock – Prevent interface removal to avoid losing it on reboot
Assigning an interface to the VM bridge

Allow traffic through the bridge

  • Navigate to Firewall → Rules → bhyve_switch_public
  • Add a rule to allow all traffic (you can tighten this later if needed)
Allow all firewall rule on switch for virtual machines on opnsense

One thing to note: the forum post I referenced above did not mention anything about assigning an interface or adding a firewall rule for the bridge. However, in my experience, my virtual machines in OPNsense had no network connectivity until I completed both of these steps.

Step 3: Setting Up bhyve

With bhyve installed and your network bridge configured, the next step is to prepare the virtual machine manager and your storage directory. You have two options here: using ZFS (ideal for advanced snapshots and performance features) or a standard directory (simpler and perfectly fine for one or two VMs).

Option 1: Using ZFS for VM Storage

If you’re using ZFS (like zroot), create a dataset for your VMs:

zfs create zroot/bhyve

Then enable and configure vm-bhyve:

sysrc vm_enable="YES"
sysrc vm_dir="zfs:zroot/bhyve"
vm init

Sizing a new pool for VM disks? This ZFS capacity calculator estimates usable space after slop, then you can copy a zpool create command that fits your setup.

Option 2: Using a Standard Directory

If you’re not using ZFS or want a simpler setup for running just a few virtual machines on OPNsense:

mkdir /bhyve
sysrc vm_enable="YES"
sysrc vm_dir="/bhyve"
vm init

This sets up /bhyve as the default VM storage directory. You’ll now be able to manage and create virtual machines using the vm command-line tool, with bhyve handling the hypervisor backend.

This is the option that I personally chose for my setup, since I only plan on running 1 or 2 VMs.

Step 4: Configuring bhyve

With the storage and base configuration out of the way, the next step is to configure networking for bhyve. To do this, we’ll create a virtual switch that connects bhyve’s virtual NICs to the bridge interface we created in Step 2.

Setting up Networking

Run the following command to create a manual switch that binds to the bridge0 interface:

vm switch create -t manual -b bridge0 public

This tells vm-bhyve to create a virtual switch named public and associate it with bridge0, allowing your VMs to communicate with the rest of your network. Any virtual machine you create can now be attached to this switch to access LAN or internet resources just like any other device on your network.

Copying VM Templates

Before you start creating virtual machines in OPNsense, it’s helpful to copy the sample configuration templates that come with vm-bhyve. These templates make it easier to define virtual machines for different operating systems like FreeBSD, Linux, or Windows.

If you’re using ZFS and followed the zroot/bhyve setup as described in Option 1 above:

cp /usr/local/share/examples/vm-bhyve/* /zroot/bhyve/.templates/

If you’re using a standard directory setup like /bhyve, as described in Option 2 above:

cp /usr/local/share/examples/vm-bhyve/* /bhyve/.templates/

This copies example VM configuration templates into the .templates directory within your VM storage location. These templates provide base config files for creating new VMs and are a helpful starting point for most operating systems.

Step 5: Setting Up Your Virtual Machine

In this step, we’ll walk through creating your first VM using bhyve. Since there’s more to this than just launching a template, I’ve broken it down into three parts: setting up the VM itself (including creating a config), installing the operating system, and then configuring the firewall for the VM.

Configuring the VM

For my setup, I was creating an Ubuntu Server instance that runs TP-Link Omada controller software.

First, navigate into your templates directory. If you’re using ZFS, it’ll look like this:

cd /zroot/bhyve/.templates/

Or if you’re using a regular directory setup:

cd /zroot/bhyve/.templates/

Inside, you’ll find configuration files for different OS templates. I used the one for Ubuntu, found at ubuntu.conf, which contains the following:

loader="grub"
cpu=1
memory=512M
network0_type="virtio-net"
network0_switch="public"
disk0_type="virtio-blk"
disk0_name="disk0.img"

This basic config uses GRUB as the loader and allocates 1 CPU and 512MB of RAM. It attaches to the public switch we created earlier and uses a virtual block device for storage.

To create a custom config for my Omada controller VM, I simply copied the Ubuntu template:

cp ubuntu.conf omada-controller.conf

This gave me a dedicated configuration file I could tweak without touching the original Ubuntu template.

Next, I edited the omada-controller.conf file using nano to better suit the needs of the Omada software:

nano omada-controller.conf

And the contents:

loader="uefi"
cpu=2
memory=3G
network0_type="virtio-net"
network0_switch="public"
disk0_type="virtio-blk"
disk0_name="disk0.img"

This configuration increases the resources available to the VM—allocating 2 CPU cores and 3GB of RAM, which is more appropriate for running the Omada controller software.

Initially, I tried using the GRUB loader as shown in the Ubuntu template, but I ran into problems booting the OS after installation. After doing some research, I found that this is a fairly common issue when using GRUB with certain Linux distributions on bhyve. Switching to uefi resolved the problem for me and allowed the VM to boot normally. Your mileage may vary, but if you’re stuck at boot, switching the loader to uefi is worth trying.

Starting Guest OS Installation

Before you can install the operating system, you’ll need to download and import the installation media (ISO file) for your OS into bhyve. This is easy to do with the vm iso command.

I downloaded the Ubuntu 22.04.5 server installer ISO using:

vm iso https://mirror.digitaloceans.dev/ubuntu-releases/22.04.5/ubuntu-22.04.5-live-server-amd64.iso

This downloaded the ISO file directly into the /bhyve/.iso directory (or /zroot/bhyve/.iso if you’re using ZFS).

Once the ISO was downloaded, I started the installation process for my VM using:

vm install -f omada-controller ubuntu-22.04.5-live-server-amd64.iso

This command tells vm-bhyve to boot the VM using the downloaded ISO so you can proceed with installing the operating system in console mode.

In my case, when using the GRUB loader, the console mode installer worked fine. However, after switching to UEFI mode, I ran into a problem where the console installer would no longer boot properly. After doing some research, I found that this is a common issue with bhyve when using UEFI.

To work around this, I edited my omada-controller.conf and temporarily added the following line to enable graphical output:

graphics="yes"

The updated configuration file looked like this:

loader="uefi"
cpu=2
memory=3G
network0_type="virtio-net"
network0_switch="public"
disk0_type="virtio-blk"
disk0_name="disk0.img"
graphics="yes"

This allowed the ISO to boot into the graphical installer, which I accessed using VNC. After installation, I planned to enable SSH on the VM to manage it more easily and remove the graphics option.

However, to use VNC to complete the installation, I needed to add additional firewall rules to allow VNC access to the virtual machine after it was created and booted.

Step 6: Configuring the Firewall (Again)

When a bhyve virtual machine boots, it creates a new network interface on the host system, usually with the prefix tap. When my bhyve VM booted, the firewall blocked all network access on the new VM interface. As a result, I was unable to connect to the VM, and the VM itself had no network connectivity.

Here’s what I did to properly assign the VM network interface and open up traffic:

  • Run ifconfig from the console to see the list of interfaces.
  • Identify the new interface created by bhyve (it will usually start with tap). In my case, it was named tap0.
  • Rename the tap interface so it can be properly assigned in the OPNsense GUI:
ifconfig tap0 name bhyve0
  • Go to Interfaces → Assignments in the OPNsense UI.
  • Assign the newly renamed bhyve0 interface.
  • Give it a description like bhyve_vm_omada_controller.
  • Enable the interface.
Assigning the vm to an interface in the OPNsense UI
  • Go to Firewall → Rules → [bhyve_vm_omada_controller].
  • Add a rule to allow all traffic through the interface.

This setup ensured that the virtual machine had full network access through its own dedicated interface, while still keeping things clean and organized within OPNsense.

Keep in mind that each time the VM is powered off and started again, a new tap interface is created. Because of this, you must manually rename and reassign the interface every time the VM boots until we set up a persistent interface configuration after the OS installation is complete.

Keeping the interface name consistent between shutdowns so firewall rules apply correctly was one of the trickiest parts of the entire setup for me. I’ll dive deeper into the different solutions I tested, and what finally solved the issue reliably, in the final step of this article.

Step 7: Connecting with VNC and Installing the OS

Now that our virtual machine on OPNsense is configured, the ISO is loaded, and the firewall rules are in place, it’s time to connect to the VM and install the OS.

  • Open your preferred VNC client. I personally used Remmina for this, but other popular options like TightVNC and TigerVNC will also work fine.
  • Connect to your OPNsense router’s IP address on port 5900.
  • You should see your OS installer’s screen boot up!
Remotely connect to virtual machines on OPNsense with VNC

Proceed through the guest OS installation like you normally would. During installation, I made sure to:

  • Enable the OpenSSH server in Ubuntu so I could manage the VM over SSH instead of VNC afterward.
  • Configure the VM with a static IP address within my LAN subnet.

Once installation was completed, I rebooted the VM. If you enabled SSH, you should now be able to connect to your VM via its IP address without needing to rely on VNC anymore.

After confirming SSH access, I edited the VM’s configuration to remove the graphics="yes" line from omada-controller.conf for security and resource efficiency.

After powering off the VM to make these changes, I had to manually rename the network adapter again following the steps from Step 6, before I could access it via SSH.

Now let’s configure the VM to start automatically at boot, and find a more permanent solution for the network adapter issue in the next step.

Step 8: Starting the VM at Boot and Fixing the Network Interface Name Issue

Starting the VM at Boot

By default, bhyve VMs don’t start automatically with the system. You can set individual VMs to start using:

vm set omada-controller boot=on

However, there’s a more flexible method that allows you to define a list of VMs that should start at boot. I used the following command to specify mine:

sysrc vm_list="omada-controller"

This ensures vm-bhyve starts the omada-controller VM whenever the system boots.

If you want to start multiple VMs, just list them separated by spaces:

sysrc vm_list="omada-controller another-vm some-other-vm"

This is useful if you plan to run multiple VMs on your OPNsense machine via bhyve.

Fixing the Networking Interface Name at Boot

One of the trickiest parts of my setup was keeping the VM’s network interface name consistent between reboots. I initially tried using the following line in my VM config:

network0_name="bhyve0"

This is supposed to create the VM’s network interface with the name bhyve0 when it boots.

However, I found that while this approach worked with loader="grub" in the VM config (BIOS mode), it caused the VM to crash immediately at startup when using loader="uefi".

Instead, I leaned into ifconfig and ran the following:

sysrc ifconfig_tap0_name="bhyve0"

This ensures the tap0 interface is automatically renamed to bhyve0 at boot time, which seems to be working well for me. This seems to allow the firewall rules we created earlier to be applied successfully without manual intervention.

Now we have configured our VM to start running at boot, and we automatically rename the created network interface at boot.

Keep in mind that with the ifconfig method, you will have to manually run ifconfig again if the VM is powered off and on, but the OPNsense host is not:

ifconfig tap0 name bhyve0

This is because the interface gets destroyed and recreated with the default tap0 name.

Running Virtual Machines on OPNsense: My Final Thoughts

Running virtual machines directly on OPNsense using bhyve is an advanced but rewarding undertaking. It allows you to consolidate infrastructure and put underutilized hardware to work, all while keeping your firewall on bare metal for maximum reliability. While the process involves a lot of manual setup—especially around networking and boot configuration—it ultimately gives you a lightweight, headless VM host tightly integrated into your router.

Just remember that this is an unofficial and unsupported use case. Updates to OPNsense or FreeBSD may break things, so keep good backups and approach with a tinkerer’s mindset. But if you’re comfortable on the command line and like squeezing every drop of utility out of your hardware, this setup is a powerful way to do just that.

Remarks

  • vm list is a good command to help see loaded VMs and their status. You can also start and stop VMs with vm start vm-name and vm stop vm-name.
  • You will have to configure the network adapter settings for each VM you create and apply the firewall rules for each VM in the OPNsense UI as we did above.
  • It’s important to note the configuration differences between using the UEFI loader and the BIOS loader when setting up virtual machines on OPNsense, as stated throughout the article.
  • To see a sample VM configuration, you can take a look at an example on GitHub here.
  • Again, this is all unsupported. Follow at your own risk.
    • This is for advanced users only. None of it is manageable through the OPNsense UI, except firewall rules. Know what you’re doing in the terminal before following this guide.

The post Run Virtual Machines on OPNsense with bhyve appeared first on TechOpt.

]]>
https://www.techopt.io/servers-networking/run-virtual-machines-on-opnsense-with-bhyve/feed 0
How to Create a Custom Warning in Zabbix https://www.techopt.io/servers-networking/how-to-create-a-custom-warning-in-zabbix https://www.techopt.io/servers-networking/how-to-create-a-custom-warning-in-zabbix#respond Sun, 16 Mar 2025 03:53:55 +0000 https://www.techopt.io/?p=837 Zabbix actively monitors business infrastructure, but sometimes the built-in alerts do not cover all needs. In my case, I wanted to create a custom warning in Zabbix if a Btrfs snapshot hadn’t been completed in the last 24 hours on my Linux server. With this setup, I get an alert whenever my snapshot process fails […]

The post How to Create a Custom Warning in Zabbix appeared first on TechOpt.

]]>
Zabbix actively monitors business infrastructure, but sometimes the built-in alerts do not cover all needs. In my case, I wanted to create a custom warning in Zabbix if a Btrfs snapshot hadn’t been completed in the last 24 hours on my Linux server. With this setup, I get an alert whenever my snapshot process fails or experiences a delay.

Why Use Custom Warnings in Zabbix?

Zabbix provides various alerts by default, but I needed a custom warning to:

  • Ensure Btrfs snapshots are being created regularly
  • Get notified if the snapshot process fails
  • Integrate this check into my existing monitoring system

There are numerous various use-cases for custom warnings in Zabbix. Custom warnings provide greater flexibility, allowing you to adapt them to your specific monitoring needs.

Setting Up a Custom Warning in Zabbix

1. Create a Custom Script

First, I created a dedicated directory for custom scripts in Zabbix on my Linux host:

mkdir -p /etc/zabbix/scripts

Then, I created the script file and opened it for editing:

nano /etc/zabbix/scripts/check_btrfs_snapshot.sh

I added the following script to check if a Btrfs snapshot exists within the last 24 hours:

#!/bin/bash

SNAPSHOT_DIR="/mnt/btrfs/.snapshots"  # Adjust to your snapshot location
THRESHOLD=$(date -d '24 hours ago' +%s)

latest_snapshot=$(find "$SNAPSHOT_DIR" -maxdepth 1 -type d -printf '%T@ %p\n' | sort -nr | head -n1 | awk '{print $1}')

if [[ -z "$latest_snapshot" ]]; then
    echo 0  # No snapshots found
elif (( $(echo "$latest_snapshot >= $THRESHOLD" | bc -l) )); then
    echo 1  # Recent snapshot exists
else
    echo 0  # No recent snapshot
fi

After saving the script, I made it executable:

chmod +x /etc/zabbix/scripts/check_btrfs_snapshot.sh

2. Add the Script to Zabbix Agent 2

I’m using Zabbix Agent 2 for advanced features. Since Zabbix Agent 2 supports system.run, I modified zabbix_agent2.conf to add the script:

  1. Open the configuration file: nano /etc/zabbix/zabbix_agent2.conf
  2. Add this line to allow the script: AllowKey=system.run[/etc/zabbix/scripts/check_btrfs_snapshot.sh]
  3. Restart the agent: systemctl restart zabbix-agent2

3. Create a Custom Item in Zabbix

  1. Go to Zabbix Web Interface → Monitoring → Hosts
  2. Select the host where Btrfs is monitored (or where you’re adding your custom script) and click Items.
  3. Click Create Item and set your parameters:
    • Name: Btrfs Snapshot in the last 24 hours
    • Type: Zabbix Agent
    • Key: system.run[/etc/zabbix/scripts/check_btrfs_snapshot.sh]
    • Type of information: Text
    • Update interval: 30m
Parameters for custom zabbix item

I also recommend testing your script to make sure it runs properly with the Test option at the bottom of the Item UI from above.

4. Set Up a Custom Trigger

  1. Navigate to the Triggers tab.
  2. Click Create Trigger.
  3. Use this expression to trigger a warning if no snapshot exists in the last 24 hours:last(/example-hostname/system.run[/etc/zabbix/scripts/check_btrfs_snapshot.sh])=0.
    You can also use the “Add Condition UI” in the side panel by clicking Add to help generate the correct expression.
  4. Set severity (e.g., Warning).
  5. Click Add.
Zabbix custom trigger parameters to create custom warning

Final Thoughts

Setting up this custom warning in Zabbix ensures my Btrfs snapshots are created on schedule. If a snapshot fails, I get a warning immediately, allowing me to troubleshoot the issue before it becomes a bigger problem.

If you’re using Zabbix for monitoring, creating custom warnings like this can significantly improve your ability to track and respond to important system events. Try it out and enhance your monitoring experience!

The post How to Create a Custom Warning in Zabbix appeared first on TechOpt.

]]>
https://www.techopt.io/servers-networking/how-to-create-a-custom-warning-in-zabbix/feed 0
LXC Containers (CTs) vs. Virtual Machines (VMs) in Proxmox https://www.techopt.io/servers-networking/lxc-containers-vs-virtual-machines-in-proxmox https://www.techopt.io/servers-networking/lxc-containers-vs-virtual-machines-in-proxmox#respond Tue, 25 Feb 2025 02:40:15 +0000 https://www.techopt.io/?p=824 Proxmox is a powerful open-source platform that makes it easy to create and manage both LXC containers (CTs) and virtual machines (VMs). When considering LXC containers vs virtual machines in Proxmox, it’s essential to understand their differences and best use cases. When setting up a new environment, you might wonder whether you should deploy your […]

The post LXC Containers (CTs) vs. Virtual Machines (VMs) in Proxmox appeared first on TechOpt.

]]>
Proxmox is a powerful open-source platform that makes it easy to create and manage both LXC containers (CTs) and virtual machines (VMs). When considering LXC containers vs virtual machines in Proxmox, it’s essential to understand their differences and best use cases.

When setting up a new environment, you might wonder whether you should deploy your workload inside an LXC container or a full VM. The choice depends on what you are trying to achieve.

LXC Containers: Lightweight and Efficient

LXC (Linux Containers) provides an efficient way to run isolated environments on a Proxmox system. Unlike traditional VMs, containers share the host system’s kernel while maintaining their own isolated user space. This means they use fewer resources, start up quickly, and offer near-native performance.

When to Use LXC Containers:

  • Single Applications – If you need to run a single application in an isolated environment, an LXC container is an excellent choice.
  • Docker Workloads – If an application is only available as a Docker image, you can run Docker inside an LXC container, avoiding the overhead of a full VM.
  • Resource Efficiency – LXC containers consume fewer resources, making them ideal for lightweight applications that don’t require their own kernel.
  • Speed – Since LXC containers don’t require full emulation, they start almost instantly compared to VMs.

Considerations for LXC Containers:

  • Less Isolation – Since they share the host kernel, they are not as isolated as a full VM, which can pose security risks if an attacker exploits vulnerabilities in the kernel or improperly configured permissions.
  • Compatibility Issues – Some applications that expect a full OS environment may not work well inside an LXC container.
  • Limited System Control – You don’t have complete control over kernel settings like you would in a VM.

Virtual Machines: Full System Isolation

Virtual machines in Proxmox use KVM (Kernel-based Virtual Machine) technology to provide a fully virtualized system. Each VM runs its own operating system with its own kernel, making it functionally identical to a physical machine.

When to Use Virtual Machines:

  • Multiple Applications Working Together – If you need to run a system with multiple interacting services, a VM provides a fully isolated environment.
  • Custom Kernel or OS Requirements – If your application requires a specific kernel version or a non-Linux operating system (e.g., Windows or BSD), a VM is the way to go.
  • Strict Security Requirements – Since VMs have strong isolation from the host system, they provide better security for untrusted workloads.
  • Compatibility – Any software that runs on a physical machine will run in a VM without modification.

Considerations for Virtual Machines:

  • Higher Resource Usage – VMs require more CPU, RAM, and disk space compared to containers.
  • Slower Start Times – Because they emulate an entire system, VMs take longer to boot up.
  • More Maintenance – You’ll need to manage full OS installations, updates, and security patches for each VM separately.

Final Thoughts: When to Choose LXC Containers vs. Virtual Machines in Proxmox

In general, if you need to run a single application in isolation, or if your application is only available as a Docker image, an LXC container is the better choice. Containers are lightweight, fast, and efficient. However, if you’re running a more complex system with multiple interacting applications, need complete OS independence, or require strong isolation, a VM is the better solution.

Proxmox makes it easy to work with both LXC and VMs, so understanding your workload’s needs will help you choose the right tool for the job. By leveraging the strengths of each, you can optimize performance, security, and resource usage in your environment.

The post LXC Containers (CTs) vs. Virtual Machines (VMs) in Proxmox appeared first on TechOpt.

]]>
https://www.techopt.io/servers-networking/lxc-containers-vs-virtual-machines-in-proxmox/feed 0
Importing a QCOW2 Image in Proxmox https://www.techopt.io/servers-networking/importing-a-qcow2-image-in-proxmox https://www.techopt.io/servers-networking/importing-a-qcow2-image-in-proxmox#respond Tue, 31 Dec 2024 17:01:00 +0000 https://www.techopt.io/?p=678 If you’re setting up a virtual machine with a QCOW2 image in Proxmox, you might feel overwhelmed at first. However, understanding the steps can make it a manageable task. This guide will explain how to import a QCOW2 image into Proxmox, including uploading the file using SFTP via the command line. Working with QCOW2 in […]

The post Importing a QCOW2 Image in Proxmox appeared first on TechOpt.

]]>
If you’re setting up a virtual machine with a QCOW2 image in Proxmox, you might feel overwhelmed at first. However, understanding the steps can make it a manageable task. This guide will explain how to import a QCOW2 image into Proxmox, including uploading the file using SFTP via the command line.

Working with QCOW2 in Proxmox setups involves a few terminal commands and some configuration in the Proxmox web interface, but it’s straightforward once you’ve done it once.

Steps to Import a QCOW2 Image in Proxmox

1. Access Your Proxmox Terminal

Log in to the terminal of your Proxmox host. This can be done directly or via SSH. The terminal is essential for transferring and managing files during the setup process.

2. Prepare the QCOW2 Storage Directory

Store QCOW2 images in a dedicated directory for better organization. Proxmox doesn’t create this directory by default, so you may need to set it up. A good place to store QCOW2 images is in /var/lib/vz/template/qcow.

  • Check if the directory exists:

    ls /var/lib/vz/template/qcow

  • If it doesn’t exist, create it:

    mkdir -p /var/lib/vz/template/qcow

3. Upload the QCOW2 Image Using SFTP

Uploading the QCOW2 image requires an SFTP connection. Here’s how to do it using the command line:

  1. Open a terminal on your local machine.
  2. Use the sftp command to connect to your Proxmox host. Replace user with your Proxmox username and host with the IP address of your Proxmox server:

    sftp user@host

  3. Navigate to the directory where you want to upload the image:

    cd /var/lib/vz/template/qcow

  4. Upload the QCOW2 file from your local system:

    put /path/to/your_image.qcow2

    Replace /path/to/your_image.qcow2 with the actual path to the QCOW2 file on your local machine.
  5. Exit the SFTP session with bye.
uploading qcow2 images to proxmox with sftp

4. Create a New Virtual Machine

Now, create a VM in Proxmox using the web interface:

  1. Log in to the Proxmox web interface.
  2. Select Create VM and configure the basic settings, such as the name and operating system. Make sure to select Do not use any media on the OS tab.
  3. On the Disks tab, leave all settings at their default values:
    • The default settings work because QCOW2 images already define their initial drive size. If needed, you can expand the drive size later.
    • Select any bus type for now, as you will replace this with the QCOW2 disk.
Do not use any media when uploading qcow2 image to proxmox

5. Import the QCOW2 Image

After creating the VM, use the qm importdisk command to import the QCOW2 image to the VM.

  1. Identify the VM’s ID from the Proxmox web interface (e.g., 120).
  2. Run the following command in the Proxmox terminal:

    qm importdisk 120 /var/lib/vz/template/qcow/your_image.qcow2 local

This imports the QCOW2 image as an unused disk linked to the VM.

Make sure to replace the VM ID, image location and storage name with the values for your system.

Here is a sample output from the command:

importing disk '/var/lib/vz/template/qcow/haos_ova-14.1.qcow2' to VM 120 ...
transferred 0.0 B of 32.0 GiB (0.00%)
transferred 396.5 MiB of 32.0 GiB (1.21%)
transferred 737.3 MiB of 32.0 GiB (2.25%)
transferred 1.1 GiB of 32.0 GiB (3.36%)
transferred 1.4 GiB of 32.0 GiB (4.47%)
transferred 1.8 GiB of 32.0 GiB (5.59%)
transferred 2.1 GiB of 32.0 GiB (6.70%)
transferred 2.5 GiB of 32.0 GiB (7.81%)
transferred 2.9 GiB of 32.0 GiB (8.92%)
...
transferred 20.9 GiB of 32.0 GiB (65.44%)
transferred 21.3 GiB of 32.0 GiB (66.56%)
transferred 21.7 GiB of 32.0 GiB (67.67%)
transferred 22.0 GiB of 32.0 GiB (68.78%)
transferred 22.4 GiB of 32.0 GiB (69.89%)
transferred 22.7 GiB of 32.0 GiB (71.01%)
transferred 32.0 GiB of 32.0 GiB (100.00%)
unused0: successfully imported disk 'VMs:vm-120-disk-3'

In this case, this created a new 32.0 GiB drive specified by our image that we can attach to our VM.

6. Attach the Disk to the VM

Once imported, attach the QCOW2 image to the VM:

  1. In the Proxmox web interface, go to the VM and select Hardware.
  2. Find the unused disk and attach it as a VirtIO Block Device or a SATA device (if your guest OS doesn’t support QEMU agent and VirtIO drivers).
  3. Optionally, remove or detach the original disk created during VM setup if it’s no longer needed.
Add the unused disk created from the qcow2 image in proxmox

7. Update the Boot Order

Ensure the VM boots from the newly attached QCOW2 image:

  1. Go to the Options tab in the VM’s settings.
  2. Set the newly attached VirtIO disk as the primary boot device.
Change the primary boot entry to the imported qcow2 image attached drive

8. Start the VM

Boot up the VM. If everything is set up correctly, the system should load from the QCOW2 image!

Remarks

  • When creating the VM, default disk settings are sufficient since the QCOW2 image specifies its own initial drive size. You can expand the size later if needed.
  • The sftp command is a straightforward way to upload the QCOW2 file. If you prefer a graphical interface, tools like FileZilla can achieve the same result.
  • Use the /var/lib/vz/template/qcow directory to keep QCOW2 images organized and accessible for future use. You can use any directory, but this is an easy place to remember since it’s with other templates.
  • You can also use any other method to upload your QCOW2 images to this directory, or even download them to this directory directly with a tool such as wget.

The post Importing a QCOW2 Image in Proxmox appeared first on TechOpt.

]]>
https://www.techopt.io/servers-networking/importing-a-qcow2-image-in-proxmox/feed 0
pfSense to OPNsense Migration of a Router/Firewall https://www.techopt.io/servers-networking/pfsense-to-opnsense-migration-of-a-router-firewall https://www.techopt.io/servers-networking/pfsense-to-opnsense-migration-of-a-router-firewall#respond Tue, 13 Aug 2024 03:29:26 +0000 http://localhost:8080/?p=55 I had been using pfSense Community Edition on my home router and firewall for years without any problems. In fact, it served me very well for several years. But after using OPNsense at work, I decided it was time to migrate my router/firewall away from pfSense to OPNsense. OPNsense was forked from pfSense in 2015. […]

The post pfSense to OPNsense Migration of a Router/Firewall appeared first on TechOpt.

]]>
I had been using pfSense Community Edition on my home router and firewall for years without any problems. In fact, it served me very well for several years. But after using OPNsense at work, I decided it was time to migrate my router/firewall away from pfSense to OPNsense.

OPNsense was forked from pfSense in 2015. The development team actively updates it and has given it a modern UI. Over time, OPNsense has deviated from pfSense and matured into its own operating system, led by its own team of developers and decision-makers. This makes it similar to, but not identical to pfSense.

As such, migration does require some planning, since not everything in OPNsense is identical to pfSense.

Motivations for Migrating from pfSense to OPNsense

pfSense was great and modern when I first started using it, but over the years it became very clunky. Updates to the Community Edition also seem to have slowed. The interface is dated. There was also a bit of a scandal with their original WireGuard implementation. This left a bit of a bad taste for me, considering I already used OpenVPN frequently through pfSense and would have liked to setup WireGuard as well.

Unfortunately, because OPNsense has deviated from pfSense so much, there is no direct way to migrate from pfSense to OPNsense. This means you must port pretty much all configuration manually. This may or may not be a big deal, depending how much you rely on services running on your pfSense box.

Planning the Migration

In my case, I relied on my pfSense box for the following services:

  • Router
  • Firewall
  • Squid caching HTTP proxy
  • HAProxy reverse proxy for many services, both internal and external
  • OpenVPN
  • WireGuard
  • ACME Client (SSL Renewal)

I needed to port the configurations of each service to OPNsense. I also had a few (loose) requirements for the migration:

  • The migration shall appear transparent to all other clients on the network.
  • Behaviour of services shall remain unaffected before and after the migration.
  • Clients shall not have to do any reconfiguration.
  • There shall be minimal downtime, ideally less than 30 minutes. My household loves our internet!

Migrating from pfSense to OPNsense

These are the steps I took to migrate my pfSense box to OPNsense. You can use this as a guide, but some steps will vary depending on your setup and the tools you have available.

The steps I took involved creating a virtual machine and installing OPNsense, configuring OPNsense in the virtual machine and exporting the configuration as an OPNsense configuration file. This way, I could install OPNsense on my bare metal router box and simply import the configuration, all ready to go.

1. Creating an OPNsense Virtual Machine

To start, I created a virtual machine in Proxmox to simulate my router box. The important thing was to give it 2 network interfaces, so I could simulate the WAN and LAN ports on the physical box.

specs assigned to OPNsense vm in proxmox
The specs I assigned to my OPNsense VM in Proxmox

Next, I installed OPNsense in the virtual machine. By default, OPNsense assigns a new subnet on the LAN interface after installation. To access the OPNsense configuration page, I needed to give the LAN interface a static IP address in my current network range.

My pfSense box has an IP address of 172.16.0.1, so I decided to give my OPNsense VM LAN interface an IP address of 172.16.0.201 so I could easily remember it. This interface will eventually take over the IP address of the pfSense box when porting is complete.

I assigned a static IP in my LAN range to the LAN interface, and used the DHCP address to simulate WAN
I assigned a static IP in my LAN range to the LAN interface, and used the DHCP address to simulate WAN

2. Using Both Admin Panels to Convert pfSense Configuration to OPNsense Configuration

First, I logged into both my pfSense box at 172.16.0.1, and my OPNsense virtual machine at 172.16.0.201. I checked which external software packages I had installed on my pfSense box and installed the equivalents on OPNsense.

Next, I started going through each relevant pfSense configuration page, finding the corresponding page in OPNsense and manually porting the configuration values. I started with basic network configuration such as hostname and routing, then firewall rules, and finally external software packages.

Squid config page in pfsense
One of the pfSense config pages (Squid)

This was a long and tedious process with so much network configuration and so many service configurations to port. Since OPNsense is a fork of pfSense after all, a lot of the configuration pages are almost identical, but with a new look.

However, there were some notable mentions where the configuration pages did differ quite a bit and I had to figure out the equivalent configuration from pfSense to OPNsense. This was especially true for HAProxy, which I left until last, since I knew it would be the biggest pain.

OPNsense general information page
OPNsense general information page

I’ll outline my general experience with each configuration and service I ended up porting.

2.1. Hostname, Interfaces, DHCP, Routing & Firewall

These categories were super easy since the structure of the configuration pages for hostname, interfaces, DHCP, routing and firewall are almost identical between pfSense and OPNsense. I set the hostname, turned off DHCP (I have another DHCP server on my network), recreated all static routes in my route table (which weren’t many), and recreated all firewall rules (again, not many).

2.2. Squid Caching HTTP Proxy, WireGuard, OpenVPN and ACME Client

This category of configurations was relatively easy to port as well, with the only caveat being that some of the options were separated into different tabs and hidden under an Advanced flag. Still, they were all pretty easy to work with.

The WireGuard page was notably slightly different, mostly due to the scandal I mentioned above, but the equivalent options were easy enough to find.

2.3. HAProxy Reverse Proxy

This one was the biggest pain. I had about 20 different services running through this proxy using pfSense. The way HAProxy is configured on pfSense is totally different from the way it’s configured on OPNsense. My configuration on pfSense was already pretty elaborate.

The OPNsense HAProxy configuration does a better job of separation of concerns over pfSense’s implementation. Different pages are used for rules and checks, whereas these are defined with your frontend configuration in pfSense.

This also did make configuration on OPNsense a bit more tedious, but a lot cleaner in the long run. Overall, I spent about an hour porting everything and making sure it worked correctly.

2.3.1. Testing the HAProxy Configuration

Testing the services proxied by HAProxy and making sure SSL worked correctly was also a bit of a pain, but since I run Linux I was able to temporarily edit my /etc/hosts file to point to 172.16.0.201, the LAN IP of my OPNsense VM, for whichever service I was testing as I ported each one.

If you do this, just make sure you are familiar with /etc/hosts and know what you’re doing.

3. Exporting the OPNsense Configuration File

Finally, I exported the OPNsense configuration file, saved it to my computer and my USB drive (just in case) and shutdown the virtual machine.

At this point, I also exported my pfSense configuration file, just in case something was to happen and I had to quickly revert to pfSense for the time being.

It’s important to note that these two files are not interchangeable and it’s important not to mix them up.

4. Installing OPNsense on my Hardware Router Box

I added the OPNsense ISO to my Ventoy USB and booted the installer on my hardware router box. After installation, I gave the router LAN address an IP address of 172.16.0.1, the same IP address that the router was assigned when it ran on pfSense.

5. Applying the New OPNsense Configuration

Next, I went to back to my computer and went to the OPNsense configuration page at 172.16.0.1. From there, I simply applied the configuration file I had downloaded to my computer earlier, gave it a moment and logged back into the OPNsense configuration page at 172.16.0.201.

Note that the IP address I’m using to login is still the IP address I assigned the LAN interface in the VM, because that’s what it was in the exported configuration file.

I changed the LAN IP address to 172.16.0.1 from the configuration page and rebooted.

After a few minutes, I went to the admin page at the new IP address of 172.16.0.1. I just needed to manually install all the packages in the OPNsense VM from the plugins page. The UI highlights the missing plugins, allowing you to easily reinstall them.

plugins labeled missing after config restore opnsense
The OPNsense UI highlights missing plugins so you can easily install them after restore

Thankfully the configuration for all these plugins is saved in the OPNsense configuration file, so I gave the router one last reboot to make sure everything was up and running correctly.

6. Fully Testing the Migration of pfSense to OPNsense

That was it! We had internet access. I was able to access all my internal HAProxy services with SSL working using the original URLs as expected.

To test external HAProxy services, WireGuard and OpenVPN, I used my data connection on my phone. All were working as expected.

Conclusion

All in all, it took me about 2 hours to port all the configuration from pfSense to OPNsense manually using the admin panels, and then about 20 minutes of internet downtime to install OPNsense and restore the configuration file on my bare metal router box. Not bad! It’s not a hard process once you figure out a good plan, just tedious.

I also suggest you pick OPNsense over pfSense Community Edition for a new build, since it’s much more modern and updated more regularly.

Remarks

  • I used Proxmox for my OPNsense virtual machine used to create my configuration, but you can do the same thing with VirtualBox or Hyper-V.
  • If you’re having trouble accessing the OPNsense administration page after restoring your configuration file on your router box, you should try both the IP address you assigned in the VM, and the IP address you assigned via the console upon first boot on your actual hardware (172.16.0.201 and 172.16.0.1 in my case, respectively).
  • If you were wondering about the hardware in my router box, it’s made from some old PC parts mounted in a server rack: an AMD A8-6600K CPU, 8 GB of RAM and a 120 GB SSD with an Intel dual NIC. Nothing fancy by today’s standards, but even so it’s still a bit overkill for this purpose.

The post pfSense to OPNsense Migration of a Router/Firewall appeared first on TechOpt.

]]>
https://www.techopt.io/servers-networking/pfsense-to-opnsense-migration-of-a-router-firewall/feed 0
Free Extra Disk Space Used by Docker https://www.techopt.io/programming/free-extra-disk-space-used-by-docker https://www.techopt.io/programming/free-extra-disk-space-used-by-docker#respond Tue, 16 Jul 2024 00:52:42 +0000 http://localhost:8080/?p=116 If you’ve been using Docker for awhile, you’ll likely notice that your free disk space keeps shrinking, specifically after pulling and updating images. This is because by default, Docker stores images and volumes even if they’re not in use anymore. If you’ve updated your containers, you may also have old versions of images stored that […]

The post Free Extra Disk Space Used by Docker appeared first on TechOpt.

]]>
If you’ve been using Docker for awhile, you’ll likely notice that your free disk space keeps shrinking, specifically after pulling and updating images. This is because by default, Docker stores images and volumes even if they’re not in use anymore. If you’ve updated your containers, you may also have old versions of images stored that you no longer need.

To free up some storage, we should delete these unnecessary images and volumes. We can use the docker system prune command to do so.

Free Disk Space by Pruning Unused Images

To remove unused images, use the following command:

docker system prune -a -f

The -a flag removes all unused images, and the -f flag skips confirmation.

Free Disk Space by Pruning Unused Volumes

Warning! This command may delete data created by the containers if the container volumes you want to keep aren’t in use. Therefore, use it at your own risk.

To remove unused volumes, use the following command:

docker system prune --volumes -f

The --volumes flag removes all unused volumes, and the -f flag skips confirmation.

Pruning Both Unused Images & Volumes

Warning! This command may delete data created by the containers if the container volumes you want to keep aren’t in use. Use it at your own risk.

To remove unused images and volumes together, use the following command:

docker system prune -a --volumes -f

The -a flag removes all unused images, the --volumes flag removes all unused volumes, and the -f flag skips confirmation.

Scheduling Pruning of Unused Images & Volumes

Unfortunately, at this time there is no way that is built into Docker to periodically prune unused images and volumes automatically.

However, if you’re on a *nix system, BSD system or macOS, you can use cron to periodically run one of the commands above periodically. My cron expression builder can produce a weekly Sunday 03:00 line if you would rather not guess the five fields.

If you’re on Windows, you can use Task Scheduler to trigger one of the commands above.

Conclusion

By pruning unused images and volumes using the docker system prune command, you can often save tens of gigabytes of storage!

To see all the available flags for this command, please refer to the Docker documentation.

The post Free Extra Disk Space Used by Docker appeared first on TechOpt.

]]>
https://www.techopt.io/programming/free-extra-disk-space-used-by-docker/feed 0