Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/appliance.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Appliance

# Builds the downloadable virtual appliances for ALL hypervisors in one job: customizes the Ubuntu 24.04
# cloud image with libguestfs (no Hyper-V host / nested virt needed) into a Gen2/UEFI image with PostgreSQL
# + Dispatch baked in, emits it as VHDX (Hyper-V), OVA (VMware), and qcow2 (KVM/Proxmox), then boots
# it under QEMU/UEFI to prove the service comes up on first boot. Artifacts: dispatch-appliance-hyperv,
# dispatch-appliance-vmware, dispatch-appliance-kvm.
# cloud image with libguestfs (no Hyper-V host / nested virt needed) into a Gen2/UEFI image with Dispatch
# baked in (bundled SQLite - no database server), emits it as VHDX (Hyper-V), OVA (VMware), and qcow2
# (KVM/Proxmox), then boots it under QEMU/UEFI to prove the service comes up on first boot. Artifacts:
# dispatch-appliance-hyperv, dispatch-appliance-vmware, dispatch-appliance-kvm.
on:
# Runs AFTER a release is published - linked to the release, but it never blocks or races it (the release
# is already out; this attaches the appliance images to it). The Build workflow also kicks this off after
Expand Down Expand Up @@ -209,7 +209,7 @@ jobs:
sudo convert /tmp/screen.ppm "$1" 2>/dev/null && echo "captured $1" || echo "no screenshot"
}
ok=0
for i in $(seq 1 90); do # up to ~8 min (KVM boot is fast; this is mostly PostgreSQL first-run)
for i in $(seq 1 90); do # up to ~8 min - generous headroom for slow CI runners' boot + first-run
if curl -fsSk -m 3 https://localhost:8420/health >/dev/null 2>&1; then ok=1; break; fi
[ "$i" = 18 ] && screenshot boot-90s.png # ~90s in: capture whatever's on screen
sleep 5
Expand Down
15 changes: 8 additions & 7 deletions appliance/Import-DispatchAppliance.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
.DESCRIPTION
Creates a Generation 2 VM, copies the appliance VHDX into the chosen storage location, sets the Secure
Boot template to the Microsoft UEFI Certificate Authority (required for Linux), connects a virtual
switch (optionally on a specific VLAN), and (optionally) starts it. The appliance configures PostgreSQL
+ Dispatch on first boot; browse to https://<vm-ip>:8420 and set the admin password.
switch (optionally on a specific VLAN), and (optionally) starts it. The appliance creates its bundled
SQLite database and starts Dispatch on first boot; browse to https://<vm-ip>:8420 and set the admin
password.

Run it with no networking/storage flags for a guided menu: it lists the host's virtual switches and
storage volumes and prompts for the VLAN, memory, and CPU. Pass -SwitchName for fully unattended use.
Expand Down Expand Up @@ -88,7 +89,7 @@ function Select-Storage([string]$Default) {
Where-Object { $_.DriveLetter -and $_.Size -gt 0 } | Sort-Object DriveLetter | ForEach-Object {
Write-Host (" {0}: {1:N0} GB free of {2:N0} GB {3}" -f $_.DriveLetter, ($_.SizeRemaining / 1GB), ($_.Size / 1GB), $_.FileSystemLabel)
}
Write-Host " (The appliance disk is ~6-10 GB thin-provisioned; PostgreSQL + logs grow it over time.)"
Write-Host " (The appliance disk is ~6-10 GB thin-provisioned; the SQLite database + logs grow it over time.)"
return (Read-WithDefault "VM storage folder" $Default)
}

Expand All @@ -106,13 +107,13 @@ if (-not ($isAdmin -or $isHyperVAdm)) {
if (-not $VhdxPath) {
$found = @(Get-ChildItem -LiteralPath $PSScriptRoot -Filter *.vhdx -File -ErrorAction SilentlyContinue)
if ($found.Count -eq 1) { $VhdxPath = $found[0].FullName; Write-Host "Using VHDX: $VhdxPath" }
elseif ($found.Count -eq 0) { throw "No .vhdx found next to this script. Unzip dispatch-appliance.vhdx.zip here first, or pass -VhdxPath." }
elseif ($found.Count -eq 0) { throw "No .vhdx found next to this script. Unzip the downloaded dispatch-appliance-*-hyperv.zip here first, or pass -VhdxPath." }
else { throw "Multiple .vhdx files found next to this script; pass -VhdxPath to pick one." }
}
if (-not (Test-Path -LiteralPath $VhdxPath -PathType Leaf)) { throw "VHDX file not found: $VhdxPath" }
$VhdxPath = (Resolve-Path -LiteralPath $VhdxPath).Path
if ([System.IO.Path]::GetExtension($VhdxPath) -notin @('.vhdx', '.vhd')) {
throw "-VhdxPath must point at the appliance .vhdx file, but got '$VhdxPath'. Unzip dispatch-appliance.vhdx.zip and pass the dispatch-appliance.vhdx inside it."
throw "-VhdxPath must point at the appliance .vhdx file, but got '$VhdxPath'. Unzip the downloaded dispatch-appliance-*-hyperv.zip and pass the dispatch-appliance.vhdx inside it."
}

# Detect a failover cluster on this host (Get-Cluster ships in the FailoverClusters module, present only on
Expand Down Expand Up @@ -177,7 +178,7 @@ try {
Set-VMProcessor -VM $vm -Count $CpuCount
# Linux on Gen2 needs the Microsoft UEFI CA Secure Boot template (not the default Windows one).
Set-VMFirmware -VM $vm -EnableSecureBoot On -SecureBootTemplate "MicrosoftUEFICertificateAuthority"
# PostgreSQL needs a stable working set; disable Dynamic Memory.
# A relay service needs a stable working set; disable Dynamic Memory.
Set-VMMemory -VM $vm -DynamicMemoryEnabled $false
# Apply a VLAN tag to the adapter if requested (access mode = single tagged VLAN).
if ($VlanId -gt 0) {
Expand Down Expand Up @@ -211,7 +212,7 @@ Write-Host ("Created '{0}' (Gen2, {1} vCPU, {2} GB, switch '{3}'{4}{5})." -f `
$Name, $CpuCount, $MemoryGB, $SwitchName, $(if ($VlanId -gt 0) { ", VLAN $VlanId" } else { "" }), $(if ($AddToCluster) { ", clustered" } else { "" }))
if ($Start) {
Start-VM -VM $vm
Write-Host "Started. First boot configures PostgreSQL + Dispatch (a few minutes)."
Write-Host "Started. First boot creates the bundled SQLite database and starts Dispatch - it's quick."
} else {
Write-Host "Start it with: Start-VM -Name '$Name'"
}
Expand Down
11 changes: 5 additions & 6 deletions appliance/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dispatch SMTP Relay - virtual appliance

A ready-to-run **Ubuntu 24.04 LTS** virtual machine with Dispatch and PostgreSQL pre-installed. Import it, power it on, and the dashboard comes up - no .NET, database, or command line needed.
A ready-to-run **Ubuntu 24.04 LTS** virtual machine with Dispatch pre-installed, using its bundled SQLite database. Import it, power it on, and the dashboard comes up - no .NET, database server, or command line needed.

There's a **separate download per hypervisor** - each is a single zip (unzip once) with that format's image, its README, and the import helper:

Expand All @@ -10,7 +10,7 @@ There's a **separate download per hypervisor** - each is a single zip (unzip onc
| **VMware** (vSphere/ESXi/Workstation/Fusion) | `dispatch-appliance-vmware` | `dispatch-appliance.ova` + README |
| **KVM/libvirt & Proxmox** | `dispatch-appliance-kvm` | `dispatch-appliance.qcow2` + `import-libvirt.sh` / `import-proxmox.sh` + README |

All are **Gen2/UEFI**, ~4 GB RAM recommended (PostgreSQL needs ~2 GB). They boot on DHCP, but a relay your apps point at should have a **static IP** - set one after first boot (see [Static IP](#static-ip)).
All are **Gen2/UEFI**, ~4 GB RAM recommended. They boot on DHCP, but a relay your apps point at should have a **static IP** - set one after first boot (see [Static IP](#static-ip)).

## Hyper-V

Expand Down Expand Up @@ -49,7 +49,7 @@ Creates a q35/OVMF VM, imports the disk as `scsi0`, and sets it to boot.

## First boot

On first start the appliance gives itself a **unique** database password, creates the `dispatch` role and `DispatchLog` database, starts PostgreSQL, and starts Dispatch (the schema is created automatically). This takes a couple of minutes the first time.
On first start Dispatch creates its bundled SQLite database under `/var/lib/dispatch` (the schema is created automatically) and starts - there's no database server to bootstrap, so it's quick.

Then browse to the dashboard:

Expand Down Expand Up @@ -90,15 +90,14 @@ Pass `-i <nic>` to target a specific interface (see `ip a`); run `dispatch-set-i
## Maintenance (console)

- Service: `systemctl status dispatch` · logs: `journalctl -u dispatch -f` and `/var/log/dispatch/`.
- PostgreSQL: `systemctl status postgresql`.
- Database: bundled SQLite file at `/var/lib/dispatch` - no separate server process to manage.
- Config (connection string only): `/var/lib/dispatch/appsettings.json` - everything else is in the dashboard.

## Security notes

- Every appliance generates its **own** database password, at-rest encryption key (`.dispatch-key`), dashboard TLS cert, SSH host keys, and machine-id on first boot - nothing secret is shared across downloads.
- Every appliance generates its **own** at-rest encryption key (`.dispatch-key`), dashboard TLS cert, SSH host keys, and machine-id on first boot - nothing secret is shared across downloads.
- The **OS login** (`ubuntu`/`dispatch`) is a known default but **must be changed on first login** - do so immediately, especially before exposing the VM beyond a trusted LAN.
- The dashboard admin password is set by **you** on first login and is stored only as a bcrypt hash in the database.
- The appliance bundles **PostgreSQL**, which is free and open source.

## Building it yourself

Expand Down
7 changes: 3 additions & 4 deletions appliance/build-appliance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# Dispatch SMTP Relay - Hyper-V appliance builder (Ubuntu 24.04 LTS).
#
# Customizes the official Ubuntu cloud image offline with libguestfs (no Hyper-V host, no nested virt) and
# converts it to a Gen2/UEFI dynamic VHDX. PostgreSQL's binaries are baked in; each VM configures the
# Dispatch starts on first boot against the bundled SQLite database it creates under /var/lib/dispatch.
# converts it to a Gen2/UEFI dynamic VHDX. Dispatch is baked in and starts on first boot against the
# bundled SQLite database it creates under /var/lib/dispatch - no database server to install or configure.
#
# Requires (host): libguestfs-tools, qemu-utils, curl.
# Usage:
Expand Down Expand Up @@ -79,7 +79,6 @@ docker run --rm -e KVER="$KVER" -v "$STAGE/debs:/debs" ubuntu:24.04 bash -ec '
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl ca-certificates >/dev/null
# PostgreSQL 16 ships in the default Ubuntu 24.04 repos, so no extra apt source or signing key is needed.
cd /debs
# Full recursive runtime dependency closure of the target packages (skip virtual/undownloadable entries).
deps=$(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances \
Expand Down Expand Up @@ -107,7 +106,7 @@ echo "==> Expanding the root partition into a ${DISK_SIZE} working image"
qemu-img create -f qcow2 "$WORK/disk.qcow2" "$DISK_SIZE"
virt-resize --expand /dev/sda1 "$WORK/base.img" "$WORK/disk.qcow2"

echo "==> Customizing the image (PostgreSQL + Dispatch + first-boot)"
echo "==> Customizing the image (Dispatch + first-boot)"
# --no-network: provisioning installs pre-downloaded .debs offline, so the appliance needs no in-guest
# network (and we avoid libguestfs's passt networking, which fails on CI runners).
virt-customize -a "$WORK/disk.qcow2" \
Expand Down
2 changes: 1 addition & 1 deletion appliance/dispatch.ovf.template
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
OVF descriptor for the Dispatch SMTP Relay VMware appliance (vSphere/ESXi). Generated from the same
Ubuntu 24.04 + PostgreSQL + Dispatch image as the Hyper-V VHDX. Conservative virtual hardware (EFI,
Ubuntu 24.04 + Dispatch (bundled SQLite) image as the Hyper-V VHDX. Conservative virtual hardware (EFI,
LSI Logic SCSI, E1000e NIC) for broad import compatibility; placeholders (@@...@@) are filled by
build-appliance.sh. vmx-14 = ESXi 6.7+.
-->
Expand Down
5 changes: 3 additions & 2 deletions appliance/hyperv-README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ This zip contains:
------------------------------------------------------------------------------
Notes
------------------------------------------------------------------------------
* First boot configures PostgreSQL + Dispatch; allow a few minutes.
* First boot creates the bundled SQLite database and starts Dispatch - no
database server to bootstrap, so it's quick.
* The appliance is self-contained - no separate .NET runtime needed.
* Full docs: https://chrismuench.github.io/Dispatch-SMTP-Relay/
* Full docs: https://docs.dispatchrelay.app/
==============================================================================
5 changes: 3 additions & 2 deletions appliance/import-libvirt.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
#
# Import the Dispatch SMTP Relay appliance qcow2 into KVM/libvirt as a UEFI VM (virt-install --import).
# The appliance configures PostgreSQL + Dispatch on first boot; then browse to https://<vm-ip>:8420.
# The appliance creates its bundled SQLite database and starts Dispatch on first boot (quick - no database
# server to bootstrap); then browse to https://<vm-ip>:8420.
#
# Usage:
# sudo ./import-libvirt.sh dispatch-appliance.qcow2 [--name dispatch] [--memory 4096] [--vcpus 2]
Expand Down Expand Up @@ -54,7 +55,7 @@ virt-install \
# qemu-guest-agent then reports the VM's IP to libvirt ("virsh domifaddr <vm> --source agent").

echo
echo "VM '$NAME' defined. First boot configures PostgreSQL + Dispatch (a few minutes)."
echo "VM '$NAME' defined. First boot creates the bundled SQLite database and starts Dispatch - it's quick."
[ "$START" = 1 ] || echo "Start it with: virsh start $NAME"
echo "Find its IP with: virsh domifaddr $NAME"
echo "Then browse to https://<vm-ip>:8420 and set the admin password."
5 changes: 3 additions & 2 deletions appliance/import-proxmox.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
#
# Import the Dispatch SMTP Relay appliance qcow2 into Proxmox VE as a UEFI (OVMF) VM. Run on the Proxmox
# host. The appliance configures PostgreSQL + Dispatch on first boot; then browse to https://<vm-ip>:8420.
# host. The appliance creates its bundled SQLite database and starts Dispatch on first boot (quick - no
# database server to bootstrap); then browse to https://<vm-ip>:8420.
#
# Usage:
# ./import-proxmox.sh dispatch-appliance.qcow2 <vmid> [--storage local-lvm] [--bridge vmbr0]
Expand Down Expand Up @@ -50,6 +51,6 @@ qm set "$VMID" --scsi0 "$disk"
qm set "$VMID" --boot order=scsi0

echo
echo "VM $VMID ($NAME) created. First boot configures PostgreSQL + Dispatch (a few minutes)."
echo "VM $VMID ($NAME) created. First boot creates the bundled SQLite database and starts Dispatch - it's quick."
if [ "$START" = 1 ]; then qm start "$VMID"; echo "Started."; else echo "Start it with: qm start $VMID"; fi
echo "Then browse to https://<vm-ip>:8420 and set the admin password."
7 changes: 4 additions & 3 deletions appliance/kvm-README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ This zip contains:
------------------------------------------------------------------------------
After import
------------------------------------------------------------------------------
* Start the VM. First boot configures PostgreSQL + Dispatch (allow a
few minutes). The VM uses DHCP.
* Start the VM. First boot creates the bundled SQLite database and starts
Dispatch - no database server to bootstrap, so it's quick. The VM uses
DHCP.
* Browse to https://<vm-ip>:8420 (self-signed cert - accept the warning) and
SET THE ADMIN PASSWORD on the first login.
* Full docs: https://chrismuench.github.io/Dispatch-SMTP-Relay/
* Full docs: https://docs.dispatchrelay.app/
==============================================================================
7 changes: 4 additions & 3 deletions appliance/vmware-README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ This zip contains:
------------------------------------------------------------------------------
After import
------------------------------------------------------------------------------
* Power on the VM. First boot configures PostgreSQL + Dispatch (allow a
few minutes). The VM uses DHCP.
* Power on the VM. First boot creates the bundled SQLite database and
starts Dispatch - no database server to bootstrap, so it's quick. The VM
uses DHCP.
* Browse to https://<vm-ip>:8420 (self-signed cert - accept the warning) and
SET THE ADMIN PASSWORD on the first login.
* Full docs: https://chrismuench.github.io/Dispatch-SMTP-Relay/
* Full docs: https://docs.dispatchrelay.app/
==============================================================================
Loading