Hybrid install mode done?

This commit is contained in:
Owen 2025-08-20 12:40:21 -07:00
parent ad8ab63fd5
commit 8273554a1c
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
4 changed files with 140 additions and 100 deletions

View file

@ -1,6 +1,15 @@
# To see all available options, please visit the docs: # To see all available options, please visit the docs:
# https://docs.digpangolin.com/self-host/advanced/config-file # https://docs.digpangolin.com/self-host/advanced/config-file
gerbil:
start_port: 51820
base_endpoint: "{{.DashboardDomain}}"
{{if .HybridMode}}
hybrid:
id: "{{.HybridId}}"
secret: "{{.HybridSecret}}"
{{else}}
app: app:
dashboard_url: "https://{{.DashboardDomain}}" dashboard_url: "https://{{.DashboardDomain}}"
log_level: "info" log_level: "info"
@ -17,11 +26,6 @@ server:
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"] allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false credentials: false
gerbil:
start_port: 51820
base_endpoint: "{{.DashboardDomain}}"
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"
@ -30,15 +34,9 @@ email:
smtp_pass: "{{.EmailSMTPPass}}" smtp_pass: "{{.EmailSMTPPass}}"
no_reply: "{{.EmailNoReply}}" no_reply: "{{.EmailNoReply}}"
{{end}} {{end}}
flags: flags:
require_email_verification: {{.EnableEmail}} require_email_verification: {{.EnableEmail}}
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
{{if and .HybridMode .HybridId .HybridSecret}}
hybrid:
id: "{{.HybridId}}"
secret: "{{.HybridSecret}}"
{{end}} {{end}}

View file

@ -6,6 +6,8 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./config:/app/config - ./config:/app/config
- pangolin-data:/var/certificates
- pangolin-data:/var/dynamic
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"] test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "10s" interval: "10s"
@ -31,8 +33,8 @@ services:
ports: ports:
- 51820:51820/udp - 51820:51820/udp
- 21820:21820/udp - 21820:21820/udp
- 443:443 # Port for traefik because of the network_mode - 443:{{if .HybridMode}}8443{{else}}443{{end}}
- 80:80 # Port for traefik because of the network_mode - 80:80
{{end}} {{end}}
traefik: traefik:
image: docker.io/traefik:v3.5 image: docker.io/traefik:v3.5
@ -54,9 +56,15 @@ services:
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration - ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
# Shared volume for certificates and dynamic config in file mode
- pangolin-data:/var/certificates:ro
- pangolin-data:/var/dynamic:ro
networks: networks:
default: default:
driver: bridge driver: bridge
name: pangolin name: pangolin
{{if .EnableIPv6}} enable_ipv6: true{{end}} {{if .EnableIPv6}} enable_ipv6: true{{end}}
volumes:
pangolin-data:

View file

@ -3,12 +3,17 @@ api:
dashboard: true dashboard: true
providers: providers:
{{if not .HybridMode}}
http: http:
endpoint: "http://pangolin:3001/api/v1/traefik-config" endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s" pollInterval: "5s"
file: file:
filename: "/etc/traefik/dynamic_config.yml" filename: "/etc/traefik/dynamic_config.yml"
{{else}}
file:
directory: "/var/dynamic"
watch: true
{{end}}
experimental: experimental:
plugins: plugins:
badger: badger:
@ -22,7 +27,7 @@ log:
maxBackups: 3 maxBackups: 3
maxAge: 3 maxAge: 3
compress: true compress: true
{{if not .HybridMode}}
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
acme: acme:
@ -31,7 +36,7 @@ certificatesResolvers:
email: "{{.LetsEncryptEmail}}" email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json" storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory" caServer: "https://acme-v02.api.letsencrypt.org/directory"
{{end}}
entryPoints: entryPoints:
web: web:
address: ":80" address: ":80"
@ -40,9 +45,12 @@ entryPoints:
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: "30m" readTimeout: "30m"
http: {{if not .HybridMode}} http:
tls: tls:
certResolver: "letsencrypt" certResolver: "letsencrypt"{{end}}
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true
ping:
entryPoint: "web"

View file

@ -65,12 +65,9 @@ func main() {
fmt.Println("Welcome to the Pangolin installer!") fmt.Println("Welcome to the Pangolin installer!")
fmt.Println("This installer will help you set up Pangolin on your server.") fmt.Println("This installer will help you set up Pangolin on your server.")
fmt.Println("") fmt.Println("\nPlease make sure you have the following prerequisites:")
fmt.Println("Please make sure you have the following prerequisites:")
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.") fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
fmt.Println("") fmt.Println("\nLets get started!")
fmt.Println("Lets get started!")
fmt.Println("")
if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS
for _, p := range []int{80, 443} { for _, p := range []int{80, 443} {
@ -102,51 +99,52 @@ func main() {
moveFile("config/docker-compose.yml", "docker-compose.yml") moveFile("config/docker-compose.yml", "docker-compose.yml")
config.InstallationContainerType = podmanOrDocker(reader) fmt.Println("\nConfiguration files created successfully!")
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
installDocker()
// try to start docker service but ignore errors
if err := startDockerService(); err != nil {
fmt.Println("Error starting Docker service:", err)
} else {
fmt.Println("Docker service started successfully!")
}
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
fmt.Println("Waiting for Docker to start...")
for i := 0; i < 5; i++ {
if isDockerRunning() {
fmt.Println("Docker is running!")
break
}
fmt.Println("Docker is not running yet, waiting...")
time.Sleep(2 * time.Second)
}
if !isDockerRunning() {
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
os.Exit(1)
}
fmt.Println("Docker installed successfully!")
}
}
fmt.Println("\n=== Starting installation ===") fmt.Println("\n=== Starting installation ===")
if (isDockerInstalled() && config.InstallationContainerType == Docker) || if readBool(reader, "Would you like to install and start the containers?", true) {
(isPodmanInstalled() && config.InstallationContainerType == Podman) {
if readBool(reader, "Would you like to install and start the containers?", true) {
if err := pullContainers(config.InstallationContainerType); err != nil {
fmt.Println("Error: ", err)
return
}
if err := startContainers(config.InstallationContainerType); err != nil { config.InstallationContainerType = podmanOrDocker(reader)
fmt.Println("Error: ", err)
return if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
installDocker()
// try to start docker service but ignore errors
if err := startDockerService(); err != nil {
fmt.Println("Error starting Docker service:", err)
} else {
fmt.Println("Docker service started successfully!")
}
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
fmt.Println("Waiting for Docker to start...")
for i := 0; i < 5; i++ {
if isDockerRunning() {
fmt.Println("Docker is running!")
break
}
fmt.Println("Docker is not running yet, waiting...")
time.Sleep(2 * time.Second)
}
if !isDockerRunning() {
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
os.Exit(1)
}
fmt.Println("Docker installed successfully!")
} }
} }
if err := pullContainers(config.InstallationContainerType); err != nil {
fmt.Println("Error: ", err)
return
}
if err := startContainers(config.InstallationContainerType); err != nil {
fmt.Println("Error: ", err)
return
}
} }
} else { } else {
fmt.Println("Looks like you already installed, so I am going to do the setup...") fmt.Println("Looks like you already installed, so I am going to do the setup...")
@ -171,15 +169,16 @@ func main() {
config = collectUserInput(reader) config = collectUserInput(reader)
} }
} }
}
// Check if Pangolin is already installed with hybrid section // Check if Pangolin is already installed with hybrid section
if checkIsPangolinInstalledWithHybrid() { // if checkIsPangolinInstalledWithHybrid() {
fmt.Println("\n=== Convert to Self-Host Node ===") // fmt.Println("\n=== Convert to Self-Host Node ===")
if readBool(reader, "Do you want to convert this Pangolin instance into a manage self-host node?", true) { // if readBool(reader, "Do you want to convert this Pangolin instance into a managed self-host node?", true) {
fmt.Println("hello world") // fmt.Println("hello world")
return // return
} // }
// }
} }
if !checkIsCrowdsecInstalledInCompose() { if !checkIsCrowdsecInstalledInCompose() {
@ -217,25 +216,30 @@ func main() {
} }
} }
// Setup Token Section if !config.HybridMode {
fmt.Println("\n=== Setup Token ===") // Setup Token Section
fmt.Println("\n=== Setup Token ===")
// Check if containers were started during this installation // Check if containers were started during this installation
containersStarted := false containersStarted := false
if (isDockerInstalled() && config.InstallationContainerType == Docker) || if (isDockerInstalled() && config.InstallationContainerType == Docker) ||
(isPodmanInstalled() && config.InstallationContainerType == Podman) { (isPodmanInstalled() && config.InstallationContainerType == Podman) {
// Try to fetch and display the token if containers are running // Try to fetch and display the token if containers are running
containersStarted = true containersStarted = true
printSetupToken(config.InstallationContainerType, config.DashboardDomain) printSetupToken(config.InstallationContainerType, config.DashboardDomain)
}
// If containers weren't started or token wasn't found, show instructions
if !containersStarted {
showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain)
}
} }
// If containers weren't started or token wasn't found, show instructions fmt.Println("\nInstallation complete!")
if !containersStarted {
showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain)
}
fmt.Println("Installation complete!") if !config.HybridMode {
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
}
} }
func podmanOrDocker(reader *bufio.Reader) SupportedContainer { func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
@ -310,18 +314,32 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration // Basic configuration
fmt.Println("\n=== Basic Configuration ===") fmt.Println("\n=== Basic Configuration ===")
config.HybridMode = readBoolNoDefault(reader, "Do you want to use hybrid mode?") for {
response := readString(reader, "Do you want to install Pangolin as a cloud-managed self-hosted node? (yes/no)", "")
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
config.HybridMode = true
break
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
config.HybridMode = false
break
}
fmt.Println("Please answer 'yes' or 'no'")
}
if config.HybridMode { if config.HybridMode {
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false) alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false)
if alreadyHaveCreds { if alreadyHaveCreds {
config.HybridId = readString(reader, "Enter your hybrid ID", "") config.HybridId = readString(reader, "Enter your hybrid ID", "")
config.HybridSecret = readString(reader, "Enter your hybrid secret", "") config.HybridSecret = readString(reader, "Enter your hybrid secret", "")
} else {
// Just print instructions for right now
fmt.Println("Please visit https://pangolin.fossorial.io, create a self hosted node, and return with the credentials.")
} }
}
if !config.HybridMode { config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
config.InstallGerbil = true
} else {
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
// Set default dashboard domain after base domain is collected // Set default dashboard domain after base domain is collected
@ -331,16 +349,12 @@ func collectUserInput(reader *bufio.Reader) Config {
} }
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
} config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
if !config.HybridMode {
// Email configuration // Email configuration
fmt.Println("\n=== Email Configuration ===") fmt.Println("\n=== Email Configuration ===")
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
if config.EnableEmail { if config.EnableEmail {
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
@ -348,23 +362,29 @@ func collectUserInput(reader *bufio.Reader) Config {
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
config.EmailNoReply = readString(reader, "Enter no-reply email address", "") config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
} }
// Validate required fields // Validate required fields
if config.BaseDomain == "" { if config.BaseDomain == "" {
fmt.Println("Error: Domain name is required") fmt.Println("Error: Domain name is required")
os.Exit(1) os.Exit(1)
} }
if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required")
os.Exit(1)
}
if config.LetsEncryptEmail == "" { if config.LetsEncryptEmail == "" {
fmt.Println("Error: Let's Encrypt email is required") fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1) os.Exit(1)
} }
} }
// Advanced configuration
fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required")
os.Exit(1)
}
return config return config
} }
@ -393,6 +413,11 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
// the hybrid does not need the dynamic config
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
return nil
}
// skip .DS_Store // skip .DS_Store
if strings.Contains(path, ".DS_Store") { if strings.Contains(path, ".DS_Store") {
return nil return nil
@ -443,6 +468,7 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
func copyFile(src, dst string) error { func copyFile(src, dst string) error {
source, err := os.Open(src) source, err := os.Open(src)
if err != nil { if err != nil {