From e3f5850934e8d4f79e6fbb3b822475b40774e107 Mon Sep 17 00:00:00 2001 From: axadrn Date: Wed, 11 Feb 2026 10:51:08 +0400 Subject: [PATCH 1/4] docs: update deploying and domains documentation for pod-to-pod communication --- content/docs/deploying.md | 32 +++++++++++++++++++++++++++++--- content/docs/domains.md | 25 ++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/content/docs/deploying.md b/content/docs/deploying.md index 74b5010..dbaa756 100644 --- a/content/docs/deploying.md +++ b/content/docs/deploying.md @@ -11,6 +11,7 @@ Deploy your first application step by step. Projects are containers that organize your pods. In the sidebar: + 1. Select the **Projects** panel 2. Press `n` to create a new project @@ -19,6 +20,7 @@ In the sidebar: A pod is a single deployable application. One pod = one container. **Required fields:** + - **Title** - A name for your pod - **Repository URL** - Your Git repository (e.g., `https://github.com/user/repo`) - **Branch** - The branch to deploy (default: `main`) @@ -40,9 +42,10 @@ If your app needs runtime variables: Then restart or redeploy the pod to apply changes. -## 4. Add a Domain +## 4. Add a Domain (Optional) -Your pod needs a domain to be accessible. You have two options: +Add a domain only if this pod should be reachable publicly (from browser/users). +You have two options: - **Auto-generated** - Instant subdomain like `pod-abc123.1.2.3.4.sslip.io` - **Custom** - Your own domain like `myapp.example.com` (requires [DNS setup](/docs/domains)) @@ -58,7 +61,30 @@ Hit "Deploy" and watch the build logs. The process: 3. Start the container 4. Route traffic via Traefik -Once complete, your app is live at the domain URL. +Once complete: + +- with domain: your app is live at the domain URL +- without domain: your app is internal-only (pod-to-pod on the project network) + +## 6. Internal Pod-to-Pod Communication + +Pods can communicate internally without public domains. + +- Internal network aliases are created automatically on deploy/restart +- Current format: `-` + +Use internal calls like: + +```bash +http://api-gateway-9b77346e:8080 +``` + +Important: + +- Hostname and port are separate. You must call `host:port`. +- Browser clients cannot resolve internal Docker DNS names directly. +- Internal DNS name (NetworkAlias) is for server-side/container-side calls only. +- Existing running containers get the new aliases after deploy/restart. ## Managing Your Pod diff --git a/content/docs/domains.md b/content/docs/domains.md index 4d5b414..4c57b22 100644 --- a/content/docs/domains.md +++ b/content/docs/domains.md @@ -13,6 +13,7 @@ order: 6 1. **Point your domain to your server** Add an A record in your DNS provider: + ``` Type: A Name: @ (or subdomain like "deploy") @@ -39,11 +40,31 @@ After setting the server domain: ## Pod Domains -Each pod needs at least one domain to be accessible. +Pod domains are optional. + +- If a pod should be public, add at least one domain. +- If a pod is internal-only (pod-to-pod), you can run it without any domain. Domain changes are not applied to a running container automatically. After adding, editing, or deleting pod domains, run **Deploy** or **Restart** to apply routing changes. +### Internal-Only Pods + +Internal-only pods communicate over the project Docker network using internal DNS aliases. + +- Network alias format: `-` + +Example: + +```bash +http://api-gateway-9b77346e:8080 +``` + +Note: + +- Internal aliases are intended for container/server-side communication. +- Browser clients cannot resolve these internal DNS names directly. + ### Auto-Generated Domains The quickest way to get started. Deeploy generates a subdomain using [sslip.io](https://sslip.io): @@ -59,6 +80,7 @@ Works instantly, no DNS configuration needed. For production apps, use your own domain: 1. **Add DNS record** + ``` Type: A Name: myapp (for myapp.example.com) @@ -76,5 +98,6 @@ For production apps, use your own domain: ### Multiple Domains A single pod can have multiple domains. Useful for: + - `www.example.com` and `example.com` - Different subdomains pointing to the same app From c5a425e455699c6f0d396842db740bb7d86a7db9 Mon Sep 17 00:00:00 2001 From: axadrn Date: Wed, 11 Feb 2026 14:27:35 +0400 Subject: [PATCH 2/4] feat: add naming functions for pod and project network aliases using slug --- go.mod | 2 ++ go.sum | 4 ++++ internal/server/app/app.go | 2 +- internal/server/naming/naming.go | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 internal/server/naming/naming.go diff --git a/go.mod b/go.mod index 8117067..0e46715 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/docker/go-connections v0.6.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 + github.com/gosimple/slug v1.15.0 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/pressly/goose/v3 v3.24.3 @@ -52,6 +53,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/gosimple/unidecode v1.0.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 5f0f372..b10d2f1 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,10 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= diff --git a/internal/server/app/app.go b/internal/server/app/app.go index e050716..c4b2e19 100644 --- a/internal/server/app/app.go +++ b/internal/server/app/app.go @@ -62,7 +62,7 @@ func New(cfg *config.Config) (*App, error) { podEnvVarService := service.NewPodEnvVarService(podEnvVarRepo, encryptor) podDomainService := service.NewPodDomainService(podDomainRepo) gitTokenService := service.NewGitTokenService(gitTokenRepo, encryptor) - deployService := service.NewDeployService(podRepo, podDomainRepo, podEnvVarService, gitTokenService, dockerService) + deployService := service.NewDeployService(podRepo, projectRepo, podDomainRepo, podEnvVarService, gitTokenService, dockerService) traefikService := service.NewTraefikService(serverSettingsRepo, cfg.TraefikConfigDir, cfg.IsDevelopment()) return &App{ diff --git a/internal/server/naming/naming.go b/internal/server/naming/naming.go new file mode 100644 index 0000000..f2a075d --- /dev/null +++ b/internal/server/naming/naming.go @@ -0,0 +1,17 @@ +package naming + +import ( + "github.com/gosimple/slug" +) + +func NetworkAliasForPod(podID, podTitle string) string { + return slug.Make(podTitle) + "-" + podID[:8] +} + +func ProjectNetworkName(projectTitle, projectID string) string { + return "deeploy-" + slug.Make(projectTitle) + "-" + projectID[:8] +} + +func ContainerNameForPod(projectTitle, podTitle, podID string) string { + return "deeploy-" + slug.Make(projectTitle) + "-" + slug.Make(podTitle) + "-" + podID[:8] +} From 5456323901bf364ceaa602ea2d1ddbdd14ac9418 Mon Sep 17 00:00:00 2001 From: axadrn Date: Wed, 11 Feb 2026 14:32:20 +0400 Subject: [PATCH 3/4] feat: display network alias in pod overview panel --- internal/tui/ui/panel/pod.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/tui/ui/panel/pod.go b/internal/tui/ui/panel/pod.go index 70d32f5..a210839 100644 --- a/internal/tui/ui/panel/pod.go +++ b/internal/tui/ui/panel/pod.go @@ -449,9 +449,14 @@ func (p *PodPanel) renderOverview() string { domainCount := len(p.domainItems) varCount := len(p.envVars) + internalHost := "-" + if p.pod.NetworkAlias != "" { + internalHost = p.pod.NetworkAlias + } - lines = append(lines, " "+styles.MutedStyle().Render("Domains ")+fmt.Sprintf("%d configured", domainCount)) - lines = append(lines, " "+styles.MutedStyle().Render("Env Vars ")+fmt.Sprintf("%d configured", varCount)) + lines = append(lines, " "+styles.MutedStyle().Render("Domains ")+fmt.Sprintf("%d configured", domainCount)) + lines = append(lines, " "+styles.MutedStyle().Render("Env Vars ")+fmt.Sprintf("%d configured", varCount)) + lines = append(lines, " "+styles.MutedStyle().Render("Network Alias ")+internalHost) return strings.Join(lines, "\n") } From a64c843c5535d0281fc9f13d64adbb66b9dd4e4f Mon Sep 17 00:00:00 2001 From: axadrn Date: Wed, 11 Feb 2026 14:38:04 +0400 Subject: [PATCH 4/4] feat: enhance pod-to-pod communication with network alias and project network support --- internal/server/docker/docker.go | 122 +++++++++++++++++++----------- internal/server/service/deploy.go | 87 ++++++++++++--------- internal/server/service/pod.go | 12 +++ internal/shared/model/pod.go | 1 + 4 files changed, 145 insertions(+), 77 deletions(-) diff --git a/internal/server/docker/docker.go b/internal/server/docker/docker.go index 570094b..3fb413d 100644 --- a/internal/server/docker/docker.go +++ b/internal/server/docker/docker.go @@ -194,10 +194,6 @@ func (d *DockerService) RunContainer(ctx context.Context, opts RunContainerOptio //───────────────────────────────────────────────────────────────────────── labels := map[string]string{ - // Enable Traefik for this container - // Without this, Traefik ignores the container completely - "traefik.enable": "true", - // Deeploy metadata for container identification "deeploy.pod.id": opts.PodID, } @@ -209,44 +205,49 @@ func (d *DockerService) RunContainer(ctx context.Context, opts RunContainerOptio if d.isDevelopment { entrypoint = "web" } + port := 8080 + if len(opts.Domains) > 0 { + port = opts.Domains[0].Port + } + + if opts.EnablePublicAccess { + // Enable Traefik only for public pods. + labels["traefik.enable"] = "true" - // Create a router for each domain - // Each domain gets its own router but shares the same service (load balancer) - for i, domain := range opts.Domains { - routerName := fmt.Sprintf("%s-%d", opts.PodID, i) + // Create a router for each domain + // Each domain gets its own router but shares the same service (load balancer) + for i, domain := range opts.Domains { + routerName := fmt.Sprintf("%s-%d", opts.PodID, i) - // Routing rule: Which domain goes to this container? - // Host(`example.com`) matches requests with that exact Host header - labels["traefik.http.routers."+routerName+".rule"] = fmt.Sprintf("Host(`%s`)", domain.Domain) + // Routing rule: Which domain goes to this container? + // Host(`example.com`) matches requests with that exact Host header + labels["traefik.http.routers."+routerName+".rule"] = fmt.Sprintf("Host(`%s`)", domain.Domain) - // Service: Where to forward the traffic - // @docker suffix is required because Traefik auto-appends it to Docker services - labels["traefik.http.routers."+routerName+".service"] = opts.PodID + "@docker" + // Service: Where to forward the traffic + // @docker suffix is required because Traefik auto-appends it to Docker services + labels["traefik.http.routers."+routerName+".service"] = opts.PodID + "@docker" - // Entrypoint: Which port to listen on (web=80, websecure=443) - labels["traefik.http.routers."+routerName+".entrypoints"] = entrypoint + // Entrypoint: Which port to listen on (web=80, websecure=443) + labels["traefik.http.routers."+routerName+".entrypoints"] = entrypoint - // SSL/TLS: Only in production (not development) - // certresolver=letsencrypt tells Traefik to automatically get a certificate - // from Let's Encrypt using the HTTP challenge - if !d.isDevelopment { - labels["traefik.http.routers."+routerName+".tls.certresolver"] = "letsencrypt" + // SSL/TLS: Only in production (not development) + // certresolver=letsencrypt tells Traefik to automatically get a certificate + // from Let's Encrypt using the HTTP challenge + if !d.isDevelopment { + labels["traefik.http.routers."+routerName+".tls.certresolver"] = "letsencrypt" + } } - } - // Service configuration: One service for all routers - // All domains for this pod route to the same container/port - port := 8080 - if len(opts.Domains) > 0 { - port = opts.Domains[0].Port - } - labels["traefik.http.services."+opts.PodID+".loadbalancer.server.port"] = fmt.Sprintf("%d", port) + // Service configuration: One service for all routers + // All domains for this pod route to the same container/port + labels["traefik.http.services."+opts.PodID+".loadbalancer.server.port"] = fmt.Sprintf("%d", port) - // Health checks: Traefik pings each container every 2 seconds - // Only containers that respond get traffic. This ensures zero-downtime - // during redeploys - new container only gets traffic once it's ready. - labels["traefik.http.services."+opts.PodID+".loadbalancer.healthcheck.path"] = "/" - labels["traefik.http.services."+opts.PodID+".loadbalancer.healthcheck.interval"] = "2s" + // Health checks: Traefik pings each container every 2 seconds + // Only containers that respond get traffic. This ensures zero-downtime + // during redeploys - new container only gets traffic once it's ready. + labels["traefik.http.services."+opts.PodID+".loadbalancer.healthcheck.path"] = "/" + labels["traefik.http.services."+opts.PodID+".loadbalancer.healthcheck.interval"] = "2s" + } // Container config config := &container.Config{ @@ -264,12 +265,25 @@ func (d *DockerService) RunContainer(ctx context.Context, opts RunContainerOptio RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, } - // Network config - join the deeploy network so Traefik can reach this container - networkConfig := &network.NetworkingConfig{ - EndpointsConfig: map[string]*network.EndpointSettings{ - NetworkName: {}, + if opts.ProjectNetwork == "" { + return "", fmt.Errorf("project network is required") + } + if err := d.EnsureNetwork(ctx, opts.ProjectNetwork); err != nil { + return "", fmt.Errorf("failed to ensure project network: %w", err) + } + + endpoints := map[string]*network.EndpointSettings{ + opts.ProjectNetwork: { + Aliases: opts.NetworkAliases, }, } + if opts.EnablePublicAccess { + endpoints[NetworkName] = &network.EndpointSettings{} + } + + networkConfig := &network.NetworkingConfig{ + EndpointsConfig: endpoints, + } // Create container resp, err := d.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, opts.ContainerName) @@ -286,6 +300,25 @@ func (d *DockerService) RunContainer(ctx context.Context, opts RunContainerOptio return resp.ID, nil } +// EnsureNetwork creates a Docker network if it does not already exist. +func (d *DockerService) EnsureNetwork(ctx context.Context, name string) error { + _, err := d.client.NetworkInspect(ctx, name, network.InspectOptions{}) + if err == nil { + return nil + } + + _, err = d.client.NetworkCreate(ctx, name, network.CreateOptions{}) + if err != nil { + // Another deploy may have created the network in the meantime. + if _, inspectErr := d.client.NetworkInspect(ctx, name, network.InspectOptions{}); inspectErr == nil { + return nil + } + return err + } + + return nil +} + // StopContainer stops a running container. func (d *DockerService) StopContainer(ctx context.Context, containerID string) error { timeout := 30 @@ -409,11 +442,14 @@ type DomainConfig struct { // RunContainerOptions holds options for running a container. type RunContainerOptions struct { - ImageName string - ContainerName string - PodID string - Domains []DomainConfig - EnvVars map[string]string + ImageName string + ContainerName string + PodID string + Domains []DomainConfig + EnvVars map[string]string + ProjectNetwork string + EnablePublicAccess bool + NetworkAliases []string } func mapToEnvSlice(m map[string]string) []string { diff --git a/internal/server/service/deploy.go b/internal/server/service/deploy.go index 139fbcd..abe8a91 100644 --- a/internal/server/service/deploy.go +++ b/internal/server/service/deploy.go @@ -7,11 +7,13 @@ import ( "time" "github.com/deeploy-sh/deeploy/internal/server/docker" + "github.com/deeploy-sh/deeploy/internal/server/naming" "github.com/deeploy-sh/deeploy/internal/server/repo" ) type DeployService struct { podRepo repo.PodRepoInterface + projectRepo repo.ProjectRepoInterface podDomainRepo repo.PodDomainRepoInterface podEnvVarService PodEnvVarServiceInterface gitTokenService GitTokenServiceInterface @@ -28,6 +30,7 @@ type DeployService struct { func NewDeployService( podRepo *repo.PodRepo, + projectRepo *repo.ProjectRepo, podDomainRepo *repo.PodDomainRepo, podEnvVarService PodEnvVarServiceInterface, gitTokenService *GitTokenService, @@ -35,6 +38,7 @@ func NewDeployService( ) *DeployService { return &DeployService{ podRepo: podRepo, + projectRepo: projectRepo, podDomainRepo: podDomainRepo, podEnvVarService: podEnvVarService, gitTokenService: gitTokenService, @@ -97,14 +101,20 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { return fmt.Errorf("pod has no repo URL configured") } - // 2. Check domain exists BEFORE starting build (fail fast) + // 2. Load domains (optional for internal-only pods) domains, _ := s.podDomainRepo.DomainsByPod(podID) - if len(domains) == 0 { - s.appendBuildLog(podID, "ERROR: no domain configured - add a domain first") - return fmt.Errorf("no domain configured for pod") + + // 3. Resolve project naming/network + project, err := s.projectRepo.Project(pod.ProjectID) + if err != nil { + s.appendBuildLog(podID, fmt.Sprintf("ERROR: project not found: %v", err)) + return fmt.Errorf("project %s: %w", pod.ProjectID, err) } + projectNetwork := naming.ProjectNetworkName(project.Title, project.ID) + networkAliases := []string{naming.NetworkAliasForPod(podID, pod.Title)} + containerName := naming.ContainerNameForPod(project.Title, pod.Title, podID) - // 3. Update status to building + // 4. Update status to building pod.Status = "building" err = s.podRepo.Update(*pod) if err != nil { @@ -114,7 +124,7 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { s.appendBuildLog(podID, "=== Starting deployment ===") s.appendBuildLog(podID, fmt.Sprintf("Repo: %s @ %s", *pod.RepoURL, pod.Branch)) - // 4. Get git token if configured + // 5. Get git token if configured var gitToken string if pod.GitTokenID != nil { token, err := s.gitTokenService.GitToken(*pod.GitTokenID) @@ -126,7 +136,7 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { s.appendBuildLog(podID, "Using configured git token for private repo") } - // 5. Clone repo + // 6. Clone repo s.appendBuildLog(podID, "Cloning repository...") clonePath, err := s.docker.CloneRepo(*pod.RepoURL, pod.Branch, gitToken) if err != nil { @@ -138,7 +148,7 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { defer s.docker.Cleanup(clonePath) s.appendBuildLog(podID, "Repository cloned successfully") - // 6. Build image + // 7. Build image s.appendBuildLog(podID, "") s.appendBuildLog(podID, "=== Building Docker image ===") imageName := fmt.Sprintf("deeploy-%s:latest", podID) @@ -156,7 +166,7 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { s.appendBuildLog(podID, "") s.appendBuildLog(podID, "=== Docker image built successfully ===") - // 7. Prepare domains and env vars + // 8. Prepare domains and env vars var domainConfigs []docker.DomainConfig for _, d := range domains { domainConfigs = append(domainConfigs, docker.DomainConfig{ @@ -182,13 +192,12 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { s.appendBuildLog(podID, fmt.Sprintf("Loaded %d environment variables", len(envMap))) } - // 8. Rename existing container to make room for new one (zero-downtime) + // 9. Rename existing container to make room for new one (zero-downtime) oldContainerID := "" - containerName := fmt.Sprintf("deeploy-%s", podID) if pod.ContainerID != nil && *pod.ContainerID != "" { oldContainerID = *pod.ContainerID s.appendBuildLog(podID, "Preparing zero-downtime deployment...") - err := s.docker.RenameContainer(ctx, oldContainerID, fmt.Sprintf("deeploy-%s-old", podID)) + err := s.docker.RenameContainer(ctx, oldContainerID, containerName+"-old") if err != nil { // Container ID is stale - cleanup DB and orphaned container s.appendBuildLog(podID, "Cleaning up stale container...") @@ -200,25 +209,28 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { } } - // 9. Clean up any orphaned container with target name (from previous failed deploys) + // 10. Clean up any orphaned container with target name (from previous failed deploys) // This handles edge case where deploy failed after container creation but before DB update s.docker.StopContainer(ctx, containerName) s.docker.RemoveContainer(ctx, containerName) - // 10. Run new container (old still running for zero-downtime) + // 11. Run new container (old still running for zero-downtime) s.appendBuildLog(podID, "") s.appendBuildLog(podID, "=== Starting new container ===") containerID, err := s.docker.RunContainer(ctx, docker.RunContainerOptions{ - ImageName: imageName, - ContainerName: fmt.Sprintf("deeploy-%s", podID), - PodID: podID, - Domains: domainConfigs, - EnvVars: envMap, + ImageName: imageName, + ContainerName: containerName, + PodID: podID, + Domains: domainConfigs, + EnvVars: envMap, + ProjectNetwork: projectNetwork, + EnablePublicAccess: len(domainConfigs) > 0, + NetworkAliases: networkAliases, }) if err != nil { // Rollback: rename old container back if oldContainerID != "" { - s.docker.RenameContainer(ctx, oldContainerID, fmt.Sprintf("deeploy-%s", podID)) + s.docker.RenameContainer(ctx, oldContainerID, containerName) } pod.Status = "failed" s.podRepo.Update(*pod) @@ -226,7 +238,7 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { return fmt.Errorf("failed to run container: %w", err) } - // 11. Stop old container (Traefik handles health checks and routing) + // 12. Stop old container (Traefik handles health checks and routing) // Wait 5s before stopping old container for zero-downtime deployment: // - Traefik health check interval is 2s // - Gives new container time to be detected and marked healthy @@ -239,7 +251,7 @@ func (s *DeployService) Deploy(ctx context.Context, podID string) error { s.docker.RemoveContainer(ctx, oldContainerID) } - // 12. Update pod with container ID and status + // 13. Update pod with container ID and status pod.ContainerID = &containerID pod.Status = "running" err = s.podRepo.Update(*pod) @@ -293,6 +305,7 @@ func (s *DeployService) Restart(ctx context.Context, podID string) error { return fmt.Errorf("pod has no running container") } oldContainerID := *pod.ContainerID + containerName := fmt.Sprintf("deeploy-%s", podID) // 2. Get image from current container imageName, err := s.docker.GetContainerImage(ctx, oldContainerID) @@ -303,7 +316,6 @@ func (s *DeployService) Restart(ctx context.Context, podID string) error { s.podRepo.Update(*pod) // Cleanup any orphaned container with this name - containerName := fmt.Sprintf("deeploy-%s", podID) s.docker.StopContainer(ctx, containerName) s.docker.RemoveContainer(ctx, containerName) @@ -312,9 +324,6 @@ func (s *DeployService) Restart(ctx context.Context, podID string) error { // 3. Get domains domains, _ := s.podDomainRepo.DomainsByPod(podID) - if len(domains) == 0 { - return fmt.Errorf("no domain configured for pod") - } var domainConfigs []docker.DomainConfig for _, d := range domains { @@ -324,6 +333,14 @@ func (s *DeployService) Restart(ctx context.Context, podID string) error { }) } + project, err := s.projectRepo.Project(pod.ProjectID) + if err != nil { + return fmt.Errorf("project %s: %w", pod.ProjectID, err) + } + projectNetwork := naming.ProjectNetworkName(project.Title, project.ID) + networkAliases := []string{naming.NetworkAliasForPod(podID, pod.Title)} + containerName = naming.ContainerNameForPod(project.Title, pod.Title, podID) + // 4. Get env vars (decrypted via service) envVars, err := s.podEnvVarService.EnvVarsByPod(podID) if err != nil { @@ -336,19 +353,22 @@ func (s *DeployService) Restart(ctx context.Context, podID string) error { } // 5. Rename old container (zero-downtime: keep running) - s.docker.RenameContainer(ctx, oldContainerID, fmt.Sprintf("deeploy-%s-old", podID)) + s.docker.RenameContainer(ctx, oldContainerID, containerName+"-old") // 6. Start new container containerID, err := s.docker.RunContainer(ctx, docker.RunContainerOptions{ - ImageName: imageName, - ContainerName: fmt.Sprintf("deeploy-%s", podID), - PodID: podID, - Domains: domainConfigs, - EnvVars: envMap, + ImageName: imageName, + ContainerName: containerName, + PodID: podID, + Domains: domainConfigs, + EnvVars: envMap, + ProjectNetwork: projectNetwork, + EnablePublicAccess: len(domainConfigs) > 0, + NetworkAliases: networkAliases, }) if err != nil { // Rollback: rename old container back - s.docker.RenameContainer(ctx, oldContainerID, fmt.Sprintf("deeploy-%s", podID)) + s.docker.RenameContainer(ctx, oldContainerID, containerName) return fmt.Errorf("failed to run container: %w", err) } @@ -385,4 +405,3 @@ func (s *DeployService) GetLogs(ctx context.Context, podID string, lines int) ([ logs, err := s.docker.GetLogsLines(ctx, *pod.ContainerID, lines) return logs, pod.Status, err } - diff --git a/internal/server/service/pod.go b/internal/server/service/pod.go index ddc8280..d49a71f 100644 --- a/internal/server/service/pod.go +++ b/internal/server/service/pod.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/deeploy-sh/deeploy/internal/server/docker" + "github.com/deeploy-sh/deeploy/internal/server/naming" "github.com/deeploy-sh/deeploy/internal/server/repo" "github.com/deeploy-sh/deeploy/internal/shared/model" ) @@ -42,6 +43,10 @@ func (s *PodService) enrichWithContainerState(pod *model.Pod) { pod.ContainerState = state } +func (s *PodService) enrichWithInternalHost(pod *model.Pod) { + pod.NetworkAlias = naming.NetworkAliasForPod(pod.ID, pod.Title) +} + func (s *PodService) Create(pod *model.Pod) (*model.Pod, error) { err := s.repo.Create(pod) if err != nil { @@ -56,6 +61,7 @@ func (s *PodService) Pod(id string) (*model.Pod, error) { return nil, err } s.enrichWithContainerState(pod) + s.enrichWithInternalHost(pod) return pod, nil } @@ -64,6 +70,9 @@ func (s *PodService) PodsByProject(id string) ([]model.Pod, error) { if err != nil { return nil, err } + for i := range pods { + s.enrichWithInternalHost(&pods[i]) + } return pods, nil } @@ -72,6 +81,9 @@ func (s *PodService) PodsByUser(id string) ([]model.Pod, error) { if err != nil { return nil, err } + for i := range pods { + s.enrichWithInternalHost(&pods[i]) + } return pods, nil } diff --git a/internal/shared/model/pod.go b/internal/shared/model/pod.go index 60286f0..f6d2c2e 100644 --- a/internal/shared/model/pod.go +++ b/internal/shared/model/pod.go @@ -19,6 +19,7 @@ type Pod struct { // ContainerState is the live Docker container state (running, exited, etc.) // Not stored in DB - fetched on demand from Docker ContainerState string `json:"container_state" db:"-"` + NetworkAlias string `json:"network_alias" db:"-"` } type PodEnvVar struct {