diff --git a/cabotage/celery/tasks/build.py b/cabotage/celery/tasks/build.py
index af2e65e0..9728dcf1 100644
--- a/cabotage/celery/tasks/build.py
+++ b/cabotage/celery/tasks/build.py
@@ -539,25 +539,25 @@ def file_path(filename):
return os.path.join(image.application.subdirectory, filename)
return filename
+ dockerfile_candidates = ["Dockerfile.cabotage", "Dockerfile"]
+ if image.application.dockerfile_path:
+ dockerfile_candidates = [image.application.dockerfile_path]
+
dockerfile_name = None
- dockerfile_body = _fetch_github_file(
- image.application.github_repository,
- image.commit_sha,
- access_token=access_token,
- filename=file_path("Dockerfile.cabotage"),
- )
- dockerfile_name = "Dockerfile.cabotage"
- if dockerfile_body is None:
+ dockerfile_body = None
+ for candidate in dockerfile_candidates:
dockerfile_body = _fetch_github_file(
image.application.github_repository,
image.commit_sha,
access_token=access_token,
- filename=file_path("Dockerfile"),
+ filename=file_path(candidate),
)
- dockerfile_name = "Dockerfile"
+ if dockerfile_body is not None:
+ dockerfile_name = candidate
+ break
if dockerfile_body is None:
raise BuildError(
- "No Dockerfile.cabotage or Dockerfile found in root of "
+ f"No Dockerfile found in "
f"{git_ref(image.application.github_repository, image.commit_sha)}"
)
diff --git a/cabotage/client/templates/user/project_application_settings.html b/cabotage/client/templates/user/project_application_settings.html
index b7bee8e4..a313659d 100644
--- a/cabotage/client/templates/user/project_application_settings.html
+++ b/cabotage/client/templates/user/project_application_settings.html
@@ -25,6 +25,7 @@
Deployment Automation
{{ form_field(form.github_repository) }}
{{ form_field(form.auto_deploy_branch) }}
{{ form_field(form.subdirectory) }}
+ {{ form_field(form.dockerfile_path) }}
{{ form_field(form.github_repository_is_private) }}
{{ form_field(form.github_app_installation_id) }}
{{ form_field(form.github_environment_name ) }}
diff --git a/cabotage/server/models/projects.py b/cabotage/server/models/projects.py
index 1253f2ef..949e0ea1 100644
--- a/cabotage/server/models/projects.py
+++ b/cabotage/server/models/projects.py
@@ -192,6 +192,11 @@ class Application(db.Model, Timestamp):
nullable=True,
)
+ dockerfile_path = db.Column(
+ db.Text(),
+ nullable=True,
+ )
+
auto_deploy_branch = db.Column(
db.Text(),
nullable=True,
diff --git a/cabotage/server/user/forms.py b/cabotage/server/user/forms.py
index 3858a74b..f9630910 100755
--- a/cabotage/server/user/forms.py
+++ b/cabotage/server/user/forms.py
@@ -262,6 +262,14 @@ class EditApplicationSettingsForm(FlaskForm):
"Subdirectory",
description="Subdirectory to build out of",
)
+ dockerfile_path = StringField(
+ "Dockerfile Path",
+ description="Custom path to Dockerfile (e.g. docker/Dockerfile), falls back to Dockerfile.cabotage then Dockerfile",
+ filters=[
+ (lambda x: x.strip() if (x and isinstance(x, str)) else x),
+ (lambda x: x if x else None),
+ ],
+ )
github_app_installation_id = StringField(
"GitHub Application Installation ID",
description="Application Installation ID from GitHub",
diff --git a/migrations/versions/a1b2c3d4e5f6_add_dockerfile_path.py b/migrations/versions/a1b2c3d4e5f6_add_dockerfile_path.py
new file mode 100644
index 00000000..da3b5187
--- /dev/null
+++ b/migrations/versions/a1b2c3d4e5f6_add_dockerfile_path.py
@@ -0,0 +1,30 @@
+"""Add configurable dockerfile_path to applications
+
+Revision ID: a1b2c3d4e5f6
+Revises: c2ae2e19e1f2
+Create Date: 2026-02-17 20:30:00.000000
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+revision = "a1b2c3d4e5f6"
+down_revision = "c2ae2e19e1f2"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.add_column(
+ "project_applications", sa.Column("dockerfile_path", sa.Text(), nullable=True)
+ )
+ op.add_column(
+ "project_applications_version",
+ sa.Column("dockerfile_path", sa.Text(), autoincrement=False, nullable=True),
+ )
+
+
+def downgrade():
+ op.drop_column("project_applications_version", "dockerfile_path")
+ op.drop_column("project_applications", "dockerfile_path")