Setting Up an AI Server on Proxmox 9.x Using LXC and Docker

This guide walks through setting up a local AI server on Proxmox VE 9.x. The finished setup uses a Debian 13 LXC container to run Docker, Dockge, and llama.cpp, with an NVIDIA GPU available to the llama.cpp server.

The example uses a Qwen3.5 4B GGUF model, but the same setup can be adapted for other models and supported hardware. This guide starts with an existing Proxmox VE installation; it does not cover installing Proxmox itself.

To make it clear where each command belongs, the steps identify whether you should run it on the Proxmox host, inside the Debian LXC, or in the Dockge web interface.

1. Prepare the Proxmox Host

The first step is to prepare the Proxmox host for the NVIDIA driver and create the shared directory that will hold model files and presets.

Run the Proxmox post-install script

I recommend running the Proxmox post-install script from the Community Scripts project. It takes care of several common post-install tasks.

Proxmox VE Post Install

Run on the Proxmox host:

bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pve-install.sh)"

Install the required packages

Install the packages needed to build and load the NVIDIA driver. The package list includes the headers for the currently running Proxmox kernel.

Run on the Proxmox host:

apt install g++ freeglut3-dev build-essential libx11-dev libxmu-dev libxi-dev libglu1-mesa-dev libfreeimage-dev libglfw3-dev wget htop btop nvtop nano glances git pciutils cmake curl libcurl4-openssl-dev pve-headers-$(uname -r) dkms make -y

Install the NVIDIA driver

This guide uses NVIDIA driver version 580.196.07. Copy the driver installer to the Proxmox host. The example command uses 192.168.150.49 as the host address; change it to match your system.

Run from the machine that has the installer:

scp NVIDIA-Linux-x86_64-580.196.07-no-compat32.run root@192.168.150.49:/root/

Connect to the Proxmox host, make the installer executable, and install the driver with DKMS enabled.

Run on the Proxmox host:

chmod +x NVIDIA-Linux-x86_64-580.196.07-no-compat32.run
./NVIDIA-Linux-x86_64-580.196.07-no-compat32.run --dkms

Update the initramfs and reboot:

update-initramfs -u && reboot now

After the host comes back up, run the installer a second time:

./NVIDIA-Linux-x86_64-580.196.07-no-compat32.run --dkms

Use nvtop to verify that the NVIDIA driver is loaded and the GPU is visible:

nvtop

Configure NVIDIA persistence and memory locking

Enable NVIDIA persistence mode and set the power limit at boot. Edit root’s crontab:

crontab -e

Add this line:

@reboot nvidia-smi -pm 1 && nvidia-smi -pl 250

Next, configure memory locking limits:

nano /etc/security/limits.conf

Add these lines:

* soft memlock unlimited
* hard memlock unlimited
root soft memlock unlimited
root hard memlock unlimited

Save the file and reboot the host for the memory-locking changes to take effect.

Create the shared models directory

The model files and preset configuration will be stored in /opt/models. This directory will later be mounted inside the LXC container.

Run on the Proxmox host:

mkdir /opt/models

2. Create and Configure the Debian LXC

The AI services will run inside a Debian 13 LXC container. This setup uses a privileged container and enables nesting so Docker can run inside it.

Download the Debian 13 template

In the Proxmox web interface, select your node and then select the storage where you want to keep the template. Open CT Templates, click Templates, and download the latest Debian 13 template.

The template is stored on the selected Proxmox storage. You only need to download it once for each storage location where you plan to create containers.

Create the container

Create a new container and select the Debian 13 template. Make sure Unprivileged container is unchecked. This guide uses a privileged container.

When assigning CPU cores, count physical cores rather than hyperthreading threads. For example, an 8-core/16-thread processor has 8 physical cores, not 16. You can assign up to 8 cores to the container, depending on how many you want to reserve for Proxmox and other workloads.

Allocate the amount of memory you want the container to use. Complete the remaining settings and create the container, but do not start it yet.

Enable nesting

There is a Proxmox quirk worth knowing: after you select a privileged container, the Nesting option cannot be enabled in the creation wizard. Create the container first, then open Options → Features and enable Nesting.

Add GPU device access

Before starting the container, add the GPU device entries. The exact device names and numbers depend on your hardware. This is an example for a system with one NVIDIA GPU:

/dev/nvidia0
/dev/nvidiactl
/dev/nvidia-uvm
/dev/nvidia-uvm-tools
/dev/nvidia-caps/nvidia-cap1
/dev/nvidia-caps/nvidia-cap2
/dev/dri/card0
/dev/dri/renderD128

Set the device permissions to 0666. Systems with multiple GPUs will have additional NVIDIA device entries. The DRI devices are also needed for Vulkan access.

Configure the LXC

Open the container configuration file on the Proxmox host. Replace <CTID> with the container ID:

Run on the Proxmox host:

nano /etc/pve/lxc/<CTID>.conf

Add the following configuration:

# allow lxc to pin large memory blocks
lxc.prlimit.memlock: unlimited

# mount shared models directory
mp0: /opt/models,mp=/opt/models

# cpu pinning
lxc.cgroup2.cpuset.cpus: 0-5

The CPU pinning example assigns logical CPU IDs 0 through 5 to the container. Adjust this range to match your system and the CPUs you want to reserve for the container.

Start the container and copy the driver installer

Start the LXC container from the Proxmox web interface. Then copy the NVIDIA installer from the Proxmox host into the running container.

Run on the Proxmox host:

pct push <CTID> /root/NVIDIA-Linux-x86_64-580.196.07-no-compat32.run /root/NVIDIA-Linux-x86_64-580.196.07-no-compat32.run

3. Prepare Debian and Install the NVIDIA Driver

The next steps are performed inside the Debian LXC container.

Update Debian and install dependencies

Open a shell in the Debian container and update the system:

Run inside the Debian LXC:

apt update && apt upgrade -y

Install the packages required for NVIDIA tools, Vulkan, and the container setup:

apt update && apt install nvtop libvulkan1 mesa-vulkan-drivers vulkan-tools curl gpg gnupg2 sudo libxext6 libegl1 -y

The libegl1 package is important for this setup. It must be installed in the LXC container; it is not added to the llama.cpp Docker image.

Install the NVIDIA driver in the LXC

The NVIDIA driver version inside the LXC should match the driver installed on the Proxmox host. Run the installer without kernel modules because the host provides the kernel driver.

chmod +x NVIDIA-Linux-x86_64-580.196.07-no-compat32.run

Run the installer:

./NVIDIA-Linux-x86_64-580.196.07-no-compat32.run --no-kernel-modules

Verify NVIDIA and Vulkan access

Check that the NVIDIA driver is visible inside the container:

nvidia-smi

Then check Vulkan:

vulkaninfo --summary

4. Install NVIDIA Container Toolkit

The NVIDIA Container Toolkit allows Docker containers to access the NVIDIA GPU.

Add the NVIDIA package repository

Create the APT keyrings directory:

Run inside the Debian LXC:

mkdir -p /etc/apt/keyrings

Download and install the NVIDIA repository key:

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o /etc/apt/keyrings/nvidia-container-toolkit-keyring.gpg

Add the NVIDIA Container Toolkit repository:

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list -o /etc/apt/sources.list.d/nvidia-container-toolkit.list

Configure the repository to use the keyring:

sed -i 's#deb https://#deb [signed-by=/etc/apt/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' /etc/apt/sources.list.d/nvidia-container-toolkit.list

Update APT and install the toolkit:

apt-get update && apt-get install -y nvidia-container-toolkit

5. Install Docker

Docker will run inside the Debian LXC container. The following steps add Docker’s official Debian repository and install Docker Engine with the Compose plugin.

Add the Docker package repository

Run inside the Debian LXC:

apt update
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

Create the Docker repository definition:

cat > /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

Install and configure Docker

Install Docker Engine and the Compose plugin:

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Configure Docker to use the NVIDIA runtime and restart Docker:

nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

Generate the NVIDIA CDI device specification:

nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml

This creates the device specification Docker can use to expose NVIDIA GPUs to containers.

6. Install Dockge

Dockge provides a web interface for managing Docker Compose stacks.

Deploy Dockge

Run inside the Debian LXC:

mkdir -p /opt/stacks /opt/dockge
cd /opt/dockge

Download the Dockge Compose file:

curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --output compose.yaml

Start Dockge:

docker compose up -d

Open Dockge in a browser:

http://<LXC-IP-ADDRESS>:5001

Create the administrator account when prompted.

7. Download the Qwen Model

The model files are stored in /opt/models, which is shared between the Proxmox host and the LXC container. You can download the model from either the Proxmox host shell or the LXC shell.

This example uses the Qwen3.5 4B GGUF model from Unsloth:

Unsloth Qwen3.5 4B GGUF

Download the Q4_K_M file:

cd /opt/models
wget https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf

Verify that the file is present:

ls -lh /opt/models/Qwen3.5-4B-Q4_K_M.gguf

8. Configure Model Presets

llama.cpp model presets let you define different configurations for CPU and GPU testing. The preset file is stored in the shared /opt/models directory, so you can create or edit it from either the Proxmox host shell or the LXC shell.

Create the preset file

nano /opt/models/mini-server.ini

Add the following configuration:

#
# llama-server --models-preset config.ini --host 127.0.0.1 --port 8080 --models-max 1
#
# This is a master file of different models that have been tested.
# Comment and uncomment as needed.
#

# =========================================================
# Global default fallback settings for your system
# =========================================================
[*]
b = 64
ub = 64
cache-prompt = true
cache-type-k = q4_0
cache-type-v = q4_0
context-shift = true
cont-batching = true
cram = 1024
ctx-checkpoints = 32
ctx-size = 8192
flash-attn = on
kv-unified = true
np = 1
prio = 2
t = 4
tb = 4

# =========================================================
# Test models - Comment out models when done.
# =========================================================
# Pure CPU Test
[test-qwen3.5_4B-CPU]
model = /models/Qwen3.5-4B-Q4_K_M.gguf 
gpu-layers = 999
ctx-size = 16384
load-mode = none
split-mode = none
device = none
t = 6
tb = 6

# GPU 0 Test
[test-qwen3.5_4B-GPU0]
model = /models/Qwen3.5-4B-Q4_K_M.gguf 
gpu-layers = 999
ctx-size = 16384
no-host = 1
load-mode = mlock
split-mode = none
device = Vulkan0

# GPU 1 Test
[test-qwen3.5_4B-GPU1]
model = /models/Qwen3.5-4B-Q4_K_M.gguf 
gpu-layers = 999
ctx-size = 16384
no-host = 1
load-mode = mlock
split-mode = none
device = Vulkan1

# GPU 2 Test
#[test-qwen3.5_4B-GPU1]
#model = /models/Qwen3.5-4B-Q4_K_M.gguf 
#gpu-layers = 999
#ctx-size = 16384
#no-host = 1
#load-mode = mlock
#split-mode = none
#device = Vulkan2

# GPU 0 + 1 Test
[test-qwen3.5_4B-dual_GPUs]
model = /models/Qwen3.5-4B-Q4_K_M.gguf 
gpu-layers = 999
ctx-size = 32768
no-host = 1
load-mode = mlock
split-mode = layer
device = Vulkan0,Vulkan1

# GPU 0 + 1 + 2 Test
#[test-qwen3.5_4B-GPU1]
#model = /models/Qwen3.5-4B-Q4_K_M.gguf 
#gpu-layers = 999
#ctx-size = 16384
#no-host = 1
#load-mode = mlock
#split-mode = none
#device = Vulkan0,Vulkan1,Vulkan2

The [*] section defines global defaults. Each model section inherits those settings and can override them. Uncomment the preset you want to test and comment out the other test presets. The preset file is passed to llama.cpp when the Docker container starts.

9. Deploy the llama.cpp Server

Use Dockge to create a new stack for llama.cpp. The Compose configuration below uses the Vulkan server image. To use the CUDA image instead, uncomment that image and comment out the Vulkan image. Keep only one image line active.

The NVIDIA runtime is enabled for NVIDIA GPUs. The optional cpuset setting can be enabled if the LXC container has not already been pinned to specific physical cores. Adjust the CPU range for your system.

Create the llama.cpp stack

In the Dockge web interface: create a new stack and use this Compose configuration:

services:
  ai-stack:
    container_name: ai-stack
    # uncomment only one of the image lines below.
    # image: ghcr.io/ggml-org/llama.cpp:server-cuda
    image: ghcr.io/ggml-org/llama.cpp:server-vulkan
    # uncomment only one of the runtimes below. Nvidia GPUs need
    # the nvidia runtime.
    runtime: nvidia
    #runtime: runc
    restart: unless-stopped
    # uncomment below if your lxc isn't pinned to the physical cores.
    # You will need to adjust to the number of cores you have.
    # cpuset: 0-5
    ports:
      - 8080:8080
    volumes:
      - /opt/models:/models
    devices:
      - /dev/dri/card0:/dev/dri/card0
      - /dev/dri/renderD128:/dev/dri/renderD128
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - NVIDIA_DRIVER_CAPABILITIES=all
      - GGML_VK_FORCE_MAX_ALLOCATION=1
      # - __GL_THREADED_OPTIMIZATIONS=0
    ulimits:
      memlock:
        soft: -1
        hard: -1
    command: >
      --host 0.0.0.0 --port 8080 --models-preset /models/mini-server.ini -lv 3 --models-max 1
networks: {}

What the Compose configuration does

  • Selects the llama.cpp Vulkan server image. The CUDA image is available as an alternative.
  • Uses the NVIDIA runtime for NVIDIA GPUs.
  • Publishes the server on port 8080.
  • Mounts the shared /opt/models directory inside the container as /models.
  • Passes the DRI devices through for Vulkan access.
  • Makes NVIDIA devices and driver capabilities available to the container.
  • Sets unlimited memory locking for the container.
  • Loads model presets from /models/mini-server.ini and limits the server to one loaded model with --models-max 1.

Save and deploy the stack in Dockge.

10. Test the llama.cpp Installation

Once the stack has started, check the logs in Dockge and confirm that llama.cpp starts without errors.

From the Debian LXC shell, you can also check GPU visibility:

nvidia-smi

Open the llama.cpp web interface in a browser:

http://<LXC-IP-ADDRESS>:8080

The web interface should load and allow you to select one of the configured model presets. Select the Qwen3.5 4B preset and start a conversation. The first request loads the model, so allow time for the model to finish loading.

While the model is running, use nvtop in the LXC shell to monitor GPU memory use and activity.

Conclusion

You now have a working foundation for running local AI models on a Proxmox server. Proxmox manages the hardware, while a dedicated Debian LXC container provides an environment for Docker, NVIDIA Container Toolkit, Dockge, and llama.cpp.

The shared /opt/models directory makes it easy to manage model files and presets without copying them into individual containers. Dockge provides a convenient web interface for managing the Docker Compose stacks.

From here, you can add more models, create presets for different GPU configurations, and experiment with model sizes and quantization levels as your hardware allows. This gives you a flexible platform for running and testing local language models without depending on cloud-based AI services.

Create a Simple Proxmox Host Dashboard

Introduction

I wanted a simple landing page for my Proxmox servers. Instead of remembering the addresses and ports for each server and service, I wanted to enter a hostname in my browser and get a page with links to everything I use regularly.

The solution is a basic HTML dashboard served directly from the Proxmox host. There is no need to install another web server or run an additional container. We will use Python to serve the page and systemd to keep the service running.

The dashboard will listen on port 80, leaving the Proxmox management interface on port 8006. We will also configure DNS so the dashboard can be accessed using a hostname.

1. Create the Dashboard Directory

SSH into your Proxmox host with an account that has root privileges.

Create a directory for the dashboard.

mkdir -p /var/www

2. Create the Dashboard HTML File

Create the HTML file.

nano -w /var/www/host-dashboard.html

Paste the following HTML into the file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Local AI Server Dashboard</title>

    <style>
        :root {
            color-scheme: dark;
            --background: #111318;
            --card-background: #1b1e25;
            --card-border: #303541;
            --text: #f1f3f5;
            --muted: #a0a7b4;
            --accent: #e04444;
            --accent-hover: #ff5b5b;
        }

        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            min-height: 100vh;
            background: var(--background);
            color: var(--text);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
                Roboto, Helvetica, Arial, sans-serif;
        }

        header {
            padding: 3rem 1.5rem 2rem;
            text-align: center;
            border-bottom: 1px solid var(--card-border);
        }

        header h1 {
            margin: 0;
            font-size: clamp(2rem, 5vw, 3rem);
            font-weight: 700;
            letter-spacing: -0.04em;
        }

        header p {
            margin: 0.8rem 0 0;
            color: var(--muted);
            font-size: 1rem;
        }

        main {
            width: min(1100px, 100%);
            margin: 0 auto;
            padding: 2.5rem 1.5rem;
        }

        .section-heading {
            margin: 0 0 1.5rem;
            font-size: 1.2rem;
            font-weight: 600;
        }

        .services-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 1.25rem;
        }

        .service-card {
            display: flex;
            align-items: flex-start;
            gap: 1rem;
            padding: 1.5rem;
            background: var(--card-background);
            border: 1px solid var(--card-border);
            border-radius: 12px;
            text-decoration: none;
            color: var(--text);
            transition: border-color 0.2s ease,
                        transform 0.2s ease,
                        background 0.2s ease;
        }

        .service-card:hover {
            border-color: var(--accent);
            background: #20242d;
            transform: translateY(-3px);
        }

        .service-card:focus-visible {
            outline: 2px solid var(--accent-hover);
            outline-offset: 3px;
        }

        .service-icon {
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
            width: 48px;
            height: 48px;
            border-radius: 10px;
            background: #302126;
            color: var(--accent-hover);
        }

        .service-icon svg {
            width: 26px;
            height: 26px;
        }

        .service-info {
            min-width: 0;
        }

        .service-info h3 {
            margin: 0 0 0.45rem;
            font-size: 1.05rem;
            font-weight: 650;
        }

        .service-info p {
            margin: 0;
            color: var(--muted);
            font-size: 0.9rem;
            line-height: 1.5;
            overflow-wrap: anywhere;
        }

        footer {
            padding: 1.5rem;
            text-align: center;
            color: var(--muted);
            font-size: 0.8rem;
            border-top: 1px solid var(--card-border);
        }

        @media (max-width: 640px) {
            header {
                padding: 2rem 1rem 1.5rem;
            }

            main {
                padding: 1.5rem 1rem;
            }

            .services-grid {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>

<body>
    <header>
        <h1>Local AI Server</h1>
        <p>Infrastructure and service dashboard</p>
    </header>

    <main>
        <h2 class="section-heading">Services</h2>
        <div id="services-grid" class="services-grid"></div>
    </main>

    <footer>
        Local Infrastructure Dashboard
    </footer>

    <script>
        const services = [
            {
                name: "Proxmox Host",
                description: "Virtual machines, containers, and host management",
                url: "https://192.168.1.1:8006",
                icon: "server"
            },
            {
                name: "Dockge UI",
                description: "Manage Docker Compose applications",
                url: "http://192.168.1.2:5001",
                icon: "cpu"
            }
        ];

        const icons = {
            server: `
                <svg viewBox="0 0 24 24" fill="none"
                    stroke="currentColor" stroke-width="1.8"
                    stroke-linecap="round" stroke-linejoin="round"
                    aria-hidden="true">
                    <rect x="3" y="3" width="18" height="7" rx="2"></rect>
                    <rect x="3" y="14" width="18" height="7" rx="2"></rect>
                    <path d="M7 6.5h.01M7 17.5h.01M11 6.5h6M11 17.5h6"></path>
                </svg>
            `,
            cpu: `
                <svg viewBox="0 0 24 24" fill="none"
                    stroke="currentColor" stroke-width="1.8"
                    stroke-linecap="round" stroke-linejoin="round"
                    aria-hidden="true">
                    <rect x="6" y="6" width="12" height="12" rx="2"></rect>
                    <rect x="9" y="9" width="6" height="6" rx="1"></rect>
                    <path d="M9 2v4M15 2v4M9 18v4M15 18v4M2 9h4M2 15h4M18 9h4M18 15h4"></path>
                </svg>
            `,
            network: `
                <svg viewBox="0 0 24 24" fill="none"
                    stroke="currentColor" stroke-width="1.8"
                    stroke-linecap="round" stroke-linejoin="round"
                    aria-hidden="true">
                    <rect x="9" y="2" width="6" height="6" rx="1"></rect>
                    <rect x="2" y="16" width="6" height="6" rx="1"></rect>
                    <rect x="16" y="16" width="6" height="6" rx="1"></rect>
                    <path d="M12 8v4M5 16v-4h14v4"></path>
                </svg>
            `
        };

        const grid = document.getElementById("services-grid");

        services.forEach((service) => {
            const card = document.createElement("a");
            card.className = "service-card";
            card.href = service.url;
            card.target = "_blank";
            card.rel = "noopener noreferrer";
            card.setAttribute("aria-label", service.name);

            const icon = document.createElement("div");
            icon.className = "service-icon";
            icon.setAttribute("aria-hidden", "true");
            icon.innerHTML = icons[service.icon] || icons.server;

            const info = document.createElement("div");
            info.className = "service-info";

            const title = document.createElement("h3");
            title.textContent = service.name;

            const description = document.createElement("p");
            description.textContent = service.description;

            info.appendChild(title);
            info.appendChild(description);
            card.appendChild(icon);
            card.appendChild(info);
            grid.appendChild(card);
        });
    </script>
</body>
</html>

Customize the Dashboard

The example includes three service links. Change the names, descriptions, and URLs in the services array to match your own environment.

For example:

{
    name: "Proxmox Host",
    description: "Virtual machines and containers",
    url: "https://192.168.1.10:8006",
    icon: "server"
}

The available icon types in this example are server, cpu, and network. You can reuse them for other services.

The dashboard is a static HTML file. You do not need to rebuild or restart the web server when you edit it.

3. Create the Python Web Server

We will use Python’s built-in HTTP server functionality to serve the dashboard. The script returns the same HTML file regardless of the URL path entered in the browser.

Create the Python script.

nano -w /usr/local/sbin/dashboard-server.py

Paste the following code.

#!/usr/bin/env python3

from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path

HOST = "192.168.1.1"
PORT = 80
HTML_FILE = Path("/var/www/host-dashboard.html")


class DashboardHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            content = HTML_FILE.read_bytes()
        except OSError:
            self.send_error(500, "Unable to read dashboard file")
            return

        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(content)))
        self.send_header("Cache-Control", "no-cache")
        self.end_headers()
        self.wfile.write(content)

    def log_message(self, format, *args):
        print("%s - %s" % (
            self.address_string(),
            format % args
        ))


if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), DashboardHandler)
    print(f"Dashboard server listening on {HOST}:{PORT}")
    server.serve_forever()

Important: Change 192.168.1.1 to the IP address of your Proxmox host. This must be the address on which you want the dashboard to listen.

Save the file and make it executable.

chmod 755 /usr/local/sbin/dashboard-server.py

The server listens only on the configured IP address and port. It does not change the Proxmox management interface on port 8006.

4. Create a Systemd Service

We want the dashboard to start automatically when Proxmox boots and restart if the Python process exits.

Create a systemd service file.

nano -w /etc/systemd/system/dashboard-server.service

Add the following configuration.

[Unit]
Description=Proxmox Host Dashboard
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/bin/python3 /usr/local/sbin/dashboard-server.py
Restart=always
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

Save the file.

The service runs as www-data instead of root. The CAP_NET_BIND_SERVICE capability allows the process to listen on port 80 without running the service as root.

5. Start the Dashboard

Before starting the service, check whether another service is already listening on port 80.

ss -ltnp 'sport = :80'

If another service is using port 80 on the Proxmox host, resolve that conflict before proceeding. A service listening on port 80 inside a separate container does not necessarily conflict with this host service; the important thing is whether the host itself has port 80 occupied.

Reload systemd and start the dashboard.

systemctl daemon-reload
systemctl enable --now dashboard-server.service

Check the service status.

systemctl status dashboard-server.service

Test the dashboard from the Proxmox host, replacing the example IP address with your own.

curl -I http://192.168.1.1/

You should receive an HTTP response with status 200 OK.

You can also test the HTML content directly:

curl http://192.168.1.1/

If the service fails to start, check its logs.

journalctl -u dashboard-server.service -n 50 --no-pager

6. Configure DNS

The final step is to configure a DNS record for the dashboard.

In your DNS server, create an A record pointing your chosen hostname to the Proxmox host’s IP address.

For example:

vmhost1.example.com    A    192.168.1.1

Replace the example hostname and IP address with your own values. The record should point to the Proxmox host, not to the separate LXC running your other applications.

Once DNS has updated, open the dashboard in your browser.

http://vmhost1.example.com

The dashboard should appear with your configured service links. Clicking a card opens the corresponding application in a new browser tab.

Use the explicit http:// prefix when opening the dashboard. This setup does not provide HTTPS, and browser behavior when the scheme is omitted can vary.

7. Updating the Dashboard

When you want to add, remove, or change a service, edit the HTML file.

nano -w /var/www/host-dashboard.html

Save your changes and refresh the browser. The Python server reads the file for each request, so you do not need to restart the systemd service.

If you change the listening IP address or port in the Python script, restart the service to apply the change.

systemctl restart dashboard-server.service

Conclusion

That’s it! We now have a simple landing page running directly on the Proxmox host. It uses one HTML file and a small Python script, with systemd handling startup and service recovery.

The dashboard is easy to customize. Add links to the services you use, update the HTML, and refresh your browser. The Proxmox management interface remains available on port 8006, while port 80 serves the dashboard.

This setup is intended for a trusted internal network. The dashboard itself uses HTTP, and the links it displays do not provide additional access controls. Continue to secure the Proxmox management interface and each linked service using your normal network and authentication policies.

Ubuntu 24.04 Base Server Setup

Step 1: Download Ubuntu 24.04 Server

Download the latest Ubuntu 24.04 Server ISO from the official Ubuntu releases page and create a bootable USB drive.

Step 2: Install Ubuntu 24.04 Server

Boot from the installation media and proceed with the installation using the guided setup. Ensure you select the required options based on your server needs.

Step 3: First Boot & System Update

Once the installation is complete, log in and update the system:

sudo -i
apt update && apt full-upgrade
apt install software-properties-common

Step 4: Enable SSH

SSH is usually installed by default, but verify and enable it:

apt install openssh-server
systemctl enable --now ssh

Step 5: Install Essential Packages

Add additional repositories and install required packages:

add-apt-repository ppa:ondrej/apache2
add-apt-repository ppa:ondrej/php
apt update
apt install make screen snmp composer libcurl4 unzip net-tools
apt install apache2 libapache2-mod-php mysql-server mysql-client
apt install libapache2-mod-fcgid php8.2-opcache php-apcu
apt install php-gd php-snmp php-mbstring php-mysql
apt install php-odbc php-imap php-xmlrpc php8.2-dba
apt install php-soap php-zip php-intl php-curl

Step 6: Configure Apache and PHP

Enable required Apache modules:

a2enmod rewrite actions fcgid alias proxy_fcgi expires headers
systemctl enable apache2
systemctl restart apache2

Step 7: Secure MySQL Installation

Run the MySQL security script and follow the prompts:

mysql_secure_installation

Step 8: Install phpMyAdmin

apt install phpmyadmin
systemctl restart apache2

Step 9: Install and Configure Postfix

Install Postfix and configure it as an Internet site:

apt install postfix

Edit /etc/postfix/virtual and add:

root admin@yourdomain.tld
postmaster admin@yourdomain.tld
abuse admin@yourdomain.tld

Apply changes:

postmap /etc/postfix/virtual
postconf -e "virtual_alias_maps = hash:/etc/postfix/virtual"
systemctl restart postfix
systemctl enable postfix

Step 10: Install Cockpit for Server Management

apt install cockpit
systemctl enable --now cockpit.socket

Access Cockpit at https://yourserver.tld:9090.

Step 11: Remove Cloud-Init (If Not Needed)

apt remove cloud-init cloud-initramfs-copymods cloud-initramfs-dyn-netconf

Final Steps

Ensure your server is secure by setting up a firewall and additional security configurations as required.

Configure Proxmox 8.x for a Low Power Home Lab Server

Introduction

I’ve been using low power CPUs for some time. This has caused some issues when I’ve needed more CPU processing power. My goal is to move to more power hungry CPUs but still try and save power. To this end I decided to change the CPU governor Proxmox uses. I also switched to using a small form factor case with its lower power power supply.

Configure CPU for Low Power

To make these changes you’ll need to ssh into your proxmox host.

Lat’s start by looking at your current CPU freequency.
> cat /proc/cpuinfo | grep MHz

The above command returns the following for my server.

cpu MHz : 3800.000
cpu MHz : 1760.822
cpu MHz : 3800.000
cpu MHz : 3800.000

First update repos.
> sudo apt update

Install acpi utilities.
> sudo apt install acpi-support acpid acpi

Its time to edit grub.
> nano -w /etc/default/grub

Look for the line that starts with “GRUB_CMDLINE_LINUX_DEFAULT” and add “intel_pstate=disable”..

Mine looks like this.

GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_pstate=disable"

Save and now rebuild grub.
> update-grub

Reboot the system and ssh back in. Change the governor being used.
> echo “conservative” | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

Check your new CPU frequency.
> cat /proc/cpuinfo | grep MHz

Mine shows then following when idle.

cpu MHz : 1605.058
cpu MHz : 1600.000
cpu MHz : 1600.000
cpu MHz : 1600.000

Edit crontab to make the changes permanent.
> crontab -e

add the following to the bottom and save.

@reboot  echo "conservative" | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

pstates vs acpi

I know I should have used pstates instead. ACPI did what I wanted. So I used it.

Conclusion

This is a quick tutorial that I hope helps someone out.

How to Upgrade PHP Packages on Ubuntu 22.04 and 24.04

Background

Managing PHP versions is crucial for maintaining compatibility, security, and performance of web applications. Ubuntu often provides multiple PHP versions, but upgrading packages across versions can be tedious. This script simplifies the process by automating package upgrades, switching the active PHP version for Apache, and cleaning up outdated packages.

Prerequisites

  • You must have administrative privileges (sudo access).
  • Ensure your system is running Ubuntu 22.04 or 24.04.
  • Make sure Apache is installed if you are using PHP with a web server.

Script

#!/bin/bash

# Get installed PHP versions
php_versions=$(update-alternatives --list php | awk -F'/' '{print $NF}' | sort -V)
echo "Installed PHP versions:"
echo "$php_versions"

# Ask user which PHP version to upgrade packages to
echo -n "Enter the PHP version to upgrade packages to: "
read target_version

# Get a list of installed PHP packages
php_packages=$(dpkg -l | awk '/^ii/ && /php/{print $2}')
echo "Checking packages that need to be upgraded to PHP $target_version..."

declare -A upgrade_list
for package in $php_packages; do
    new_package=$(echo "$package" | sed "s/[0-9]\+\.[0-9]\+/$target_version/")
    if apt-cache show "$new_package" &>/dev/null; then
        upgrade_list[$package]=$new_package
        echo "$package -> $new_package (Available)"
    else
        echo "$package -> $new_package (Not Found)"
    fi
done

# Ask user if they want to proceed
echo -n "Do you want to upgrade these packages? (y/n): "
read proceed
if [[ "$proceed" != "y" ]]; then
    echo "Upgrade canceled."
    exit 1
fi

# Upgrade packages
for old_pkg in "${!upgrade_list[@]}"; do
    new_pkg=${upgrade_list[$old_pkg]}
    echo "Upgrading $old_pkg -> $new_pkg"
    sudo apt install --only-upgrade -y "$new_pkg"
done

# Show the enabled PHP version for Apache
apache_php=$(readlink -f /etc/alternatives/php | grep -oP 'php[0-9]\.[0-9]+')
echo "Apache is currently using PHP version: $apache_php"

# Ask user if they want to change the PHP version for Apache
echo -n "Do you want to switch Apache to PHP $target_version? (y/n): "
read change_php
if [[ "$change_php" == "y" ]]; then
    sudo a2dismod "$apache_php"
    sudo a2enmod "php$target_version"
    sudo systemctl restart apache2
    echo "Apache has been switched to PHP $target_version and restarted."
fi

# List PHP packages that can be removed
old_php_packages=$(dpkg -l | awk -v ver="$target_version" '/^ii/ && /php/ && !index($2, ver){print $2}')
echo "The following PHP packages from old versions can be removed:"
echo "$old_php_packages"

# Ask user if they want to remove old PHP packages
echo -n "Do you want to remove these old PHP packages? (y/n): "
read remove_old
if [[ "$remove_old" == "y" ]]; then
    sudo apt remove --purge -y $old_php_packages
    sudo apt autoremove -y
    echo "Old PHP packages removed."
fi

echo "PHP upgrade process completed."

Setup

To use the script, follow these steps:

  1. Copy the script below.
  2. Open a terminal and create a new file:
    nano php_upgrade.sh
  3. Paste the script into the file.
  4. Save and exit (Press CTRL + X, then Y, then Enter).
  5. Make the script executable:
    chmod +x php_upgrade.sh
  6. Run the script with:
    sudo ./php_upgrade.sh
  7. Download the script:
    wget -O php_upgrade.sh https://yourwebsite.com/php_upgrade.sh
  8. Make the script executable:
    chmod +x php_upgrade.sh
  9. Run the script:
    sudo ./php_upgrade.sh

Usage

  1. The script will detect installed PHP versions and display them.
  2. It will prompt you to choose the PHP version you want to upgrade packages to.
  3. A list of packages that need upgrading will be shown, including availability.
  4. Confirm if you want to proceed with the upgrade.
  5. If needed, you can switch the active PHP version for Apache.
  6. The script will then list outdated PHP packages and ask if you want to remove them.
  7. After confirmation, it will uninstall unnecessary packages and clean up dependencies.

Notes

  • The script ensures that PHP packages are upgraded without breaking dependencies.
  • If you run a PHP-based application, check compatibility with the new version before upgrading.
  • Always back up your system before making major changes.

How To Configure a DNS Proxy on Ubuntu 22.04

Introduction

As you know it all begins with a story. My story is I didn’t want my DNS server directly connected to the internet. I wanted something basic and easy to use.

Say hello to dnsproxy. Its a basic and easy to use DNS proxy. Lets walk through the easy setup. I’ve even provided a script to help with setup. This setup relies on your server having a private IP address and a public IP address.

The Steps

Let’s start by updating Ubuntu. Hopefully you always update before installing new software.
> sudo apt update && sudo apt upgrade -y

Install dnsproxy.
> sudo apt install dnsproxy -y

Create /etc/dnsproxy.conf using the template below. There are place holders in ALL CAPS to highlight what you need to change.

# dnsproxy configuration file for Debian

# Authoritative server
# authoritative 192.168.1.2
authoritative YOUR_DNS_SERVER_IP
authoritative-port 53 # It's port. Defaults to 53.
authoritative-timeout 10 # Seconds to wait for answers.

# Recursive resolver
# recursive 192.168.1.2
recursive YOUR_DNS_SERVER_IP
recursive-port 53 # It's port. Defaults to 53.
recursive-timeout 90 # Seconds to wait for answers.

# Local addresses and ports of dnsproxy
# address and UDP port that dnsproxy use to communicate with users
# listen 8.9.1.2
listen YOUR_PUBLIC_IP
port 53

# address and UDP port that dnsproxy use to communicate with DNS servers.
# if your DNS servers are accessed by different network interfaces, you must
# use 0.0.0.0 as listen_answer. But if they are accessed by the same network
# interface, just set it to the IP address that you have on that interface.
listen_answer 0.0.0.0
port_answer 53003

# Security features
chroot /var/spool/dnsproxy
user dnsproxy

# Internal networks (allowed to do recursive queries)
internal 192.168.0.0/16 # Our internal network
internal 172.16.0.0/12 # Another internal network
internal 10.0.0.0/8 # Friendly neighbours
internal 127.0.0.1
internal YOUR_PRIVATE_IP # Private network
internal YOUR_PUBLIC_IP # Public IP considered as internal

Now start dnsproxy.
> sudo systemctl start dnsproxy

Test that everything is working.

Finally configure dnsproxy to start at boot.
> sudo systemctl enable dnsproxy

A Script To Make Things Easier

I’ve provided a script below that will help you set everything up.

#!/bin/bash
#
# This script sets up dnsproxy on a server.
# The server needs a private and a public network interface
#

#
# Variables
#
config_file="/etc/dnsproxy.conf"

#
# Functions
#

# Function to validate IP address
validate_ip() {
  local ip=$1
  local stat=1

  if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    OIFS=$IFS
    IFS='.'
    ip=($ip)
    IFS=$OIFS
    if [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]; then
      stat=0
    fi
  fi
  return $stat
}

#
# Main Script Start
#

# Check if the script is running as root
if [ "$EUID" -ne 0 ]; then
  echo "This script must be run as root. Please run it with sudo or as root."
  exit 1
fi

# Introduction message
echo "This script will create a dnsproxy.conf file for configuring DNS Proxy."
echo "It will also check if dnsproxy is installed, back up any existing config file, and manage the dnsproxy service."
echo ""
read -p "Do you want to continue? (y/n): " continue_script
if [[ $continue_script != "y" && $continue_script != "Y" ]]; then
  echo "Exiting script."
  exit 0
fi

# Check if dnsproxy is installed
if ! command -v dnsproxy &> /dev/null; then
  echo "dnsproxy is not installed."
  read -p "Do you want to install dnsproxy? (y/n): " install_dnsproxy
  if [[ $install_dnsproxy == "y" || $install_dnsproxy == "Y" ]]; then
    echo "Installing dnsproxy..."
    if ! sudo apt-get update && sudo apt-get install -y dnsproxy; then
      echo "Failed to install dnsproxy. Continuing without it..."
    fi
  else
    echo "Continuing without installing dnsproxy..."
  fi
fi

# Prompt for public interface IP
default_public_ip="8.9.1.2"
while true; do
  read -p "Enter the public interface IP address [default: $default_public_ip]: " public_ip
  public_ip=${public_ip:-$default_public_ip}
  validate_ip $public_ip
  if [[ $? -eq 0 ]]; then
    break
  else
    echo "Invalid IP address. Please enter a valid IP address."
  fi
done

# Prompt for private interface IP
default_private_ip="192.168.1.2"
while true; do
  read -p "Enter the private interface IP address [default: $default_private_ip]: " private_ip
  private_ip=${private_ip:-$default_private_ip}
  validate_ip $private_ip
  if [[ $? -eq 0 ]]; then
    break
  else
    echo "Invalid IP address. Please enter a valid IP address."
  fi
done

# Prompt for authoritative DNS server IP
while true; do
  read -p "Enter the IP address of the authoritative DNS server: " auth_dns_ip
  validate_ip $auth_dns_ip
  if [[ $? -eq 0 ]]; then
    break
  else
    echo "Invalid IP address. Please enter a valid IP address."
  fi
done

# Prompt for recursive DNS server IP
while true; do
  read -p "Enter the IP address of the recursive DNS server: " rec_dns_ip
  validate_ip $rec_dns_ip
  if [[ $? -eq 0 ]]; then
    break
  else
    echo "Invalid IP address. Please enter a valid IP address."
  fi
done

# Backup existing config file if it exists
if [ -f "$config_file" ]; then
  timestamp=$(date +%Y%m%d%H%M%S)
  backup_file="${config_file}.${timestamp}.bak"
  cp "$config_file" "$backup_file"
  echo "Existing config file backed up as $backup_file"
fi

# Create dnsproxy.conf file
cat < $config_file
# dnsproxy configuration file example for Debian

# Authoritative server
authoritative        $auth_dns_ip
authoritative-port   53              # It's port. Defaults to 53.
authoritative-timeout 10             # Seconds to wait for answers.

# Recursive resolver
recursive            $rec_dns_ip
recursive-port       53              # It's port. Defaults to 53.
recursive-timeout    90              # Seconds to wait for answers.

# Local addresses and ports of dnsproxy
# address and UDP port that dnsproxy use to communicate with users
listen $public_ip
port 53

# address and UDP port that dnsproxy use to communicate with DNS servers.
# if your DNS servers are accessed by different network interfaces, you must
# use 0.0.0.0 as listen_answer. But if they are accessed by the same network
# interface, just set it to the IP address that you have on that interface.
listen_answer 0.0.0.0
port_answer 53003

# Security features
chroot /var/spool/dnsproxy
user dnsproxy

# Internal networks (allowed to do recursive queries)
internal 192.168.0.0/16          # Our internal network
internal 172.16.0.0/12           # Another internal network
internal 10.0.0.0/8              # Friendly neighbours
internal 127.0.0.1
internal $private_ip             # Private network
internal $public_ip              # Public IP considered as internal
EOF

echo "dnsproxy.conf file created successfully."

# Check if dnsproxy is running
if pgrep dnsproxy > /dev/null; then
  echo "dnsproxy is currently running. Restarting it..."
  sudo systemctl restart dnsproxy
  echo "dnsproxy has been restarted."
else
  read -p "dnsproxy is not running. Do you want to start it? (y/n): " start_dnsproxy
  if [[ $start_dnsproxy == "y" || $start_dnsproxy == "Y" ]]; then
    echo "Starting dnsproxy..."
    sudo systemctl start dnsproxy
    sudo systemctl enable dnsproxy
    echo "dnsproxy has been started and set to start on boot."
  else
    echo "dnsproxy was not started."
  fi
fi

# Script end
echo "dnsproxy setup complete."
exit 0

You can create an empty file for pasting the script into.
> nano -w setup_dnsproxy.sh

Copy the script from the website and paste it into nano. When done exit namo and save changes.

Use the following command to run the script.
> sudo bash setup_dnsproxy.sh

Conclusion

That’s it! A quick and easy DNS proxy setup.

Ubuntu 22.04 – Extending the root partition

Introduction

I run several virtual machines based on Ubuntu. I create a base image that is cloned when I need a new virtual machine. Since most of my VMs don’t need a large disk image I keep the base image on the small side. So I extend the size of the disk image when needed. After the disk image size is increased you need to allocate that space to use it. The steps below will show you how to extend the root partition to the new size.

The Steps

I’m assuming that the disk image or physical hard drive of the machine has been increased.

Do a df -h to get the logical volume name of the partition you want to extend.
> df -h

Filesystem                        Size Used Avail Use% Mounted on
tmpfs                             393M 1.1M  392M   1% /run
/dev/mapper/ubuntu--vg-ubuntu--lv  24G 9.0G   14G  41% /
tmpfs                             2.0G    0  2.0G   0% /dev/shm
tmpfs                             5.0M    0  5.0M   0% /run/lock
/dev/sda2                         2.0G 367M  1.5G  21% /boot
tmpfs                             393M 4.0K  393M   1% /run/user/1000

Now check to be sure the volume group has free space. The free space is the possible max amount you can extend the partition.
> vgdisplay

With the information above I can expand the partition 23G. Here’s the command I would use.
> sudo lvextend -L +23G /dev/mapper/ubuntu–vg-ubuntu–lv

Finally we extend the filesystem to the new logical volume size.
> sudo resize2fs /dev/mapper/ubuntu–vg-ubuntu–lv                     

Now that we’re done, lets reboot the system.
> sudo reboot

Log back into the ubuntu system. Check that your partition has been extended.
> df -h

Conclusion

Thanks to LVM the process of extending the root partition is simple and straight forward.

HOWTO Ubuntu 22.04 Virtual Web Hosting With Apache, PHP, vsFTPD and Let’s Encrypt

Introduction

The focus of this howto is for those users who need to host their own domains and maybe a few customer domains. This is not aimed at being used for mass web hosting.

There are many ways to do virtual websites under linux. The technique I am going to use is multiple domains on one ip address. I’m using standard linux users to log into the virtual domains.

Setting Up The Base Server

For a dedicated server start with the base server setup:

Howto Ubuntu 22.04 Base Server Setup

Introduction All of our servers will start with this install. This base server is based on Ubuntu 22.04 LTS Server.  I don’t explain much in the howto so if you have a question leave a comment or use Google. Downloading … Continue reading →

NOTE: If you don’t follow the base server setup then you may run into problems with this howto.

Install Software

We need to install an FTP server and Let’s Encrypt. So type the following:
> sudo add-apt-repository ppa:certbot/certbot
> sudo apt update
> sudo apt install vsftpd python3-certbot-apache

Setup Default User Directory

A new user’s directory needs to have some files and folders created for them.  We will modify the user skel directory so when a new user is created the required folder structure will be there.

Type the following.
> sudo mkdir -p /etc/skel/{website,logs,cgi-bin,backup}
> sudo echo “HELLO WORLD” > index.html
> sudo mv index.html /etc/skel/website/

Configuring vsftpd

Lets create the configuration file.  Replace the contents of /etc/vsftpd.conf with the text below.

listen=NO
listen_ipv6=YES
anonymous_enable=NO
local_enable=YES
write_enable=YES
#local_umask=022
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
chroot_local_user=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
ssl_enable=NO
allow_writeable_chroot=YES
pasv_enable=Yes
pasv_min_port=40000
pasv_max_port=40100

Start vsftpd.
> sudo systemctl enable vsftpd
> sudo systemctl start vsftpd.service

Configuring Apache

Most of the apache configuration is already done.  We are going to do some changes to make managing websites easier.

Create the virtual host config file. I defined macros to make virtual host creation easier.  I also turn on compression. Create /etc/apache2/conf-available/virtual.conf with the following:

# Go ahead and accept connections for these vhosts
# from non-SNI clients
SSLStrictSNIVHostCheck off

# define a macro for the virtual hosts
# the user's directory should be setup as follows:
# |- cgi-bin
# |- logs
# |- website
# |- ssl
#
LoadModule macro_module modules/mod_macro.so

<Macro virtHost $type $user $host>
    use $type $host

    ServerName $host
    ServerAlias www.$host 
    DocumentRoot /home/$user/website
    ScriptAlias "/cgi-bin/" "/home/$user/cgi-bin"
    LogFormat "%h %l %u %t \"%r\" %>s %b" common
    CustomLog /home/$user/logs/access_log common
    ErrorLog /home/$user/logs/error_log
    <Directory /home/$user/website>
      DirectoryIndex index.html index.php
      Options Indexes FollowSymLinks
      AllowOverride All
      Require all granted

      # setup file compression
      use CompressFiles

      # setup browser caching
      use BrowserCache

      # disable hotlinking for some files
      use DisableHotLink $host
    </Directory>
  </VirtualHost>
</Macro>

<Macro BrowserCache>
  # Enable expires cache
  <IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType text/css "access 1 month"
    ExpiresByType text/html "access 1 month"
    ExpiresByType image/gif "access 1 year"
    ExpiresByType image/png "access 1 year"
    ExpiresByType image/jpg "access 1 year"
    ExpiresByType image/jpeg "access 1 year"
    ExpiresByType image/x-icon "access 1 year"
    ExpiresByType application/pdf "access 1 month"
    ExpiresByType application/javascript "access 1 month"
    ExpiresByType text/x-javascript "access 1 month"
    ExpiresDefault "access 1 month"
  </IfModule>

  # Cache-Control Headers
  <ifModule mod_headers.c>
    <filesMatch "\.(ico|jpe?g|png|gif|swf)$">
      Header set Cache-Control "public"
    </filesMatch>
    <filesMatch "\.(css)$">
      Header set Cache-Control "public"
    </filesMatch>
    <filesMatch "\.(js)$">
      Header set Cache-Control "private"
    </filesMatch>
    <filesMatch "\.(x?html?|php)$">
      Header set Cache-Control "private, must-revalidate"
    </filesMatch>
  </ifModule>
</Macro>

<Macro CompressFiles>
  # enable compression
  <IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE "application/atom+xml"
    AddOutputFilterByType DEFLATE "application/javascript"
    AddOutputFilterByType DEFLATE "application/json"
    AddOutputFilterByType DEFLATE "application/ld+json"
    AddOutputFilterByType DEFLATE "application/manifest+json"
    AddOutputFilterByType DEFLATE "application/rdf+xml"
    AddOutputFilterByType DEFLATE "application/rss+xml"
    AddOutputFilterByType DEFLATE "application/schema+json"
    AddOutputFilterByType DEFLATE "application/vnd.geo+json"
    AddOutputFilterByType DEFLATE "application/vnd.ms-fontobject"
    AddOutputFilterByType DEFLATE "application/x-font"
    AddOutputFilterByType DEFLATE "application/x-font-opentype"
    AddOutputFilterByType DEFLATE "application/x-font-otf"
    AddOutputFilterByType DEFLATE "application/x-font-truetype"
    AddOutputFilterByType DEFLATE "application/x-font-ttf"
    AddOutputFilterByType DEFLATE "application/x-javascript"
    AddOutputFilterByType DEFLATE "application/x-web-app-manifest+json"
    AddOutputFilterByType DEFLATE "application/xhtml+xml"
    AddOutputFilterByType DEFLATE "application/xml"
    AddOutputFilterByType DEFLATE "font/eot"
    AddOutputFilterByType DEFLATE "font/otf"
    AddOutputFilterByType DEFLATE "font/ttf"
    AddOutputFilterByType DEFLATE "font/opentype"
    AddOutputFilterByType DEFLATE "image/bmp"
    AddOutputFilterByType DEFLATE "image/svg+xml"
    AddOutputFilterByType DEFLATE "image/vnd.microsoft.icon"
    AddOutputFilterByType DEFLATE "image/x-icon"
    AddOutputFilterByType DEFLATE "text/cache-manifest"
    AddOutputFilterByType DEFLATE "text/css"
    AddOutputFilterByType DEFLATE "text/html"
    AddOutputFilterByType DEFLATE "text/javascript"
    AddOutputFilterByType DEFLATE "text/plain"
    AddOutputFilterByType DEFLATE "text/vcard"
    AddOutputFilterByType DEFLATE "text/vnd.rim.location.xloc"
    AddOutputFilterByType DEFLATE "text/vtt"
    AddOutputFilterByType DEFLATE "text/x-component"
    AddOutputFilterByType DEFLATE "text/x-cross-domain-policy"
    AddOutputFilterByType DEFLATE "text/xml"
  </IfModule>
</Macro>

<Macro DisableHotLink $host >
  # Disable file hotlinking - jpg jpeg png gif pdf
  <IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteCond %{HTTP_REFERER} !^$
    RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?$host [NC]
    RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?google.com [NC]
    RewriteRule \.(jpg|jpeg|png|gif|pdf)$ – [NC,F,L]
  </IfModule>
</macro>

<Macro VHost443 $host >
  <VirtualHost *:443>
    SSLEngine on
    SSLProtocol all -SSLv2
    SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5
    SSLCertificateFile /etc/letsencrypt/live/$host/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/$host/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/$host/fullchain.pem
</Macro>

<Macro VHost80 $host >
  <VirtualHost *:80>
</Macro>

Enable the configuration.
> sudo a2enconf virtual

Enable macros and ssl.
> sudo a2enmod macro
> sudo a2enmod ssl

Restart apache
> sudo service apache2 restart

Configuring Let’s Encrypt

Let’s Encrypt needs to be configured to auto renew certs. Lets create a daily cron job
> sudo nano -w /etc/cron.daily/letsencrypt

#!/usr/bin/bash
# letsencrypt auto renew
/usr/bin/certbot renew --no-self-upgrade >> /var/log/le-renew.log

Adding a Default Website

Now we will create a default website.  This site will be used when no other website can be found.

Setup a DNS record for the new domain. I won’t cover this here.

Add a user. This user will be associated with the new domain name. Type the following.
> sudo useradd -m -U -s /bin/bash -c “default website” defaultweb
> sudo passwd defaultweb

Add the apache user to the new user’s group.
> sudo usermod -a -G defaultweb www-data

Update directory permissions.
> sudo chmod g+rwx /home/defaultweb
> sudo chown -R defaultweb:defaultweb /home/defaultweb

Create the virtual host file.  For the default server we will use port 80. Past the text below into the file.  Type:
> sudo nano -w /etc/apache2/sites-available/00-default.conf

# Virtual host config file
#
# MACRO FORMAT
# virtHost [type] [user] [host]
#  type = VHost80 or VHost443
#  user = the username of the website
#  host = domain name or virtual host name
#
# Use the line below to configure a site on port 80
use virtHost VHost80 defaultweb myserver.mydomain.tld

# Uncomment the line below once lets encrypt is setup
# use virtHost VHost443 defaultweb myserver.mydomain.tld

Disable the old default site and enable our default site.
> sudo a2dissite 000-default
> sudo a2ensite 00-default

Reload apache config
> sudo service apache2 reload

Test out the new website.  You should get a page that says ‘Hello World’.

Now we will setup lets encrypt for the default website.  The website must be reachable from the internet.  So lets get the cert:
> sudo certbot certonly –webroot -w /home/defaultweb/website/ -d <YOUR_DOMAIN> –email <YOUR_EMAIL_ADDRESS> –agree-tos

Edit /etc/httpd/virtualHosts.d/00-default.conf
Uncomment the last line to enable ssl connections for the virtual host.

Reload apache.
> sudo service apache2 reload

Test it out.  Connect to your default host via https.

Setup Additional Virtual Hosts

Adding a new virtual host is like adding the default virtual host.  Lets go through the steps.

Be sure DNS is configured for the new virtual host.

Setup a new user. This user will be associated with the new domain name. Type the following. Change ‘NEWUSER’ to the username you want.
> sudo useradd -m -U -s /bin/bash -c “Virtual Website User” NEWUSER
> sudo passwd NEWUSER
> sudo usermod -a -G NEWUSER www-data
> sudo chmod g+rwx /home/NEWUSER
> sudo chown -R NEWUSER:NEWUSER /home/NEWUSER

Create the virtual host file.  For the virtual server we will use port 80. Past the text below into the file.  Replace ‘NEWUSER’ with your user name.  Replace NEWVHOST with your hostname.
> sudo nano -w /etc/apache2/sites-available/NEWUSER.conf

# Virtual host config file
#
# MACRO FORMAT
# virtHost [type] [user] [host]
#  type = VHost80 or VHost443
#  user = the username of the website
#  host = domain name or virtual host name
#
# Use the line below to configure a site on port 80
use virtHost VHost80 NEWUSER NEWVHOST

# Uncomment the line below once lets encrypt is setup
# use virtHost VHost443 NEWUSER NEWVHOST

Enable the new site and reload apache config
> sudo a2ensite NEWUSER
> sudo service apache2 reload

Now we will setup lets encrypt for the new website.  The website must be reachable from the internet.  Replace NEWUSER and NEWVHOST with the info you have.  So lets get the cert.
> sudo certbot certonly –webroot -w /home/NEWUSER/website/ -d NEWHOST -d www.NEWHOST –email YOUR_EMAIL_ADDRESS –agree-tos

Edit /etc/httpd/virtualHosts.d/NEWUSER.conf
Uncomment the last line to enable ssl connections for the virtual host.

Reload apache.
> sudo service apache2 reload

Test it out.  Connect to your new host via https.

Conclusion

That’s the complete setup.

Howto Ubuntu 22.04 Base Server Setup

Introduction

All of our servers will start with this install. This base server is based on Ubuntu 22.04 LTS Server.  I don’t explain much in the howto so if you have a question leave a comment or use Google.

Downloading the ISO

Visit the Ubuntu website and download the ubuntu 22.04 server ISO.

Initial Install

The install screens are straight forward.  I’m not going to cover them in much detail.  Unless you need to customize a setting, just choose the default settings.  Boot the install DVD.

Click ‘Reboot’ when it appears.

First boot

Reboot the machine when the install finishes.
The OS will boot. Log in. All the commands need to be run as root so lets start a shell with root privilleges.
> sudo bash

Get everything updated and install a couple of items.
> sudo apt update
> sudo apt dist-upgrade
> sudo apt install software-properties-common

Install and enable ssh.
> sudo apt install ssh
> sudo systemctl enable –now ssh

WARNING: My server isn’t directly connected to the internet. The firewall is disabled to help with installation, configuration and testing easier. Once everything is working, turn on the firewall and configure it. I wil remind you to secure your server at the end of this howto.

now reboot the server.

The Second Boot – Installing Additional Packages

We need quite a few other packages. In this howto I’m installing packages regardless if they were already installed by another dependency. This guards against package changes that could cause a package to not be installed. Once again log in to your server.

Let’s add a 3rd party repo for Apache.
> add-apt-repository ppa:ondrej/apache2

Add a 3rd party repo for PHP.
> sudo add-apt-repository ppa:ondrej/php

Now bring everything up to date.
> sudo apt update && sudo apt upgrade

Install the following packages. Multiple lines to make cut and paste easier.
> sudo apt install make screen snmp composer libcurl4 unzip net-tools
> sudo apt install apache2 libapache2-mod-php mysql-server mysql-client
> sudo apt install libapache2-mod-fcgid php8.2-opcache php-apcu

Install some extra PHP libraries.
> sudo apt install php-gd php-snmp php-mbstring php-mysql
> sudo apt install php-odbc php-imap
> sudo apt install php-xmlrpc php8.2-dba
> sudo apt install php-soap php-zip php-intl php-curl

Configure Apache and PHP

Enable the rewrite module.
> sudo a2enmod rewrite actions fcgid alias proxy_fcgi expires headers

Enable Apache.
> sudo systemctl enable apache2.service

Reload apache.
> sudo systemctl restart apache2.service

Configuring MySQL

We need to set the root password in mysql. so type:
> sudo mysql

Set the root password for mysql.
mysql> ALTER USER ‘root’@’localhost’ IDENTIFIED WITH mysql_native_password by ‘my-secret-password’;
mysql> exit

Configure mysql.
> sudo mysql_secure_installation

Installing and Configuring phpMyAdmin

I prefer to phpMyAdmin to manage my MySQL databases.

Now install phpMyAdmin.
> sudo apt install phpmyadmin

Restart Apache.
> sudo systemctl restart apache2.service

NOTE: You should think about limiting access to phpMyAdmin. This will help secure your server.

Installing Postfix

Lets install postfix.
> sudo apt install postfix

When prompted select internet site. Next set the mail server name.

Installing cockpit

I’m using cockpit as my server admin tool.  Do the following to set it up.
> sudo apt install cockpit
> sudo systemctl start cockpit
> sudo systemctl enable cockpit.socket

You can now login to https://yourserver.tld:9090 to administer your server.

NOTE: You should limit access to this service.

Getting root’s and other’s mail

You need to get some local system user’s mail. We’ll use postfix’s virtual file to get the emails to the right place.

Add the following to /etc/postfix/virtual

root admin@yourdomain.tld
postmaster admin@yourdomain.tld
abuse admin@yourdomain.tld

Now add the configuration option to main.cf
> sudo postconf -e “virtual_alias_maps = hash:/etc/postfix/virtual”

Just a couple commands to wrap everything up.
> sudo postmap /etc/postfix/virtual
> sudo systemctl restart postfix
> sudo systemctl enable postfix

Remove Cloud Init

Our server won’t be using cloud-init.  To keep from causing you trouble in the future we will remove it now.

Type:
> sudo apt remove cloud-init cloud-initramfs-copymods cloud-initramfs-dyn-netconf

Final Settings

You may want to enable the linux firewall.
Set your timezone in /etc/php.ini

Conclusion

That’s it for the basic server setup. This is an example of a standard linux server setup. Be sure to use cockpit to set which services you want to start at boot time. See the other pages for info on configuring servers for virtual webhosting or virtual email hosting. Remember to configure the firewall on the server.

HOWTO: Change Thin LVM to Directory Storage in Proxmox 7.x

Introduction

In Proxmox 7.x LVM thin is the default local storage for VMs.  However I prefer the old method of directory based storage for local storage.  You can find this information in the proxmox wiki.  These are my terse notes to get the job done.

The Conversion

This should only be done on a new installation of proxmox.  We will be mounting the storage to /var/lib/vz. If you have data in that directory you will lose it.

Start by logging into the Proxmox gui. Click on cluster then select storage. Remove the lvm-thin storage.

Next you will need to ssh into your Proxmox node. Type the following to set everything up.
> sudo lvremove pve/data
> sudo lvcreate –name data -l +100%FREE pve
> sudo mkfs.ext4 /dev/pve/data

Add the following to /etc/fstab
> nano -w /etc/fstab

/dev/pve/data /var/lib/vz ext4 defaults 0 1

Restart proxmox.
> sudo reboot

Conclusion

Once rebooted you can log into the gui and see your new local directory storage.  Maybe someday I’ll switch to lvm-thin or zfs.