Skip to content

Instantly share code, notes, and snippets.

@rajeakshay
Last active January 19, 2026 08:19
Show Gist options
  • Select an option

  • Save rajeakshay/a0f45d14f2195db844e642f4484f4676 to your computer and use it in GitHub Desktop.

Select an option

Save rajeakshay/a0f45d14f2195db844e642f4484f4676 to your computer and use it in GitHub Desktop.
Self-Hosting BentoPDF in Windows 11 with Docker

Self-Hosting BentoPDF in Windows 11 with Docker

  1. Install and start Docker Desktop.
  2. Copy the scripts in this Gist into a folder named C:\SelfHosted\BentoPDF.
    • Launch-BentoPDF.ps1 is the entrypoint to run BentoPDF.
      • It pulls the latest image of bentopdf/bentopdf-simple from Docker Hub and launches a Docker container from it.
      • It can automatically update the image if it is older than 7 days and run a fresh Docker container.
    • bentopdf-idle-monitor.ps1 is a helper script called by Launch-BentoPDF.ps1.
      • It monitors the Docker container running BentoPDF for any network activity such as browser tabs connecting over the container port. Idle Docker container will be stopped after a default timeout of 30 minutes.
      • It will automatically exit if the Docker container running BentoPDF is stopped externally by some other method.
  3. Update Settings to allow executing Powershell scripts. Quick way is to right-click Windows Powershell to Run as administrator and then execute -
    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
  4. Run Windows Powershell as the current user and execute -
    cd C:\SelfHosted\BentoPDF
    .\Launch-BentoPDF.ps1
  5. BentoPDF will be available in your browser at http://localhost:8181.
  6. (Optional): Verify that some config files are auto-generated by the scripts in C:\SelfHosted\BentoPDF. File structure will look something like -
    C:\SelfHosted\BentoPDF\
    ├── Launch-BentoPDF.ps1              ✅ Main launcher
    ├── bentopdf-idle-monitor.ps1        ✅ Idle monitor
    ├── bentopdf-config.json             ✅ Auto-created
    └── bentopdf-monitor-config.json     ✅ Auto-created
    
  7. (Optional): Create a Desktop shortcut named BentoPDF to launch target -
    powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\SelfHosted\BentoPDF\Launch-BentoPDF.ps1"
    
    After the Desktop shortcut is created -
    • Right-click on the shortcut to open Properties.
    • Set the working directory with Start in: or a similar field pointing to C:\SelfHosted\BentoPDF. Click Apply and then OK to save the changes.
    • Open %AppData%\Microsoft\Windows\Start Menu\Programs folder in the File Explorer and then Copy-Paste the Desktop shortcut in that folder. This will make the shortcut available as an App in Start.
# BentoPDF Idle Monitor
# Monitors container for idle activity and stops it after configured timeout
# Set window title for Task Manager identification
$Host.UI.RawUI.WindowTitle = "BentoPDF Idle Monitor"
$ErrorActionPreference = "SilentlyContinue"
# Get script directory and load config
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ConfigFile = Join-Path $ScriptDir "bentopdf-monitor-config.json"
# Load configuration
if (-not (Test-Path $ConfigFile)) {
Write-Host "Monitor config not found. Exiting." -ForegroundColor Red
exit 1
}
try {
$config = Get-Content $ConfigFile -Raw | ConvertFrom-Json
$containerName = $config.ContainerName
$idleMinutes = $config.IdleMinutes
$port = $config.Port
} catch {
Write-Host "Failed to load monitor config. Exiting." -ForegroundColor Red
exit 1
}
Write-Host "BentoPDF Idle Monitor Started" -ForegroundColor Cyan
Write-Host "Container: $containerName" -ForegroundColor Gray
Write-Host "Port: $port" -ForegroundColor Gray
Write-Host "Idle timeout: $idleMinutes minutes" -ForegroundColor Gray
Write-Host "Monitoring started at $(Get-Date -Format 'HH:mm:ss')" -ForegroundColor Gray
$lastActivityTime = Get-Date
$checkIntervalSeconds = 60
while ($true) {
Start-Sleep -Seconds $checkIntervalSeconds
# Check if container is still running
$running = docker ps --filter "name=^$containerName`$" --format "{{.Names}}" 2>$null
if ($running -ne $containerName) {
Write-Host "Container stopped externally. Exiting monitor." -ForegroundColor Yellow
exit 0
}
# Check for active TCP connections on the port
$connections = Get-NetTCPConnection -LocalPort $port -State Established -ErrorAction SilentlyContinue
if ($connections) {
$lastActivityTime = Get-Date
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Active connections detected" -ForegroundColor Green
} else {
$idleTime = (Get-Date) - $lastActivityTime
$minutesIdle = [Math]::Round($idleTime.TotalMinutes, 1)
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Idle for $minutesIdle minutes" -ForegroundColor Gray
if ($idleTime.TotalMinutes -ge $idleMinutes) {
Write-Host "`nNo activity for $idleMinutes minutes. Stopping container..." -ForegroundColor Yellow
docker stop $containerName | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host "Container '$containerName' stopped successfully." -ForegroundColor Green
} else {
Write-Host "Failed to stop container." -ForegroundColor Red
}
exit 0
}
}
}
# BentoPDF Docker Launcher Script
param(
[int]$Port = 8181,
[string]$ContainerName = "bentopdf",
[string]$ImageName = "bentopdf/bentopdf-simple:latest",
[int]$MaxDockerWaitSeconds = 60,
[int]$UpdateCheckDays = 7,
[switch]$ForceUpdate,
[int]$IdleShutdownMinutes = 30,
[switch]$NoIdleShutdown
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ConfigFile = Join-Path $ScriptDir "bentopdf-config.json"
$MonitorScript = Join-Path $ScriptDir "bentopdf-idle-monitor.ps1"
function Write-ColorOutput {
param([string]$Message, [string]$Color = "White")
Write-Host $Message -ForegroundColor $Color
}
function Get-Configuration {
if (Test-Path $ConfigFile) {
try {
return (Get-Content $ConfigFile -Raw | ConvertFrom-Json)
}
catch {
return @{LastUpdateCheck = "1970-01-01T00:00:00"; LastImagePull = "1970-01-01T00:00:00"}
}
}
return @{LastUpdateCheck = "1970-01-01T00:00:00"; LastImagePull = "1970-01-01T00:00:00"}
}
function Save-Configuration {
param($Config)
try {
$Config | ConvertTo-Json | Set-Content $ConfigFile -Force
}
catch {
Write-ColorOutput "Warning: Could not save configuration." "Yellow"
}
}
function Test-DockerRunning {
try {
$ErrorActionPreference = "SilentlyContinue"
$output = docker version --format '{{.Server.Version}}' 2>&1
$exitCode = $LASTEXITCODE
$ErrorActionPreference = "Stop"
if ($exitCode -eq 0 -and $output) {
return $true
}
return $false
}
catch {
return $false
}
}
function Wait-ForDocker {
param([int]$MaxWaitSeconds)
Write-ColorOutput "Waiting for Docker Engine..." "Yellow"
$elapsed = 0
while ($elapsed -lt $MaxWaitSeconds) {
if (Test-DockerRunning) {
Write-ColorOutput "Docker Engine is ready!" "Green"
return $true
}
Start-Sleep -Seconds 2
$elapsed += 2
Write-Host "." -NoNewline
}
Write-Host ""
return $false
}
function Get-LocalImageDate {
param([string]$ImageName)
try {
$imageInfo = docker inspect $ImageName 2>$null | ConvertFrom-Json
if ($imageInfo -and $imageInfo.Created) {
return [DateTime]::Parse($imageInfo.Created)
}
}
catch {
return $null
}
return $null
}
function Update-DockerImage {
param([string]$ImageName, [object]$Config)
Write-ColorOutput "`nChecking for updates..." "Yellow"
$currentDate = Get-LocalImageDate -ImageName $ImageName
Write-ColorOutput "Pulling latest image..." "Yellow"
$null = docker pull $ImageName 2>&1
$newDate = Get-LocalImageDate -ImageName $ImageName
$Config.LastUpdateCheck = (Get-Date).ToString("o")
if ($currentDate -and $newDate -and ($newDate -gt $currentDate)) {
Write-ColorOutput "New version downloaded!" "Green"
$Config.LastImagePull = (Get-Date).ToString("o")
Save-Configuration -Config $Config
return $true
}
elseif (-not $currentDate -and $newDate) {
Write-ColorOutput "Image downloaded!" "Green"
$Config.LastImagePull = (Get-Date).ToString("o")
Save-Configuration -Config $Config
return $true
}
else {
Write-ColorOutput "Already on latest version." "Green"
Save-Configuration -Config $Config
return $false
}
}
function Test-ContainerExists {
param([string]$Name)
try {
$result = docker ps -a --filter "name=^${Name}$" --format "{{.Names}}" 2>$null
return ($result -eq $Name)
}
catch {
return $false
}
}
function Test-ContainerRunning {
param([string]$Name)
try {
$result = docker ps --filter "name=^${Name}$" --format "{{.Names}}" 2>$null
return ($result -eq $Name)
}
catch {
return $false
}
}
function Get-ContainerPort {
param([string]$Name)
try {
$portInfo = docker port $Name 8080 2>$null
if ($portInfo -match ':(\d+)$') {
return [int]$Matches[1]
}
}
catch {
return $null
}
return $null
}
function Start-IdleMonitor {
param([string]$ContainerName, [int]$IdleMinutes, [int]$Port)
if (-not (Test-Path $MonitorScript)) {
Write-ColorOutput "Warning: Idle monitor script not found" "Yellow"
return
}
$monitorConfig = @{
ContainerName = $ContainerName
IdleMinutes = $IdleMinutes
Port = $Port
}
$monitorConfigFile = Join-Path $ScriptDir "bentopdf-monitor-config.json"
$monitorConfig | ConvertTo-Json | Set-Content $monitorConfigFile -Force
# Start with a custom window title that will appear in Task Manager
Start-Process powershell.exe -ArgumentList "-WindowStyle Hidden -ExecutionPolicy Bypass -Command `"& {`$Host.UI.RawUI.WindowTitle='BentoPDF Idle Monitor'; & '$MonitorScript'}`"" -WindowStyle Hidden
}
Write-ColorOutput "`n=== BentoPDF Launcher ===" "Cyan"
Write-ColorOutput "Container: $ContainerName" "Cyan"
Write-ColorOutput "Port: $Port" "Cyan"
$config = Get-Configuration
if ($config.LastImagePull -ne "1970-01-01T00:00:00") {
$lastPull = [DateTime]::Parse($config.LastImagePull)
$daysSince = [Math]::Round(((Get-Date) - $lastPull).TotalDays, 1)
Write-ColorOutput "Last update: $daysSince days ago" "Cyan"
}
if (-not $NoIdleShutdown -and $IdleShutdownMinutes -gt 0) {
Write-ColorOutput "Idle shutdown: $IdleShutdownMinutes minutes" "Cyan"
}
Write-ColorOutput "" "White"
# Debug: Test Docker
$dockerTest = Test-DockerRunning
Write-ColorOutput "Docker status check: $dockerTest" "Gray"
if (-not $dockerTest) {
Write-ColorOutput "Starting Docker Desktop..." "Yellow"
$dockerPath = "${env:ProgramFiles}\Docker\Docker\Docker Desktop.exe"
if (Test-Path $dockerPath) {
Start-Process $dockerPath
if (-not (Wait-ForDocker -MaxWaitSeconds $MaxDockerWaitSeconds)) {
Write-ColorOutput "Docker did not start in time." "Red"
Read-Host "Press Enter to exit"
exit 1
}
}
else {
Write-ColorOutput "Docker Desktop not found." "Red"
Read-Host "Press Enter to exit"
exit 1
}
}
else {
Write-ColorOutput "Docker is running." "Green"
}
$needsUpdate = $ForceUpdate -or ((Get-Date) - [DateTime]::Parse($config.LastUpdateCheck)).TotalDays -ge $UpdateCheckDays
if ($needsUpdate) {
$updated = Update-DockerImage -ImageName $ImageName -Config $config
if ($updated -and (Test-ContainerExists -Name $ContainerName)) {
Write-ColorOutput "Recreating container with new version..." "Yellow"
$null = docker stop $ContainerName 2>&1
$null = docker rm $ContainerName 2>&1
}
}
else {
$nextCheck = [DateTime]::Parse($config.LastUpdateCheck).AddDays($UpdateCheckDays)
$daysUntil = [Math]::Ceiling(($nextCheck - (Get-Date)).TotalDays)
Write-ColorOutput "Next update check in $daysUntil days" "Gray"
}
$running = Test-ContainerRunning -Name $ContainerName
if ($running) {
Write-ColorOutput "Container already running." "Green"
$actualPort = Get-ContainerPort -Name $ContainerName
if ($actualPort) {
$Port = $actualPort
}
}
else {
if (Test-ContainerExists -Name $ContainerName) {
Write-ColorOutput "Starting existing container..." "Yellow"
$null = docker start $ContainerName
Start-Sleep -Seconds 2
}
else {
Write-ColorOutput "Creating new container..." "Yellow"
$null = docker run -d --name $ContainerName -p "${Port}:8080" --restart unless-stopped $ImageName
Start-Sleep -Seconds 3
}
}
if (Test-ContainerRunning -Name $ContainerName) {
$url = "http://localhost:$Port"
Write-ColorOutput "`nBentoPDF is ready!" "Green"
Write-ColorOutput "URL: $url" "Cyan"
if (-not $NoIdleShutdown -and $IdleShutdownMinutes -gt 0) {
Start-IdleMonitor -ContainerName $ContainerName -IdleMinutes $IdleShutdownMinutes -Port $Port
Write-ColorOutput "Idle monitor started ($IdleShutdownMinutes min timeout)" "Cyan"
}
Write-ColorOutput "`nOpening browser..." "Yellow"
try {
Start-Process $url
Write-ColorOutput "Browser opened successfully!" "Green"
Write-ColorOutput "`nBentoPDF is running!" "Green"
Start-Sleep -Seconds 2
exit 0
}
catch {
Write-ColorOutput "Failed to open browser. Please navigate to: $url" "Yellow"
Read-Host "Press Enter to exit"
exit 1
}
}
else {
Write-ColorOutput "Container failed to start." "Red"
Read-Host "Press Enter to exit"
exit 1
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment