diff --git a/command/up.go b/command/up.go index 9cee823..04c3419 100644 --- a/command/up.go +++ b/command/up.go @@ -62,6 +62,24 @@ func upRun() { project.CreateCert() } + // Generate web server config + phpVersion := project.Env.GetString("PHP_VERSION") + if strings.Contains(phpVersion, "apache") { + confPath, err := project.WriteApacheConfig() + if err != nil { + pterm.FgRed.Printfln("Failed to generate apache config: %s", err) + return + } + project.Env.Set("APACHE_CONF", confPath) + } else if strings.Contains(phpVersion, "fpm") && (len(project.Env.GetString("DOMAIN_MAP")) > 0 || len(project.Env.GetString("DOMAINS")) > 0) { + confPath, err := project.WriteNginxConfig() + if err != nil { + pterm.FgRed.Printfln("Failed to generate nginx config: %s", err) + return + } + project.Env.Set("NGINX_CONF", confPath) + } + bin, option := utils.GetCompose() Args := []string{bin} preArgs := []string{"-p", project.Env.GetString("NETWORK_NAME"), "--project-directory", project.Env.GetString("PWD"), "up", "-d"} @@ -117,20 +135,26 @@ func startLocalServices() error { // showProjectInfo Display project links func showProjectInfo() { - l := project.Env.GetString("LOCAL_DOMAIN") - n := project.Env.GetString("NIP_DOMAIN") - schema := "http" - if viper.GetBool("ca") { schema = "https" } pterm.FgCyan.Println() - panels := pterm.Panels{ - {{Data: pterm.FgYellow.Sprintf("nip.io\nlocal")}, - {Data: pterm.FgYellow.Sprintf(schema+"://%s/\n"+schema+"://%s/", n, l)}}, - } - _ = pterm.DefaultPanel.WithPanels(panels).WithPadding(5).Render() + if len(project.Env.GetString("DOMAIN_MAP")) > 0 || len(project.Env.GetString("DOMAINS")) > 0 { + tableData := pterm.TableData{{"Domain", "Document Root"}} + for _, m := range project.DomainMappings { + tableData = append(tableData, []string{schema + "://" + m.LocalDomain + "/", m.DocumentRoot}) + } + _ = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() + } else { + l := project.Env.GetString("LOCAL_DOMAIN") + n := project.Env.GetString("NIP_DOMAIN") + panels := pterm.Panels{ + {{Data: pterm.FgYellow.Sprintf("nip.io\nlocal")}, + {Data: pterm.FgYellow.Sprintf(schema+"://%s/\n"+schema+"://%s/", n, l)}}, + } + _ = pterm.DefaultPanel.WithPanels(panels).WithPadding(5).Render() + } } diff --git a/main.go b/main.go index efd361f..d84c1b7 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,10 @@ import ( var version = "dev" +// templateVersion must be incremented whenever files in the templates/ directory change. +// This triggers a full re-extraction of embedded templates to ~/.config/dl/templates/. +const templateVersion = "2" + //go:embed templates/* var templates embed.FS @@ -79,6 +83,21 @@ func initConfig() { } } + // Re-extract templates when templateVersion changes + if viper.GetString("template_version") != templateVersion { + err = utils.CreateTemplates(true) + if err != nil { + pterm.FgRed.Printfln("Error updating templates: %s \n", err) + os.Exit(1) + } + viper.Set("template_version", templateVersion) + err = viper.WriteConfig() + if err != nil { + pterm.FgRed.Printfln("Error config file: %s \n", err) + os.Exit(1) + } + } + viper.AutomaticEnv() } diff --git a/project/apache.go b/project/apache.go new file mode 100644 index 0000000..cae025a --- /dev/null +++ b/project/apache.go @@ -0,0 +1,62 @@ +package project + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +// GenerateApacheConfig builds Apache vhost configuration from DomainMappings +func GenerateApacheConfig() string { + var b []byte + + for i, dm := range DomainMappings { + if i > 0 { + b = append(b, '\n') + } + b = fmt.Appendf(b, "\n") + b = fmt.Appendf(b, " ServerName %s\n", dm.LocalDomain) + b = fmt.Appendf(b, " ServerAlias %s\n", dm.NipDomain) + b = fmt.Appendf(b, " DocumentRoot %s\n", dm.DocumentRoot) + b = fmt.Appendf(b, "\n") + b = fmt.Appendf(b, " \n", dm.DocumentRoot) + b = fmt.Appendf(b, " AllowOverride All\n") + b = fmt.Appendf(b, " Require all granted\n") + b = fmt.Appendf(b, " \n") + b = fmt.Appendf(b, "\n") + } + + return string(b) +} + +// WriteApacheConfig writes the generated Apache vhost config to /tmp/dl//apache/. +// Skips regeneration if .env and project folder haven't changed. +// Returns the absolute path to the config file. +func WriteApacheConfig() (string, error) { + networkName := Env.GetString("NETWORK_NAME") + dir := filepath.Join(os.TempDir(), "dl", networkName, "apache") + confPath := filepath.Join(dir, "vhosts.conf") + hashPath := filepath.Join(dir, ".confhash") + + if !configNeedsUpdate(hashPath) { + logrus.Info("Apache config is up to date, skipping regeneration") + return confPath, nil + } + + if err := os.MkdirAll(dir, 0755); err != nil { + return "", fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + config := GenerateApacheConfig() + + if err := os.WriteFile(confPath, []byte(config), 0644); err != nil { + return "", fmt.Errorf("failed to write %s: %w", confPath, err) + } + + saveConfigHash(hashPath) + logrus.Info("Apache config regenerated") + + return confPath, nil +} diff --git a/project/deploy_db.go b/project/deploy_db.go index 3960515..1ac1462 100644 --- a/project/deploy_db.go +++ b/project/deploy_db.go @@ -430,14 +430,18 @@ func (c SSHClient) ImportDB(ctx context.Context) error { } if c.Config.FwType == "bitrix" { - local := Env.GetString("LOCAL_DOMAIN") - nip := Env.GetString("NIP_DOMAIN") + firstDomain := DomainMappings[0] + + var domainInserts strings.Builder + for _, m := range DomainMappings { + domainInserts.WriteString(fmt.Sprintf("INSERT IGNORE INTO b_lang_domain VALUES ('s1', '%s');\n", m.LocalDomain)) + domainInserts.WriteString(fmt.Sprintf("INSERT IGNORE INTO b_lang_domain VALUES ('s1', '%s');\n", m.NipDomain)) + } strSQL := `"UPDATE b_option SET VALUE = 'Y' WHERE MODULE_ID = 'main' AND NAME = 'update_devsrv'; -UPDATE b_lang SET SERVER_NAME='` + site + `.localhost' WHERE LID='s1'; +UPDATE b_lang SET SERVER_NAME='` + firstDomain.LocalDomain + `' WHERE LID='s1'; UPDATE b_lang SET b_lang.DOC_ROOT='' WHERE 1=(SELECT DOC_ROOT FROM (SELECT COUNT(LID) FROM b_lang) as cnt); -INSERT IGNORE INTO b_lang_domain VALUES ('s1', '` + local + `'); -INSERT IGNORE INTO b_lang_domain VALUES ('s1', '` + nip + `');"` +` + domainInserts.String() + `"` commandUpdate := "echo " + strSQL + " | " + docker + " exec -i " + siteDB + " /usr/bin/mysql --user=" + mysqlUser + " --password=" + mysqlPassword + " --host=db " + mysqlDB + "" logrus.Infof("Run command: %s", commandUpdate) diff --git a/project/env.go b/project/env.go index c5e7646..7f26345 100644 --- a/project/env.go +++ b/project/env.go @@ -17,6 +17,9 @@ import ( // Env Project variables var Env *viper.Viper +// DomainMappings holds parsed domain-to-document-root mappings +var DomainMappings []DomainMapping + var phpImagesVersion = map[string]string{ "7.3-apache": "1.1.3", "7.3-fpm": "1.0.3", @@ -55,6 +58,13 @@ func LoadEnv() { } setDefaultEnv() + + if err := validateDomains(); err != nil { + pterm.FgRed.Println(err) + os.Exit(1) + } + parseDomainMappings() + setComposeFiles() } @@ -91,6 +101,8 @@ func setDefaultEnv() { Env.Set("NGINX_CONF", getNginxConf()) } + Env.SetDefault("APACHE_CONF", "/dev/null") + Env.SetDefault("REDIS", false) Env.SetDefault("REDIS_PASSWORD", "pass") Env.SetDefault("MEMCACHED", false) @@ -218,6 +230,162 @@ func IsEnvExampleFileExists() bool { return err == nil } +func validateDomains() error { + domains := Env.GetString("DOMAINS") + domainMap := Env.GetString("DOMAIN_MAP") + + // Rule 1: DOMAINS and DOMAIN_MAP are mutually exclusive + if len(domains) > 0 && len(domainMap) > 0 { + return fmt.Errorf("DOMAINS and DOMAIN_MAP are mutually exclusive. Use DOMAINS for multiple domains with one document root, or DOMAIN_MAP for different document roots per domain.") + } + + domainNameRegex := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]*$`) + seen := make(map[string]bool) + + if len(domainMap) > 0 { + entries := strings.Split(domainMap, ",") + for _, entry := range entries { + entry = strings.TrimSpace(entry) + + // Rule 3: must contain ':' + if !strings.Contains(entry, ":") { + return fmt.Errorf("Invalid DOMAIN_MAP format for entry \"%s\". Expected format: name:/path/to/docroot", entry) + } + + parts := strings.SplitN(entry, ":", 2) + name := strings.TrimSpace(parts[0]) + path := strings.TrimSpace(parts[1]) + + // Rule 4: empty name + if len(name) == 0 { + return fmt.Errorf("Empty domain name in DOMAIN_MAP entry: \"%s\"", entry) + } + + // Rule 5: empty path + if len(path) == 0 { + return fmt.Errorf("Empty document root in DOMAIN_MAP entry for domain \"%s\"", name) + } + + // Rule 6: path must be absolute + if !filepath.IsAbs(path) { + return fmt.Errorf("Document root must be an absolute path for domain \"%s\": got \"%s\"", name, path) + } + + nameLower := strings.ToLower(name) + + // Rule 8: invalid domain name chars + if !domainNameRegex.MatchString(nameLower) { + return fmt.Errorf("Invalid domain name \"%s\": only alphanumeric characters, hyphens, and dots are allowed.", nameLower) + } + + // Rule 9: duplicate domain + if seen[nameLower] { + return fmt.Errorf("Duplicate domain name \"%s\" in DOMAIN_MAP.", nameLower) + } + seen[nameLower] = true + } + } + + if len(domains) > 0 { + // Rule 2: DOMAINS requires DOCUMENT_ROOT + if len(Env.GetString("DOCUMENT_ROOT")) == 0 { + return fmt.Errorf("DOCUMENT_ROOT is required when using DOMAINS.") + } + + entries := strings.Split(domains, ",") + for _, entry := range entries { + name := strings.TrimSpace(entry) + + // Rule 7: empty entry + if len(name) == 0 { + return fmt.Errorf("Empty domain name in DOMAINS. Check for trailing commas.") + } + + nameLower := strings.ToLower(name) + + // Rule 8: invalid domain name chars + if !domainNameRegex.MatchString(nameLower) { + return fmt.Errorf("Invalid domain name \"%s\": only alphanumeric characters, hyphens, and dots are allowed.", nameLower) + } + + // Rule 9: duplicate domain + if seen[nameLower] { + return fmt.Errorf("Duplicate domain name \"%s\" in DOMAINS.", nameLower) + } + seen[nameLower] = true + } + } + + return nil +} + +func parseDomainMappings() { + DomainMappings = nil + localIP := Env.GetString("LOCAL_IP") + domainMap := Env.GetString("DOMAIN_MAP") + domains := Env.GetString("DOMAINS") + + if len(domainMap) > 0 { + entries := strings.Split(domainMap, ",") + for _, entry := range entries { + entry = strings.TrimSpace(entry) + parts := strings.SplitN(entry, ":", 2) + name := strings.ToLower(strings.TrimSpace(parts[0])) + docRoot := strings.TrimSpace(parts[1]) + + DomainMappings = append(DomainMappings, DomainMapping{ + Name: name, + DocumentRoot: docRoot, + LocalDomain: fmt.Sprintf("%s.localhost", name), + NipDomain: fmt.Sprintf("%s.%s.nip.io", name, localIP), + }) + } + logrus.Infof("Parsed %d domain mappings from DOMAIN_MAP", len(DomainMappings)) + generateTraefikRule() + return + } + + if len(domains) > 0 { + docRoot := Env.GetString("DOCUMENT_ROOT") + entries := strings.Split(domains, ",") + for _, entry := range entries { + name := strings.ToLower(strings.TrimSpace(entry)) + + DomainMappings = append(DomainMappings, DomainMapping{ + Name: name, + DocumentRoot: docRoot, + LocalDomain: fmt.Sprintf("%s.localhost", name), + NipDomain: fmt.Sprintf("%s.%s.nip.io", name, localIP), + }) + } + logrus.Infof("Parsed %d domain mappings from DOMAINS", len(DomainMappings)) + generateTraefikRule() + return + } + + // Backward compatibility: single mapping from HOST_NAME + DOCUMENT_ROOT + hostName := strings.ToLower(Env.GetString("HOST_NAME")) + DomainMappings = append(DomainMappings, DomainMapping{ + Name: hostName, + DocumentRoot: Env.GetString("DOCUMENT_ROOT"), + LocalDomain: fmt.Sprintf("%s.localhost", hostName), + NipDomain: fmt.Sprintf("%s.%s.nip.io", hostName, localIP), + }) + logrus.Infof("Using single domain mapping from HOST_NAME: %s", hostName) + generateTraefikRule() +} + +func generateTraefikRule() { + var rules []string + for _, m := range DomainMappings { + rules = append(rules, fmt.Sprintf("Host(`%s`)", m.LocalDomain)) + rules = append(rules, fmt.Sprintf("HostRegexp(`^.+\\.%s$`)", m.LocalDomain)) + rules = append(rules, fmt.Sprintf("HostRegexp(`^%s\\..+\\.nip\\.io$`)", m.Name)) + rules = append(rules, fmt.Sprintf("HostRegexp(`^.+\\.%s\\..+\\.nip\\.io$`)", m.Name)) + } + Env.Set("TRAEFIK_RULE", strings.Join(rules, " || ")) +} + func getLocalIP() string { addrs, err := net.InterfaceAddrs() if err != nil { diff --git a/project/nginx.go b/project/nginx.go new file mode 100644 index 0000000..8af3c40 --- /dev/null +++ b/project/nginx.go @@ -0,0 +1,142 @@ +package project + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/local-deploy/dl/utils" + "github.com/sirupsen/logrus" +) + +// configNeedsUpdate checks whether the generated web server config is stale +// by comparing a SHA-256 hash of .env contents + project folder name against a stored hash. +func configNeedsUpdate(hashFilePath string) bool { + pwd := Env.GetString("PWD") + envPath := filepath.Join(pwd, ".env") + + envContent, err := os.ReadFile(envPath) + if err != nil { + return true + } + + h := sha256.New() + h.Write(envContent) + h.Write([]byte(filepath.Base(pwd))) + currentHash := hex.EncodeToString(h.Sum(nil)) + + storedHash, err := os.ReadFile(hashFilePath) + if err != nil { + return true + } + + return strings.TrimSpace(string(storedHash)) != currentHash +} + +// saveConfigHash persists the current hash so subsequent runs can skip regeneration. +func saveConfigHash(hashFilePath string) { + pwd := Env.GetString("PWD") + envPath := filepath.Join(pwd, ".env") + + envContent, err := os.ReadFile(envPath) + if err != nil { + return + } + + h := sha256.New() + h.Write(envContent) + h.Write([]byte(filepath.Base(pwd))) + currentHash := hex.EncodeToString(h.Sum(nil)) + + _ = os.WriteFile(hashFilePath, []byte(currentHash), 0644) +} + +// GenerateNginxConfig builds a complete nginx config from DomainMappings. +// One server block is generated per DomainMapping entry. +func GenerateNginxConfig() string { + hostName := Env.GetString("HOST_NAME") + var b strings.Builder + + for i, mapping := range DomainMappings { + if i > 0 { + b.WriteString("\n") + } + + tryFiles := "try_files $uri $uri/ /index.php?$args;" + if utils.BitrixCheck(mapping.DocumentRoot) { + tryFiles = "try_files $uri $uri/ /index.php?$args /bitrix/urlrewrite.php?$args /bitrix/routing_index.php?$args;" + } + + b.WriteString("server {\n") + b.WriteString(" listen 80;\n") + b.WriteString(" listen 443;\n") + b.WriteString("\n") + b.WriteString(fmt.Sprintf(" server_name %s %s;\n", mapping.LocalDomain, mapping.NipDomain)) + b.WriteString(" add_header Strict-Transport-Security \"max-age=31536000\" always;\n") + b.WriteString(" client_max_body_size 200M;\n") + b.WriteString("\n") + b.WriteString(" charset utf-8;\n") + b.WriteString("\n") + b.WriteString(fmt.Sprintf(" set $root_path %s;\n", mapping.DocumentRoot)) + b.WriteString(" root $root_path;\n") + b.WriteString("\n") + b.WriteString(" location / {\n") + b.WriteString(" root $root_path;\n") + b.WriteString(" index index.php index.html;\n") + b.WriteString(fmt.Sprintf(" %s\n", tryFiles)) + b.WriteString(" }\n") + b.WriteString("\n") + b.WriteString(" location ~ \\.php$ {\n") + b.WriteString(fmt.Sprintf(" fastcgi_pass %s_php:9000;\n", hostName)) + b.WriteString(" fastcgi_index index.php;\n") + b.WriteString(" fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n") + b.WriteString(" fastcgi_param DOCUMENT_ROOT $realpath_root;\n") + b.WriteString(" include /etc/nginx/fastcgi_params;\n") + b.WriteString(" }\n") + b.WriteString("\n") + b.WriteString(" location ~* ^.+\\.(jpg|jpeg|gif|png|svg|js|css|mp3|ogg|mpeg|avi|zip|gz|bz2|rar|swf|ico|7z|doc|docx|map|ogg|otf|pdf|tff|tif|txt|wav|webp|woff|woff2|xls|xlsx|xml)$ {\n") + b.WriteString(" expires 365d;\n") + b.WriteString(" try_files $uri $uri/ 404 = @fallback;\n") + b.WriteString(" }\n") + b.WriteString("\n") + b.WriteString(" location @fallback {\n") + b.WriteString(fmt.Sprintf(" return 302 https://%s/$uri;\n", mapping.LocalDomain)) + b.WriteString(" }\n") + b.WriteString("}\n") + } + + return b.String() +} + +// WriteNginxConfig writes the generated nginx config to /tmp/dl//nginx/. +// Skips regeneration if .env and project folder haven't changed. +// Returns the absolute path to the config file. +func WriteNginxConfig() (string, error) { + networkName := Env.GetString("NETWORK_NAME") + dir := filepath.Join(os.TempDir(), "dl", networkName, "nginx") + confPath := filepath.Join(dir, "default.conf") + hashPath := filepath.Join(dir, ".confhash") + + if !configNeedsUpdate(hashPath) { + logrus.Info("Nginx config is up to date, skipping regeneration") + return confPath, nil + } + + if err := os.MkdirAll(dir, 0755); err != nil { + return "", fmt.Errorf("failed to create nginx config directory: %w", err) + } + + config := GenerateNginxConfig() + + if err := os.WriteFile(confPath, []byte(config), 0644); err != nil { + return "", fmt.Errorf("failed to write nginx config: %w", err) + } + + saveConfigHash(hashPath) + logrus.Info("Nginx config regenerated") + + return confPath, nil +} diff --git a/project/ssl.go b/project/ssl.go index 2c6687d..0f8de53 100644 --- a/project/ssl.go +++ b/project/ssl.go @@ -49,10 +49,11 @@ func CreateCert() { certDir := filepath.Join(utils.CertDir(), Env.GetString("NETWORK_NAME")) _ = utils.CreateDirectory(certDir) - err = c.MakeCert([]string{ - Env.GetString("LOCAL_DOMAIN"), - Env.GetString("NIP_DOMAIN"), - }, Env.GetString("NETWORK_NAME")) + var domains []string + for _, m := range DomainMappings { + domains = append(domains, m.LocalDomain, m.NipDomain) + } + err = c.MakeCert(domains, Env.GetString("NETWORK_NAME")) if err != nil { pterm.FgRed.Printfln("Error: %s", err) } diff --git a/project/types.go b/project/types.go index 3b9f80a..45f76be 100644 --- a/project/types.go +++ b/project/types.go @@ -15,3 +15,11 @@ type DBSettings struct { Host, DataBase, Login, Password, Port string ExcludedTables []string } + +// DomainMapping represents a domain-to-document-root mapping +type DomainMapping struct { + Name string // domain name (e.g., "site1") + DocumentRoot string // absolute path (e.g., "/var/www/html/site1") + LocalDomain string // auto-generated: "site1.localhost" + NipDomain string // auto-generated: "site1.192.168.1.100.nip.io" +} diff --git a/templates/.env.example b/templates/.env.example index b0bd3cf..2e2f118 100644 --- a/templates/.env.example +++ b/templates/.env.example @@ -6,6 +6,11 @@ SERVER=127.0.0.1 ## Local container config ## DOCUMENT_ROOT=/var/www/html +## Multi-domain config (optional, mutually exclusive) ## +## Multiple domains, one document root (comma-separated): +# DOMAINS=site1,site2,api +## Multiple domains with different document roots: +# DOMAIN_MAP=site1:/var/www/html/site1,site2:/var/www/html/site2 ## Avalible fpm versions: 7.3-fpm 7.4-fpm 8.0-fpm 8.1-fpm 8.2-fpm 8.3-fpm 8.4-fpm ## ## Avalible apache versions: 7.3-apache 7.4-apache 8.0-apache 8.1-apache 8.2-apache 8.3-apache 8.4-apache ## PHP_VERSION=8.4-fpm diff --git a/templates/.env.example-bitrix b/templates/.env.example-bitrix index 3b94694..7b9c9da 100644 --- a/templates/.env.example-bitrix +++ b/templates/.env.example-bitrix @@ -6,6 +6,11 @@ SERVER=127.0.0.1 ## Local container config ## DOCUMENT_ROOT=/var/www/html +## Multi-domain config (optional, mutually exclusive) ## +## Multiple domains, one document root (comma-separated): +# DOMAINS=site1,site2,api +## Multiple domains with different document roots: +# DOMAIN_MAP=site1:/var/www/html/site1,site2:/var/www/html/site2 ## Avalible fpm versions: 7.3-fpm 7.4-fpm 8.0-fpm 8.1-fpm 8.2-fpm 8.3-fpm 8.4-fpm ## ## Avalible apache versions: 7.3-apache 7.4-apache 8.0-apache 8.1-apache 8.2-apache 8.3-apache 8.4-apache ## PHP_VERSION=8.4-fpm diff --git a/templates/docker-compose-apache.yaml b/templates/docker-compose-apache.yaml index bae8f53..28b0a07 100644 --- a/templates/docker-compose-apache.yaml +++ b/templates/docker-compose-apache.yaml @@ -21,15 +21,16 @@ services: volumes: - "${PWD}/:/var/www/html/" - "${PHP_INI_SOURCE:-/dev/null}:/usr/local/etc/php/conf.custom.d/custom.ini:ro" + - "${APACHE_CONF}:/etc/apache2/sites-enabled/000-default.conf:ro" - "~/.ssh/${SSH_KEY:-id_rsa}:/var/www/.ssh/id_rsa:ro" - "~/.ssh/known_hosts:/var/www/.ssh/known_hosts" labels: - "traefik.enable=true" - "traefik.http.routers.${NETWORK_NAME}.entrypoints=web" - - "traefik.http.routers.${NETWORK_NAME}.rule=Host(`${HOST_NAME}.localhost`) || HostRegexp(`^.+\\.${HOST_NAME}\\.localhost$`) || HostRegexp(`^${HOST_NAME}\\..+\\.nip\\.io$`) || HostRegexp(`^.+\\.${HOST_NAME}\\..+\\.nip\\.io$`)" + - "traefik.http.routers.${NETWORK_NAME}.rule=${TRAEFIK_RULE}" - "traefik.http.routers.${NETWORK_NAME}.middlewares=site-compress" - "traefik.http.routers.${NETWORK_NAME}_ssl.entrypoints=websecure" - - "traefik.http.routers.${NETWORK_NAME}_ssl.rule=Host(`${HOST_NAME}.localhost`) || HostRegexp(`^.+\\.${HOST_NAME}\\.localhost$`) || HostRegexp(`^${HOST_NAME}\\..+\\.nip\\.io$`) || HostRegexp(`^.+\\.${HOST_NAME}\\..+\\.nip\\.io$`)" + - "traefik.http.routers.${NETWORK_NAME}_ssl.rule=${TRAEFIK_RULE}" - "traefik.http.routers.${NETWORK_NAME}_ssl.middlewares=site-compress" - "traefik.http.routers.${NETWORK_NAME}_ssl.tls=true" - "traefik.docker.network=dl_default" diff --git a/templates/docker-compose-fpm.yaml b/templates/docker-compose-fpm.yaml index cbf13a0..9f28aec 100644 --- a/templates/docker-compose-fpm.yaml +++ b/templates/docker-compose-fpm.yaml @@ -36,9 +36,9 @@ services: labels: - "traefik.enable=true" - "traefik.http.routers.${NETWORK_NAME}.entrypoints=web" - - "traefik.http.routers.${NETWORK_NAME}.rule=Host(`${HOST_NAME}.localhost`) || HostRegexp(`^.+\\.${HOST_NAME}\\.localhost$`) || HostRegexp(`^${HOST_NAME}\\..+\\.nip\\.io$`) || HostRegexp(`^.+\\.${HOST_NAME}\\..+\\.nip\\.io$`)" + - "traefik.http.routers.${NETWORK_NAME}.rule=${TRAEFIK_RULE}" - "traefik.http.routers.${NETWORK_NAME}_ssl.entrypoints=websecure" - - "traefik.http.routers.${NETWORK_NAME}_ssl.rule=Host(`${HOST_NAME}.localhost`) || HostRegexp(`^.+\\.${HOST_NAME}\\.localhost$`) || HostRegexp(`^${HOST_NAME}\\..+\\.nip\\.io$`) || HostRegexp(`^.+\\.${HOST_NAME}\\..+\\.nip\\.io$`)" + - "traefik.http.routers.${NETWORK_NAME}_ssl.rule=${TRAEFIK_RULE}" - "traefik.http.routers.${NETWORK_NAME}_ssl.tls=true" - "traefik.docker.network=dl_default" environment: diff --git a/utils/path.go b/utils/path.go index 4f17058..f0e8693 100644 --- a/utils/path.go +++ b/utils/path.go @@ -18,7 +18,9 @@ func HomeDir() (string, error) { return os.UserHomeDir() } -// ConfigDir config directory (~/.config/dl) +// ConfigDir config directory (~/.config/) +// The directory name is derived from the executable name, +// allowing multiple installations (e.g. dl and dl-test) to coexist. func ConfigDir() string { conf, err := os.UserConfigDir() if err != nil { @@ -26,7 +28,12 @@ func ConfigDir() string { os.Exit(1) } - return filepath.Join(conf, "dl") + name := "dl" + if exe, err := os.Executable(); err == nil { + name = filepath.Base(exe) + } + + return filepath.Join(conf, name) } // TemplateDir template directory (~/.config/dl/templates)