fix(deploy): write missing content files before compose up on any server

Call write_missing_content_files() at the start of start_by_compose_file()
so bind-mounted content files exist before `docker compose up` runs, which
would otherwise create them as empty directories. This also covers
Dockerfile deployments to additional servers that lack the files.

Add a feature test for a Dockerfile deployment to an additional server,
and note in lessons that long-lived Horizon workers must flush static
caches in Queue::before.
This commit is contained in:
Andras Bacsai committed 2026-09-28 17:04:01 +02:00
1 parent d28bc91ff5
commit a1fe4b71d9
3 files changed
+52 -1

No files matched your search

+1
View File
@@ -57,6 +57,7 @@
- Use the database as the correctness source for dynamic cron occurrences shared by multiple scheduler and Horizon nodes; Redis locks are load controls, not a durable execution ledger.
- Give each schedule occurrence a unique database identity and make queue consumers claim it atomically before external work.
- Keep pending occurrences recoverable across publisher interruptions, and define an explicit bounded policy for late or offline schedules.
- Horizon workers are long-lived: flush every static or `once()` cache (for example `Server::flushIdentityMap()`) in `Queue::before`, or later jobs decide with stale state.
## Fail closed at public webhook boundaries
- Reject missing or blank secrets before signature verification, and return generic errors without logging secrets, signatures, or payloads.
+8 -1
View File
@@ -1031,7 +1031,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
/**
* The queued ServerStorageSaveJob writes new content files, but it can run after the containers
* start. Preserve-repository deployments write all file storages in write_deployment_configurations().
* Each deployment writes the files on its own server, so additional servers also get them.
* Each deployment writes the missing files on its own server before the containers start, so
* additional servers also get them.
*/
private function write_missing_content_files(): void
{
@@ -4602,8 +4603,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
->values();
}
/**
* `docker compose up` creates a missing bind source as an empty directory, so content files are
* written first. This also gives a server that was added later the files it does not have.
*/
private function start_by_compose_file()
{
$this->write_missing_content_files();
try {
// Ensure .env file exists before docker compose tries to load it (defensive programming)
$this->execute_remote_command(
@@ -361,6 +361,49 @@ test('a deployment to an additional server writes missing content files on that
->and(ContentFilesUpRecorder::ssh()[$write])->not->toContain("@'{$this->server->ip}'");
});
test('a Dockerfile deployment to an additional server writes missing content files before the container starts', function () {
$this->application->update(['build_pack' => 'dockerfile', 'docker_compose_location' => null]);
$volume = contentFilesVolume($this->application, 'config/app.conf');
$otherServer = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->server->private_key_id,
])->fresh();
fakeContentFilesServer([$volume->fs_path => 'missing']);
$job = new ContentFilesUpDeploymentJob;
$queue = Mockery::mock(ApplicationDeploymentQueue::class)->makePartial();
$queue->shouldReceive('addLogEntry')->andReturnUsing(function (string $message, string $type = 'stdout') {
$this->logEntries[] = [$message, $type];
});
foreach ([
'application' => $this->application->fresh(),
'application_deployment_queue' => $queue,
'server' => $otherServer,
'mainServer' => $otherServer,
'deployment_uuid' => 'deployment-uuid',
'workdir' => '/artifacts/deployment-uuid',
'configuration_dir' => $this->application->workdir(),
'docker_compose_location' => '/docker-compose.yaml',
'coolify_variables' => '',
'pull_request_id' => 0,
'preserveRepository' => false,
'use_build_server' => false,
'saved_outputs' => collect(),
] as $property => $value) {
(new ReflectionProperty(ApplicationDeploymentJob::class, $property))->setValue($job, $value);
}
(new ReflectionMethod(ApplicationDeploymentJob::class, 'start_by_compose_file'))->invoke($job);
$write = ContentFilesUpRecorder::indexOf('ssh', 'base64 -d | tee '.escapeshellarg($volume->fs_path));
$up = ContentFilesUpRecorder::indexOf('deployment', ' up --build -d');
expect($write)->not->toBeNull()
->and($up)->not->toBeNull()
->and($write)->toBeLessThan($up)
->and(ContentFilesUpRecorder::ssh()[$write])->toContain("@'{$otherServer->ip}'")
->and($this->logEntries)->toContain(['Writing 1 missing configuration file.', 'stdout']);
});
test('the content file check and write are safe for non-root servers', function () {
$this->server->update(['user' => 'ubuntu']);
$volume = contentFilesVolume($this->application, 'config/app.conf');