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)