Repository: superfly/flyctl
Pull request #3723 (2024-07-10 09:32:00)

Bump google.golang.org/grpc from 1.64.1 to 1.65.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.64.1 to 1.65.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's releases</a>.</em></p>
<blockquote>
<h2>Release 1.65.0</h2>
<h1>Dependencies</h1>
<ul>
<li>Change support policy to cover only the latest TWO releases of Go, matching the policy for Go itself.  See <a href="https://redirect.github.com/grpc/grpc-go/issues/7249">#7249</a> for more information. (<a href="https://redirect.github.com/grpc/grpc-go/issues/7250">#7250</a>)</li>
<li>Update x/net/http2 to address <a href="https://nvd.nist.gov/vuln/detail/CVE-2023-45288">CVE-2023-45288</a> (<a href="https://redirect.github.com/grpc/grpc-go/issues/7282">#7282</a>)</li>
</ul>
<h1>Behavior Changes</h1>
<ul>
<li>credentials/tls: clients and servers will now reject connections that don't support ALPN when environment variable <code>GRPC_ENFORCE_ALPN_ENABLED</code> is set to &quot;true&quot; (case insensitive). (<a href="https://redirect.github.com/grpc/grpc-go/issues/7184">#7184</a>)
<ul>
<li>NOTE: this behavior will become the default in a future release.</li>
</ul>
</li>
<li>metadata: remove String method from MD to make printing more consistent (<a href="https://redirect.github.com/grpc/grpc-go/issues/7373">#7373</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>grpc: add <code>WithMaxCallAttempts</code> to configure gRPC's retry behavior per-channel. (<a href="https://redirect.github.com/grpc/grpc-go/issues/7229">#7229</a>)
<ul>
<li>Special Thanks: <a href="https://github.com/imoore76"><code>@​imoore76</code></a></li>
</ul>
</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>ringhash: properly apply endpoint weights instead of ignoring them (<a href="https://redirect.github.com/grpc/grpc-go/issues/7156">#7156</a>)</li>
<li>xds: fix a bug that could cause xds-enabled servers to stop accepting new connections after handshaking errors (<a href="https://redirect.github.com/grpc/grpc-go/issues/7128">#7128</a>)
<ul>
<li>Special Thanks: <a href="https://github.com/bozaro"><code>@​bozaro</code></a></li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/grpc/grpc-go/commit/2da976983bbb33feb3e25b7daaa8f60b9769adb5"><code>2da9769</code></a> Change version to 1.65.0 (<a href="https://redirect.github.com/grpc/grpc-go/issues/7306">#7306</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/ede96b776c9cea4ff2b06dfdedad841e53fb0c26"><code>ede96b7</code></a> metadata: remove String method (<a href="https://redirect.github.com/grpc/grpc-go/issues/7373">#7373</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/64be203f7828d03224aa44944a84deacac54b72b"><code>64be203</code></a> grpc: Readd pick first name (<a href="https://redirect.github.com/grpc/grpc-go/issues/7336">#7336</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7341">#7341</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/25e33a6c207464c6017d001255f9ac1297ba5d3e"><code>25e33a6</code></a> examples: Add CSM Observability example (<a href="https://redirect.github.com/grpc/grpc-go/issues/7302">#7302</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7318">#7318</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/04a5f4604baa9297b7168543834e531cfb8858dc"><code>04a5f46</code></a> xds/internal/xdsclient: Emit unknown for CSM Labels if not present in CDS (<a href="https://redirect.github.com/grpc/grpc-go/issues/7">#7</a>...</li>
<li><a href="https://github.com/grpc/grpc-go/commit/cff5c3e3d0f104459af4f52e77fc1c49ef4cb6aa"><code>cff5c3e</code></a> stats/opentelemetry: Add e2e testing for CSM Observability (<a href="https://redirect.github.com/grpc/grpc-go/issues/7279">#7279</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7316">#7316</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/9b970fdd173c1951a354630133273639e8acf00c"><code>9b970fd</code></a> dns: fix constant 30s backoff for re-resolution (<a href="https://redirect.github.com/grpc/grpc-go/issues/7262">#7262</a>) (<a href="https://redirect.github.com/grpc/grpc-go/issues/7311">#7311</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/6d236200ea68ea644d78a164c4ad9952a765aed3"><code>6d23620</code></a> documentation: on server, use FromIncomingContext  for retrieving context and...</li>
<li><a href="https://github.com/grpc/grpc-go/commit/7e5898e7c569b8cc32d6eee9bc8ad1393a8fcc61"><code>7e5898e</code></a> xds: unify xDS client creation APIs meant for testing (<a href="https://redirect.github.com/grpc/grpc-go/issues/7268">#7268</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/5d7bd7aacb0c35fc14bf144fa81ecbb61eec91de"><code>5d7bd7a</code></a> interop/xds: Interop client and server changes for CSM Observability (<a href="https://redirect.github.com/grpc/grpc-go/issues/7280">#7280</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/grpc/grpc-go/compare/v1.64.1...v1.65.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.64.1&new-version=1.65.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/go.mod b/go.mod
index e03dce5fc..78389bdd6 100644
--- a/go.mod
+++ b/go.mod
@@ -87,7 +87,7 @@ require (
 	golang.org/x/text v0.16.0
 	golang.org/x/time v0.5.0
 	golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
-	google.golang.org/grpc v1.64.1
+	google.golang.org/grpc v1.65.0
 	gopkg.in/yaml.v2 v2.4.0
 	gopkg.in/yaml.v3 v3.0.1
 	nhooyr.io/websocket v1.8.11
@@ -134,7 +134,7 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/buildpacks/imgutil v0.0.0-20240118145509-e94a1b7de8a9 // indirect
 	github.com/buildpacks/lifecycle v0.18.5 // indirect
-	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect
 	github.com/cloudflare/circl v1.3.7 // indirect
 	github.com/containerd/console v1.0.4 // indirect
diff --git a/go.sum b/go.sum
index 37f9ca3b5..c845e65ba 100644
--- a/go.sum
+++ b/go.sum
@@ -157,8 +157,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4=
 github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
@@ -836,8 +836,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
-google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go
index ad5af59db..89cb4ed98 100644
--- a/internal/appconfig/config.go
+++ b/internal/appconfig/config.go
@@ -58,6 +58,8 @@ type Config struct {
 	Files            []File                    `toml:"files,omitempty" json:"files,omitempty"`
 	HostDedicationID string                    `toml:"host_dedication_id,omitempty" json:"host_dedication_id,omitempty"`
 
+	MachineChecks []*ServiceMachineCheck `toml:"machine_checks,omitempty" json:"machine_checks,omitempty"`
+
 	Restart []Restart `toml:"restart,omitempty" json:"restart,omitempty"`
 
 	Compute []*Compute `toml:"vm,omitempty" json:"vm,omitempty"`
diff --git a/internal/command/deploy/machines_deploymachinesapp.go b/internal/command/deploy/machines_deploymachinesapp.go
index 93c0aa422..ad6fa036d 100644
--- a/internal/command/deploy/machines_deploymachinesapp.go
+++ b/internal/command/deploy/machines_deploymachinesapp.go
@@ -730,7 +730,8 @@ func (md *machineDeployment) updateMachine(ctx context.Context, e *machineUpdate
 			// Replacing a machine with a volume will cause the placement logic to pick wthe same host
 			// dismissing the value of replacing it in case of lack of host capacity
 			return err
-		case strings.Contains(err.Error(), "could not reserve resource for machine"):
+		case strings.Contains(err.Error(), "could not reserve resource for machine"),
+			strings.Contains(err.Error(), "deploys to this host are temporarily disabled"):
 			return replaceMachine()
 		default:
 			return err
diff --git a/internal/command/extensions/core/core.go b/internal/command/extensions/core/core.go
index 07cce3899..6be47587f 100644
--- a/internal/command/extensions/core/core.go
+++ b/internal/command/extensions/core/core.go
@@ -640,6 +640,7 @@ var PlatformMap = map[string]string{
 	"NodeJS":        "node",
 	"NodeJS/Prisma": "node",
 	"Laravel":       "php-laravel",
+	"Meteor":        "javascript-meteor",
 	"NestJS":        "node",
 	"NextJS":        "javascript-nextjs",
 	"Nuxt":          "javascript-vue",
diff --git a/internal/command/launch/cmd.go b/internal/command/launch/cmd.go
index 13c0c155e..f8ecc6ff0 100644
--- a/internal/command/launch/cmd.go
+++ b/internal/command/launch/cmd.go
@@ -106,6 +106,10 @@ func New() (cmd *cobra.Command) {
 			Name:        "yaml",
 			Description: "Generate configuration in YAML format",
 		},
+		flag.Bool{
+			Name:        "no-create",
+			Description: "Do not create an app, only generate configuration files",
+		},
 	)
 
 	return
@@ -187,20 +191,22 @@ func run(ctx context.Context) (err error) {
 
 	var state *launchState = nil
 
-	defer func() {
-		if err != nil {
-			tracing.RecordError(span, err, "launch failed")
-			status.Error = err.Error()
+	if !flag.GetBool(ctx, "no-create") {
+		defer func() {
+			if err != nil {
+				tracing.RecordError(span, err, "launch failed")
+				status.Error = err.Error()
 
-			if state != nil && state.sourceInfo != nil && state.sourceInfo.FailureCallback != nil {
-				err = state.sourceInfo.FailureCallback(err)
+				if state != nil && state.sourceInfo != nil && state.sourceInfo.FailureCallback != nil {
+					err = state.sourceInfo.FailureCallback(err)
+				}
 			}
-		}
 
-		status.TraceID = span.SpanContext().TraceID().String()
-		status.Duration = time.Since(startTime)
-		metrics.LaunchStatus(ctx, status)
-	}()
+			status.TraceID = span.SpanContext().TraceID().String()
+			status.Duration = time.Since(startTime)
+			metrics.LaunchStatus(ctx, status)
+		}()
+	}
 
 	if err := warnLegacyBehavior(ctx); err != nil {
 		return err
diff --git a/internal/command/launch/deploy.go b/internal/command/launch/deploy.go
index c9a266384..1696ad68c 100644
--- a/internal/command/launch/deploy.go
+++ b/internal/command/launch/deploy.go
@@ -27,6 +27,7 @@ func (state *launchState) firstDeploy(ctx context.Context) error {
 	}
 
 	// TODO(Allison): Do we want to make the executive decision to just *always* deploy?
+	// Feedback(Sam): scanners need the abiiity to abort the deploy if they detect a problem
 
 	deployNow := true
 	// deployNow := false
@@ -42,6 +43,10 @@ func (state *launchState) firstDeploy(ctx context.Context) error {
 		// promptForDeploy = false
 	}
 
+	if flag.GetBool(ctx, "no-create") {
+		deployNow = false
+	}
+
 	/*
 		if promptForDeploy {
 			confirm, err := prompt.Confirm(ctx, "Would you like to deploy now?")
diff --git a/internal/command/launch/launch.go b/internal/command/launch/launch.go
index 1b1dae636..b8fc8572d 100644
--- a/internal/command/launch/launch.go
+++ b/internal/command/launch/launch.go
@@ -47,19 +47,21 @@ func (state *launchState) Launch(ctx context.Context) error {
 		state.warnedNoCcHa = true
 	}
 
-	app, err := state.createApp(ctx)
-	if err != nil {
-		return err
-	}
+	if !flag.GetBool(ctx, "no-create") {
+		app, err := state.createApp(ctx)
+		if err != nil {
+			return err
+		}
 
-	fmt.Fprintf(io.Out, "Created app '%s' in organization '%s'\n", app.Name, app.Organization.Slug)
-	fmt.Fprintf(io.Out, "Admin URL: https://fly.io/apps/%s\n", app.Name)
-	fmt.Fprintf(io.Out, "Hostname: %s.fly.dev\n", app.Name)
+		fmt.Fprintf(io.Out, "Created app '%s' in organization '%s'\n", app.Name, app.Organization.Slug)
+		fmt.Fprintf(io.Out, "Admin URL: https://fly.io/apps/%s\n", app.Name)
+		fmt.Fprintf(io.Out, "Hostname: %s.fly.dev\n", app.Name)
+	}
 
 	// TODO: ideally this would be passed as a part of the plan to the Launch UI
 	// and allow choices of what actions are desired to be make there.
 	if state.sourceInfo != nil && state.sourceInfo.GitHubActions.Deploy {
-		state.setupGitHubActions(ctx, app.Name)
+		state.setupGitHubActions(ctx, state.Plan.AppName)
 	}
 
 	if err = state.satisfyScannerBeforeDb(ctx); err != nil {
@@ -67,9 +69,11 @@ func (state *launchState) Launch(ctx context.Context) error {
 	}
 	// TODO: Return rich info about provisioned DBs, including things
 	//       like public URLs.
-	err = state.createDatabases(ctx)
-	if err != nil {
-		return err
+
+	if !flag.GetBool(ctx, "no-create") {
+		if err = state.createDatabases(ctx); err != nil {
+			return err
+		}
 	}
 	if err = state.satisfyScannerAfterDb(ctx); err != nil {
 		return err
@@ -84,8 +88,10 @@ func (state *launchState) Launch(ctx context.Context) error {
 	}
 
 	// Sentry
-	if err = state.launchSentry(ctx, app.Name); err != nil {
-		return err
+	if !flag.GetBool(ctx, "no-create") {
+		if err = state.launchSentry(ctx, state.Plan.AppName); err != nil {
+			return err
+		}
 	}
 
 	// Finally write application configuration to fly.toml
diff --git a/internal/command/launch/launch_frameworks.go b/internal/command/launch/launch_frameworks.go
index de3e28a7b..8aa5b0647 100644
--- a/internal/command/launch/launch_frameworks.go
+++ b/internal/command/launch/launch_frameworks.go
@@ -277,6 +277,8 @@ func (state *launchState) scannerSetAppconfig(ctx context.Context) error {
 	for envName, envVal := range srcInfo.Env {
 		if envVal == "APP_FQDN" {
 			appConfig.SetEnvVariable(envName, appConfig.AppName+".fly.dev")
+		} else if envVal == "APP_URL" {
+			appConfig.SetEnvVariable(envName, "https://"+appConfig.AppName+".fly.dev")
 		} else {
 			appConfig.SetEnvVariable(envName, envVal)
 		}
diff --git a/scanner/jsFramework.go b/scanner/jsFramework.go
index 80b05cb0c..03a6ef816 100644
--- a/scanner/jsFramework.go
+++ b/scanner/jsFramework.go
@@ -230,6 +230,12 @@ func configureJsFramework(sourceDir string, config *ScannerConfig) (*SourceInfo,
 	} else if deps["gatsby"] != nil {
 		srcInfo.Family = "Gatsby"
 		srcInfo.Port = 8080
+	} else if startScript, ok := scripts["start"].(string); ok && strings.Contains(startScript, "meteor") {
+		srcInfo.Family = "Meteor"
+		srcInfo.Env = map[string]string{
+			"PORT":     "3000",
+			"ROOT_URL": "APP_URL",
+		}
 	} else if deps["@nestjs/core"] != nil {
 		srcInfo.Family = "NestJS"
 	} else if deps["next"] != nil {
diff --git a/scanner/templates/python-docker/.dockerignore b/scanner/templates/python-docker/.dockerignore
index 69ef095a5..4a1c39cdb 100644
--- a/scanner/templates/python-docker/.dockerignore
+++ b/scanner/templates/python-docker/.dockerignore
@@ -2,3 +2,4 @@ fly.toml
 .git/
 __pycache__/
 .envrc
+.venv/
diff --git a/scanner/templates/python-docker/Dockerfile b/scanner/templates/python-docker/Dockerfile
index d7132b206..2140331e1 100644
--- a/scanner/templates/python-docker/Dockerfile
+++ b/scanner/templates/python-docker/Dockerfile
@@ -1,45 +1,34 @@
-FROM python:{{ .pyVersion }} AS builder
+FROM python:{{ .pyVersion }}-bookworm AS builder
 
 ENV PYTHONUNBUFFERED=1 \
     PYTHONDONTWRITEBYTECODE=1
-
 WORKDIR /app
-COPY . .
 
 {{ if .pipenv -}}
-
 ENV PIPENV_VENV_IN_PROJECT=1 \
     PIPENV_CUSTOM_VENV_NAME=.venv
 RUN pip install pipenv
+COPY Pipfile Pipfile.lock ./
 RUN pipenv install
-
 {{ else if .poetry -}}
-
 RUN pip install poetry
 RUN poetry config virtualenvs.in-project true
+COPY pyproject.toml poetry.lock ./
 RUN poetry install
-
 {{ else if .pep621 -}}
-
-RUN python -m venv .venv && \
-    . .venv/bin/activate && \
-    pip install --upgrade pip && \
-    pip install .
-
+RUN python -m venv .venv
+COPY pyproject.toml ./
+RUN .venv/bin/pip install .
 {{ else if .pip }}
-
-RUN python -m venv .venv && \
-    . .venv/bin/activate && \
-    pip install --upgrade pip && \
-    pip install -r requirements.txt
-
+RUN python -m venv .venv
+COPY requirements.txt ./
+RUN .venv/bin/pip install -r requirements.txt
 {{ end -}}
 
-FROM python:{{ .pyVersion }}-slim
-
+FROM python:{{ .pyVersion }}-slim-bookworm
 WORKDIR /app
-COPY --from=builder /app .
-
+COPY --from=builder /app/.venv .venv/
+COPY . .
 {{ if .flask -}}
 CMD ["/app/.venv/bin/flask", "run", "--host=0.0.0.0", "--port=8080"]
 {{ else if .fastapi -}}


Test patch:
diff --git a/internal/appconfig/definition_test.go b/internal/appconfig/definition_test.go
index cb447704a..e075092d3 100644
--- a/internal/appconfig/definition_test.go
+++ b/internal/appconfig/definition_test.go
@@ -237,6 +237,15 @@ func TestToDefinition(t *testing.T) {
 				},
 			},
 		},
+		"machine_checks": []any{
+			map[string]any{
+				"command":      []any{"curl", "https://fly.io"},
+				"image":        "curlimages/curl",
+				"entrypoint":   []any{"/bin/sh"},
+				"kill_signal":  "SIGKILL",
+				"kill_timeout": "5s",
+			},
+		},
 		"experimental": map[string]any{
 			"cmd":           []any{"cmd"},
 			"entrypoint":    []any{"entrypoint"},
diff --git a/internal/appconfig/serde_test.go b/internal/appconfig/serde_test.go
index 18bfea1fa..d36eaea35 100644
--- a/internal/appconfig/serde_test.go
+++ b/internal/appconfig/serde_test.go
@@ -464,6 +464,16 @@ func TestLoadTOMLAppConfigReferenceFormat(t *testing.T) {
 			},
 		},
 
+		MachineChecks: []*ServiceMachineCheck{
+			{
+				Command:     []string{"curl", "https://fly.io"},
+				Entrypoint:  []string{"/bin/sh"},
+				Image:       "curlimages/curl",
+				KillSignal:  fly.StringPointer("SIGKILL"),
+				KillTimeout: fly.MustParseDuration("5s"),
+			},
+		},
+
 		Statics: []Static{
 			{
 				GuestPath:     "/path/to/statics",
diff --git a/internal/appconfig/testdata/full-reference.toml b/internal/appconfig/testdata/full-reference.toml
index 67be359a7..7e9aa9730 100644
--- a/internal/appconfig/testdata/full-reference.toml
+++ b/internal/appconfig/testdata/full-reference.toml
@@ -162,6 +162,13 @@ host_dedication_id = "06031957"
     Content-Type = "application/json"
     Authorization = "super-duper-secret"
 
+[[machine_checks]]
+   command = ["curl", "https://fly.io"]
+   image = "curlimages/curl"
+   entrypoint = ["/bin/sh"]
+   kill_timeout = "5s"
+   kill_signal = "SIGKILL"
+
 [[services]]
   internal_port = 8081
   protocol = "tcp"
diff --git a/internal/command/deploy/machinebasedtest.go b/internal/command/deploy/machinebasedtest.go
index fe9992d77..caa4af56e 100644
--- a/internal/command/deploy/machinebasedtest.go
+++ b/internal/command/deploy/machinebasedtest.go
@@ -43,6 +43,7 @@ func (md *machineDeployment) runTestMachines(ctx context.Context, machineToTest
 			return nil
 		}
 	})
+	machineChecks = append(machineChecks, md.appConfig.MachineChecks...)
 
 	if len(machineChecks) == 0 {
 		span.AddEvent("no machine checks")
diff --git a/test/preflight/fly_deploy_test.go b/test/preflight/fly_deploy_test.go
index 690aeef6d..79be98b52 100644
--- a/test/preflight/fly_deploy_test.go
+++ b/test/preflight/fly_deploy_test.go
@@ -250,6 +250,24 @@ func TestFlyDeploy_DeployMachinesCheck(t *testing.T) {
 	require.Contains(f, output, "Test Machine")
 }
 
+func TestFlyDeploy_NoServiceDeployMachinesCheck(t *testing.T) {
+	f := testlib.NewTestEnvFromEnv(t)
+	appName := f.CreateRandomAppName()
+	f.Fly("launch --org %s --name %s --region %s --image nginx --internal-port 80 --ha=false", f.OrgSlug(), appName, f.PrimaryRegion())
+	appConfig := f.ReadFile("fly.toml")
+	appConfig += `
+		[[machine_checks]]
+			image = "curlimages/curl"
+			entrypoint = ["/bin/sh", "-c"]
+			command = ["curl http://[$FLY_TEST_MACHINE_IP]:80"]
+		`
+	f.WriteFlyToml(appConfig)
+	f.OverrideAuthAccessToken(f.Fly("tokens deploy").StdOut().String())
+	deployRes := f.Fly("deploy")
+	output := deployRes.StdOutString()
+	require.Contains(f, output, "Test Machine")
+}
+
 func TestFlyDeploy_DeployMachinesCheckCanary(t *testing.T) {
 	f := testlib.NewTestEnvFromEnv(t)
 	appName := f.CreateRandomAppName()