diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index db00977b8d..0a1fcdfc3d 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -460,7 +460,7 @@ class General extends Component
$this->portsMappings = $this->application->ports_mappings;
$this->customNetworkAliases = $this->application->custom_network_aliases;
$this->dockerfile = $this->application->dockerfile;
- $this->dockerfileLocation = $this->application->dockerfile_location;
+ $this->dockerfileLocation = $this->application->dockerfile_location ?? ($this->buildPack === 'dockerfile' ? '/Dockerfile' : null);
$this->dockerfileTargetBuild = $this->application->dockerfile_target_build;
$this->dockerRegistryImageName = $this->application->docker_registry_image_name;
$this->dockerRegistryImageTag = $this->application->docker_registry_image_tag;
diff --git a/resources/views/components/forms/repository-paths.blade.php b/resources/views/components/forms/repository-paths.blade.php
new file mode 100644
index 0000000000..1b5c6f94f9
--- /dev/null
+++ b/resources/views/components/forms/repository-paths.blade.php
@@ -0,0 +1,47 @@
+@props([
+ 'base',
+ 'file' => null,
+ 'fileLabel' => null,
+ 'defaultFile' => null,
+ 'disabled' => 'false',
+])
+
+
class('grid gap-4') }} x-data="{
+ base: $wire.entangle('{{ $base }}'),
+ file: {{ $file ? "\$wire.entangle('{$file}')" : 'null' }},
+ normalize(path) {
+ path = (path ?? '').trim().replace(/\/+$/, '');
+ return path === '' || path.startsWith('/') ? path : '/' + path;
+ },
+ get directory() {
+ return this.normalize(this.base);
+ },
+ get location() {
+ return this.normalize(this.file) || '{{ $defaultFile }}';
+ },
+ get repeatsBase() {
+ return this.directory !== '' && this.location.startsWith(this.directory + '/');
+ },
+ get suggestion() {
+ return this.location.slice(this.directory.length);
+ },
+}">
+
+ @if ($file)
+
+
+ Resolved file:
+
+
+
+ {{ $fileLabel }} is relative to the base directory, so it should not start with
+ again.
+
+
+ @endif
+ {{ $slot }}
+
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php
index fa986e8532..b4ccb4cff1 100644
--- a/resources/views/livewire/project/application/general.blade.php
+++ b/resources/views/livewire/project/application/general.blade.php
@@ -144,35 +144,9 @@
@if ($buildPack === 'dockercompose')
-
-
-
-
+
@else
-
-
- @if ($buildPack === 'dockerfile' && !$application->dockerfile)
-
- @endif
-
- @if ($buildPack === 'dockerfile')
-
- @endif
+
@if ($application->could_set_build_commands())
@if ($application->settings->is_static)
@endif
@endif
-
-
+
@if ($this->application->is_github_based() && !$this->application->is_public_repository())
+ @if ($buildPack === 'dockerfile')
+
+ @endif
@if ($build_pack === 'dockercompose')
-
-
-
-
- Resolved file:
-
-
-
+
@else
diff --git a/resources/views/livewire/project/new/github-private-repository.blade.php b/resources/views/livewire/project/new/github-private-repository.blade.php
index 38bd93ded9..638ea322cf 100644
--- a/resources/views/livewire/project/new/github-private-repository.blade.php
+++ b/resources/views/livewire/project/new/github-private-repository.blade.php
@@ -154,28 +154,9 @@
@endif
@if ($build_pack === 'dockercompose')
-
-
-
-
- Resolved file:
-
-
-
+
@else
diff --git a/resources/views/livewire/project/new/gitlab-private-repository.blade.php b/resources/views/livewire/project/new/gitlab-private-repository.blade.php
index b047669071..30e87ee35e 100644
--- a/resources/views/livewire/project/new/gitlab-private-repository.blade.php
+++ b/resources/views/livewire/project/new/gitlab-private-repository.blade.php
@@ -131,28 +131,9 @@
@if ($build_pack === 'dockercompose')
-
-
-
-
- Resolved file:
-
-
-
+
@else
diff --git a/resources/views/livewire/project/new/public-git-repository.blade.php b/resources/views/livewire/project/new/public-git-repository.blade.php
index 2d3d3ba596..b8ce0747e3 100644
--- a/resources/views/livewire/project/new/public-git-repository.blade.php
+++ b/resources/views/livewire/project/new/public-git-repository.blade.php
@@ -103,28 +103,9 @@
@endif
@if ($build_pack === 'dockercompose')
-
-
-
-
- Resolved file:
-
-
-
+
@else
diff --git a/tests/Feature/ApplicationGeneralBuildpackSelectorTest.php b/tests/Feature/ApplicationGeneralBuildpackSelectorTest.php
index d842d7c546..3d163f1a81 100644
--- a/tests/Feature/ApplicationGeneralBuildpackSelectorTest.php
+++ b/tests/Feature/ApplicationGeneralBuildpackSelectorTest.php
@@ -122,6 +122,26 @@ test('switching from railpack to compose preserves the existing application doma
->toBe('https://example.com,https://www.example.com');
});
+test('Dockerfile location defaults to the Dockerfile only for the Dockerfile build pack', function (string $buildPack, ?string $expected) {
+ $application = Application::factory()->create([
+ 'environment_id' => $this->environment->id,
+ 'destination_id' => $this->destination->id,
+ 'destination_type' => StandaloneDocker::class,
+ 'build_pack' => $buildPack,
+ 'static_image' => 'nginx:alpine',
+ 'base_directory' => '/',
+ 'is_http_basic_auth_enabled' => false,
+ 'redirect' => 'no',
+ ]);
+
+ Livewire::test(General::class, ['application' => $application])
+ ->assertSuccessful()
+ ->assertSet('dockerfileLocation', $expected);
+})->with([
+ 'Dockerfile' => ['dockerfile', '/Dockerfile'],
+ 'Nixpacks' => ['nixpacks', null],
+]);
+
test('networking section hints that internal ports can be set per domain', function () {
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
diff --git a/tests/v4/Browser/ApplicationConfigurationTest.php b/tests/v4/Browser/ApplicationConfigurationTest.php
index cb60e12949..7d65f22ae8 100644
--- a/tests/v4/Browser/ApplicationConfigurationTest.php
+++ b/tests/v4/Browser/ApplicationConfigurationTest.php
@@ -156,6 +156,59 @@ it('saves custom docker run options from the UI', function () {
->screenshot(filename: 'application-docker-run-options');
});
+it('warns when the Dockerfile location repeats the base directory and applies the suggested fix', function () {
+ $this->application->update([
+ 'build_pack' => 'dockerfile',
+ 'base_directory' => '/apps/api',
+ 'dockerfile_location' => '/apps/api/Dockerfile',
+ ]);
+
+ loginAndSkipBoarding();
+
+ $page = visit(applicationConfigurationUrl(
+ $this->stack['project'],
+ $this->stack['environment'],
+ $this->application
+ ));
+
+ $page->assertSee('Base directory is repeated')
+ ->assertSee('/apps/api/apps/api/Dockerfile')
+ ->screenshot(filename: 'application-dockerfile-location-repeats-base-directory')
+ ->click('Use /Dockerfile')
+ ->assertDontSee('Base directory is repeated')
+ ->assertSee('/apps/api/Dockerfile');
+
+ submitLivewireForm($page);
+
+ $page->assertNoJavaScriptErrors()
+ ->screenshot(filename: 'application-dockerfile-location-fixed');
+
+ $this->application->refresh();
+ expect($this->application->base_directory)->toBe('/apps/api')
+ ->and($this->application->dockerfile_location)->toBe('/Dockerfile');
+});
+
+it('restores the default file location when the field is cleared', function (string $buildPack, string $column, string $default) {
+ $this->application->update(['build_pack' => $buildPack, $column => '/docker/custom']);
+
+ loginAndSkipBoarding();
+
+ $page = visit(applicationConfigurationUrl(
+ $this->stack['project'],
+ $this->stack['environment'],
+ $this->application
+ ));
+
+ $page->clear("input[placeholder='{$default}']")
+ ->keys("input[placeholder='{$default}']", 'Tab')
+ ->assertValue("input[placeholder='{$default}']", $default)
+ ->assertNoJavaScriptErrors()
+ ->screenshot(filename: "application-{$buildPack}-location-cleared");
+})->with([
+ 'Dockerfile' => ['dockerfile', 'dockerfile_location', '/Dockerfile'],
+ 'Docker Compose' => ['dockercompose', 'docker_compose_location', '/docker-compose.yaml'],
+]);
+
it('opens environment variables page for the application', function () {
loginAndSkipBoarding();
diff --git a/tests/v4/Browser/NewApplicationRepositoryPathsTest.php b/tests/v4/Browser/NewApplicationRepositoryPathsTest.php
new file mode 100644
index 0000000000..7571eb06a6
--- /dev/null
+++ b/tests/v4/Browser/NewApplicationRepositoryPathsTest.php
@@ -0,0 +1,55 @@
+uuid}/environment/{$stack['environment']->uuid}/new?type=public&destination={$stack['destination']->uuid}");
+
+ $page->fill('repository_url', 'https://gitlab.com/example/monorepo.git')
+ ->click('Check repository')
+ ->assertSee('Build configuration');
+
+ selectListboxOption($page, 'build_pack', 'Docker Compose');
+
+ $page->clear("input[placeholder='/docker-compose.yaml']")
+ ->keys("input[placeholder='/docker-compose.yaml']", 'Tab')
+ ->assertValue("input[placeholder='/docker-compose.yaml']", '/docker-compose.yaml')
+ ->assertNoJavaScriptErrors()
+ ->screenshot(filename: 'new-application-compose-file-cleared');
+});
+
+it('warns when the compose file repeats the base directory and creates the application with the suggested fix', function () {
+ $stack = seedBrowserResourceStack();
+ loginAndSkipBoarding();
+
+ $page = visit("/project/{$stack['project']->uuid}/environment/{$stack['environment']->uuid}/new?type=public&destination={$stack['destination']->uuid}");
+
+ $page->fill('repository_url', 'https://gitlab.com/example/monorepo.git')
+ ->click('Check repository')
+ ->assertSee('Build configuration');
+
+ selectListboxOption($page, 'build_pack', 'Docker Compose');
+
+ $page->fill("input[placeholder='/']", 'apps/api/')
+ ->fill("input[placeholder='/docker-compose.yaml']", 'apps/api/compose.yaml')
+ ->assertSee('Base directory is repeated')
+ ->assertSee('/apps/api/apps/api/compose.yaml')
+ ->screenshot(filename: 'new-application-compose-file-repeats-base-directory')
+ ->click('button:has-text("Use /compose.yaml")')
+ ->assertDontSee('Base directory is repeated')
+ ->assertSee('/apps/api/compose.yaml')
+ ->assertNoJavaScriptErrors()
+ ->screenshot(filename: 'new-application-compose-file-fixed');
+
+ submitLivewireForm($page);
+
+ $application = Application::query()->where('git_repository', 'https://gitlab.com/example/monorepo.git')->sole();
+ expect($application->base_directory)->toBe('/apps/api')
+ ->and($application->docker_compose_location)->toBe('/compose.yaml');
+});