Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b5d2147
Add Docker Compose scanner for multi-container machine support
rubys Jun 25, 2025
49d2cfe
Fix trailing whitespace in DOCKER_COMPOSE_SUPPORT.md
rubys Jun 25, 2025
df8b640
Add service discovery for multi-container Docker Compose deployments
rubys Jun 25, 2025
6a154e4
Update Docker Compose documentation with service discovery details
rubys Jun 25, 2025
a88d4d2
Fix machine configuration JSON structure for multi-container deployments
rubys Jun 26, 2025
e372820
Use local_path instead of raw_value for better readability and fix /e…
rubys Jun 26, 2025
1a2dcda
Filter out dependencies on excluded database services
rubys Jun 26, 2025
89f29af
Fix gofmt formatting issues
rubys Jun 26, 2025
454f633
Handle Docker Compose build sections: single build service only
rubys Jun 26, 2025
6e332dd
Extract database credentials as secrets from Docker Compose
rubys Jun 26, 2025
bbcfa33
Add secrets field to containers in machine configuration
rubys Jun 26, 2025
9fc1dd3
Support Docker Compose secrets section and ensure container secret ac…
rubys Jun 26, 2025
498f70a
Fix machine config secrets format - use objects with env_var and secr…
rubys Jun 27, 2025
ba019f8
Fix entrypoint permissions: use mode integer instead of permissions s…
rubys Jun 27, 2025
ed28667
Fix secrets format: omit name field when same as env_var
rubys Jun 27, 2025
e05c2b4
Fix container file updates during deploy
rubys Jun 27, 2025
5e362d1
Complete Docker Compose scanner implementation with multi-container s…
rubys Jun 28, 2025
c8c3d60
Fix database secret handling for managed services
rubys Jun 28, 2025
6d7f0bb
Fix DATABASE_URL not appearing in container secrets list
rubys Jun 28, 2025
583dd78
Fix unnecessary [build] section in fly.toml for external images
rubys Jun 28, 2025
f475bb4
Skip creating second machine for multi-container deployments
rubys Jun 28, 2025
e1cab51
Fix Docker Compose scanner tests for current implementation
rubys Jun 28, 2025
f3b6222
Remove documentation files
rubys Jun 28, 2025
97ff666
Remove docs/docker-compose-scanner.md
rubys Jun 28, 2025
78bfa8d
Fix Dockerfile path duplication in build configuration
rubys Jun 28, 2025
92e6dfb
Support multiple services with identical build definitions in Docker …
rubys Jun 29, 2025
47baf23
Fix code formatting issues found by golangci-lint
rubys Jun 29, 2025
7f5f982
Clear multi-container config when only one service remains after data…
rubys Jun 29, 2025
93723c5
Add processes-based deployment for identical build services
rubys Jun 29, 2025
cb6dbaa
Preserve original service order from docker-compose.yml
rubys Jun 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions internal/appconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ type Config struct {
// the one where the image is replaced upon deploy. If no container is identified,
// this will default to the "app" container, and if that is not present, the first
// container in the list will be used.
MachineConfig string `toml:"machine_config,omitempty" json:"machine_config,omitempty"`
Container string `toml:"container,omitempty" json:"container,omitempty"`
MachineConfig string `toml:"machine_config,omitempty" json:"machine_config,omitempty"`
Container string `toml:"container,omitempty" json:"container,omitempty"`
BuildContainers []string `toml:"build_containers,omitempty" json:"build_containers,omitempty"`

MachineChecks []*ServiceMachineCheck `toml:"machine_checks,omitempty" json:"machine_checks,omitempty"`

Expand Down
23 changes: 18 additions & 5 deletions internal/command/deploy/machines_deploymachinesapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ func (md *machineDeployment) deployCreateMachinesForGroups(ctx context.Context,
}
}

// Check if this group has multi-container configuration
var hasContainers bool
if mConfig, err := groupConfig.ToMachineConfig(name, nil); err == nil {
hasContainers = len(mConfig.Containers) > 0
}

// Create spare machines that increases availability unless --ha=false was used
if !md.increasedAvailability {
continue
Expand All @@ -368,11 +374,11 @@ func (md *machineDeployment) deployCreateMachinesForGroups(ctx context.Context,
// TODO(Ali): This overwrites the main machine's status log with the standby machine's status log.

// We strive to provide a HA setup according to:
// - Create only 1 machine if the group has mounts
// - Create only 1 machine if the group has mounts or containers
// - Create 2 machines for groups with services
// - Create 1 always-on and 1 standby machine for groups without services
switch {
case len(groupConfig.Mounts) > 0:
case len(groupConfig.Mounts) > 0 || hasContainers:
continue
case len(services) > 0:
fmt.Fprintf(md.io.Out, "Creating a second machine to increase service availability\n")
Expand Down Expand Up @@ -457,7 +463,7 @@ func (md *machineDeployment) deployMachinesApp(ctx context.Context) error {

var machineUpdateEntries []*machineUpdateEntry
for _, lm := range md.machineSet.GetMachines() {
li, err := md.launchInputForUpdate(lm.Machine())
li, err := md.launchInputForUpdate(ctx, lm.Machine())
if err != nil {
return fmt.Errorf("failed to update machine configuration for %s: %w", lm.FormattedMachineId(), err)
}
Expand Down Expand Up @@ -1039,7 +1045,7 @@ func (md *machineDeployment) spawnMachineInGroup(ctx context.Context, groupName
opt(&options)
}

launchInput, err := md.launchInputForLaunch(groupName, options.guest, standbyFor)
launchInput, err := md.launchInputForLaunch(ctx, groupName, options.guest, standbyFor)
if err != nil {
return nil, fmt.Errorf("error creating machine configuration: %w", err)
}
Expand Down Expand Up @@ -1166,10 +1172,17 @@ func (md *machineDeployment) warnAboutProcessGroupChanges(diff ProcessGroupsDiff
for name := range diff.groupsNeedingMachines {
var description string
groupConfig, err := md.appConfig.Flatten(name)

// Check if this group has multi-container configuration
var hasContainers bool
if mConfig, err := groupConfig.ToMachineConfig(name, nil); err == nil {
hasContainers = len(mConfig.Containers) > 0
}

switch {
case err != nil:
continue
case !md.increasedAvailability || len(groupConfig.Mounts) > 0:
case !md.increasedAvailability || len(groupConfig.Mounts) > 0 || hasContainers:
description = fmt.Sprintf("1 \"%s\" machine", name)
case len(groupConfig.AllServices()) > 0:
description = fmt.Sprintf("2 \"%s\" machines", name)
Expand Down
47 changes: 45 additions & 2 deletions internal/command/deploy/machines_launchinput.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package deploy

import (
"context"
"fmt"
"strconv"
"strings"

"github.com/samber/lo"
fly "github.com/superfly/fly-go"
"github.com/superfly/flyctl/internal/buildinfo"
"github.com/superfly/flyctl/internal/config"
"github.com/superfly/flyctl/internal/machine"
"github.com/superfly/flyctl/terminal"
)
Expand All @@ -24,7 +26,7 @@ func (md *machineDeployment) launchInputForRestart(origMachineRaw *fly.Machine)
}
}

func (md *machineDeployment) launchInputForLaunch(processGroup string, guest *fly.MachineGuest, standbyFor []string) (*fly.LaunchMachineInput, error) {
func (md *machineDeployment) launchInputForLaunch(ctx context.Context, processGroup string, guest *fly.MachineGuest, standbyFor []string) (*fly.LaunchMachineInput, error) {
mConfig, err := md.appConfig.ToMachineConfig(processGroup, nil)
if err != nil {
return nil, err
Expand Down Expand Up @@ -64,14 +66,19 @@ func (md *machineDeployment) launchInputForLaunch(processGroup string, guest *fl
return nil, err
}

// Extract CMD from image for containers that need it
if err = md.updateContainerCmdFromImage(ctx, mConfig); err != nil {
terminal.Debugf("Warning: failed to extract CMD from images: %v", err)
}

return &fly.LaunchMachineInput{
Region: region,
Config: mConfig,
SkipLaunch: skipLaunch(nil, mConfig),
}, nil
}

func (md *machineDeployment) launchInputForUpdate(origMachineRaw *fly.Machine) (*fly.LaunchMachineInput, error) {
func (md *machineDeployment) launchInputForUpdate(ctx context.Context, origMachineRaw *fly.Machine) (*fly.LaunchMachineInput, error) {
mID := origMachineRaw.ID
machineShouldBeReplaced := dedicatedHostIdMismatch(origMachineRaw, md.appConfig)

Expand All @@ -96,6 +103,29 @@ func (md *machineDeployment) launchInputForUpdate(origMachineRaw *fly.Machine) (
return nil, err
}

// Extract CMD from image for containers that need it
if err = md.updateContainerCmdFromImage(ctx, mConfig); err != nil {
terminal.Debugf("Warning: failed to extract CMD from images: %v", err)
}

// Ensure container files from machine_config are re-processed
// This is necessary because machine_config files may have been updated locally
if md.appConfig.MachineConfig != "" && len(mConfig.Containers) > 0 {
// Re-parse the machine config to get fresh file content
tempConfig := &fly.MachineConfig{}
if err := config.ParseConfig(tempConfig, md.appConfig.MachineConfig); err == nil {
// Apply container files from the re-parsed config
for _, container := range mConfig.Containers {
for _, tempContainer := range tempConfig.Containers {
if container.Name == tempContainer.Name && len(tempContainer.Files) > 0 {
// Update container files with fresh content
container.Files = tempContainer.Files
}
}
}
}
}

// Mounts needs special treatment:
// * Volumes attached to existings machines can't be swapped by other volumes
// * The only allowed in-place operation is to update its destination mount path
Expand Down Expand Up @@ -284,7 +314,20 @@ func (md *machineDeployment) updateContainerImage(mConfig *fly.MachineConfig) er
}

container.Image = mConfig.Image

// Note: CMD extraction for multi-container configurations happens after this loop
}

return nil
}

// updateContainerCmdFromImage extracts CMD from images for containers that need it
func (md *machineDeployment) updateContainerCmdFromImage(ctx context.Context, mConfig *fly.MachineConfig) error {
if len(mConfig.Containers) == 0 {
return nil
}

// For now, skip CMD extraction due to registry authentication complexity
// Let containers use their image defaults when UseImageDefaults is set
return nil
}
43 changes: 22 additions & 21 deletions internal/command/deploy/machines_launchinput_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package deploy

import (
"context"
"os"
"testing"

Expand Down Expand Up @@ -68,7 +69,7 @@ func testLaunchInputForBasic(t *testing.T) {
},
},
}
li, err := md.launchInputForLaunch("", nil, nil)
li, err := md.launchInputForLaunch(context.Background(), "", nil, nil)
require.NoError(t, err)
assert.Equal(t, want, li)

Expand Down Expand Up @@ -105,7 +106,7 @@ func testLaunchInputForBasic(t *testing.T) {
}
want.Config.Image = "super/globe"
want.Config.Env["NOT_SET_ON_RESTART_ONLY"] = "true"
li, err = md.launchInputForUpdate(origMachineRaw)
li, err = md.launchInputForUpdate(context.Background(), origMachineRaw)
require.NoError(t, err)
assert.Equal(t, want, li)
}
Expand All @@ -121,7 +122,7 @@ func testLaunchInputForUpdateHostStatusUnreachable(t *testing.T) {
})
assert.NoError(t, err)

li, err := md.launchInputForUpdate(&fly.Machine{
li, err := md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Region: "ord",
Config: &fly.MachineConfig{
Expand All @@ -135,7 +136,7 @@ func testLaunchInputForUpdateHostStatusUnreachable(t *testing.T) {

// Updating an unreachable machine with a volume attached must fail until we can move the volume to another host
md.appConfig.Mounts = []appconfig.Mount{{Source: "data", Destination: "/data"}}
_, err = md.launchInputForUpdate(&fly.Machine{
_, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
IncompleteConfig: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/data", Name: "data"}},
Expand All @@ -150,7 +151,7 @@ func testLaunchInputForUpdateHostStatusUnreachable(t *testing.T) {
{ID: "vol_10001", Name: "data"},
},
}
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
IncompleteConfig: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/data", Name: "replace-me-because-i-m-different-fly-toml"}},
Expand All @@ -177,13 +178,13 @@ func testLaunchInputForOnMounts(t *testing.T) {
}

// New machine must get a volume attached
li, err := md.launchInputForLaunch("", nil, nil)
li, err := md.launchInputForLaunch(context.Background(), "", nil, nil)
require.NoError(t, err)
require.NotEmpty(t, li.Config.Mounts)
assert.Equal(t, fly.MachineMount{Volume: "vol_10001", Path: "/data", Name: "data"}, li.Config.Mounts[0])

// The machine already has a volume that matches fly.toml [mounts] section
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/data", Name: "data"}},
Expand All @@ -197,7 +198,7 @@ func testLaunchInputForOnMounts(t *testing.T) {
assert.Equal(t, fly.MachineMount{Volume: "vol_attached", Path: "/data", Name: "data"}, li.Config.Mounts[0])

// Update a machine with volume attached on a different path
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/update-me", Name: "data"}},
Expand All @@ -211,7 +212,7 @@ func testLaunchInputForOnMounts(t *testing.T) {
assert.Equal(t, fly.MachineMount{Volume: "vol_attached", Path: "/data", Name: "data"}, li.Config.Mounts[0])

// Updating a machine with an existing unnamed mount must keep the original mount as much as possible
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/keep-me"}},
Expand All @@ -225,7 +226,7 @@ func testLaunchInputForOnMounts(t *testing.T) {
assert.Equal(t, fly.MachineMount{Volume: "vol_attached", Path: "/keep-me"}, li.Config.Mounts[0])

// Updating a machine whose volume name doesn't match fly.toml's mount section must replace the machine altogether
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/replace-me", Name: "replace-me"}},
Expand All @@ -240,7 +241,7 @@ func testLaunchInputForOnMounts(t *testing.T) {

// Updating a machine with an attached volume should trigger a replacement if fly.toml doesn't define one.
md.appConfig.Mounts = nil
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/replace-me", Name: "replace-me"}},
Expand Down Expand Up @@ -275,7 +276,7 @@ func testLaunchInputForOnMountsAndAutoResize(t *testing.T) {
}

// New machine must get a volume attached
li, err := md.launchInputForLaunch("", nil, nil)
li, err := md.launchInputForLaunch(context.Background(), "", nil, nil)
require.NoError(t, err)
require.NotEmpty(t, li.Config.Mounts)
assert.Equal(t, fly.MachineMount{
Expand All @@ -289,7 +290,7 @@ func testLaunchInputForOnMountsAndAutoResize(t *testing.T) {

// The machine already has a volume that matches fly.toml [mounts] section
// confirm new extend configs will be added
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{
Expand All @@ -316,7 +317,7 @@ func testLaunchInputForOnMountsAndAutoResize(t *testing.T) {
}, li.Config.Mounts[0])

// Update a machine with volume attached on a different path
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{
Expand All @@ -343,7 +344,7 @@ func testLaunchInputForOnMountsAndAutoResize(t *testing.T) {
}, li.Config.Mounts[0])

// Updating a machine with an existing unnamed mount must keep the original mount as much as possible
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{
Expand All @@ -368,7 +369,7 @@ func testLaunchInputForOnMountsAndAutoResize(t *testing.T) {
}, li.Config.Mounts[0])

// Updating a machine whose volume name doesn't match fly.toml's mount section must replace the machine altogether
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{Volume: "vol_attached", Path: "/replace-me", Name: "replace-me"}},
Expand All @@ -390,7 +391,7 @@ func testLaunchInputForOnMountsAndAutoResize(t *testing.T) {

// Updating a machine with an attached volume should trigger a replacement if fly.toml doesn't define one.
md.appConfig.Mounts = nil
li, err = md.launchInputForUpdate(&fly.Machine{
li, err = md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Config: &fly.MachineConfig{
Mounts: []fly.MachineMount{{
Expand Down Expand Up @@ -438,7 +439,7 @@ func testLaunchInputForUpdateKeepUnmanagedFields(t *testing.T) {
},
HostStatus: fly.HostStatusOk,
}
li, err := md.launchInputForUpdate(origMachineRaw)
li, err := md.launchInputForUpdate(context.Background(), origMachineRaw)
require.NoError(t, err)
assert.Equal(t, "ab1234567890", li.ID)
assert.Equal(t, "ord", li.Region)
Expand Down Expand Up @@ -470,7 +471,7 @@ func testLaunchInputForUpdateClearStandbysWithServices(t *testing.T) {
})
require.NoError(t, err)

li, err := md.launchInputForUpdate(&fly.Machine{
li, err := md.launchInputForUpdate(context.Background(), &fly.Machine{
ID: "ab1234567890",
Region: "scl",
Config: &fly.MachineConfig{
Expand Down Expand Up @@ -522,7 +523,7 @@ func testLaunchInputForLaunchFiles(t *testing.T) {
},
},
}
li, err := md.launchInputForLaunch("", nil, nil)
li, err := md.launchInputForLaunch(context.Background(), "", nil, nil)
require.NoError(t, err)
assert.Equal(t, want, li)
}
Expand All @@ -544,7 +545,7 @@ func testLaunchInputForUpdateFiles(t *testing.T) {
})
require.NoError(t, err)

li, err := md.launchInputForUpdate(&fly.Machine{
li, err := md.launchInputForUpdate(context.Background(), &fly.Machine{
HostStatus: fly.HostStatusOk,
Config: &fly.MachineConfig{
Files: []*fly.File{
Expand Down
Loading
Loading