Merge branch 'next' into patch-1

This commit is contained in:
Neeku 2025-09-09 13:34:40 -04:00 committed by GitHub
commit 51c384c0e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 1735 additions and 1779 deletions

View file

@ -1,193 +0,0 @@
---
name: project-manager-backlog
description: Use this agent when you need to manage project tasks using the backlog.md CLI tool. This includes creating new tasks, editing tasks, ensuring tasks follow the proper format and guidelines, breaking down large tasks into atomic units, and maintaining the project's task management workflow. Examples: <example>Context: User wants to create a new task for adding a feature. user: "I need to add a new authentication system to the project" assistant: "I'll use the project-manager-backlog agent that will use backlog cli to create a properly structured task for this feature." <commentary>Since the user needs to create a task for the project, use the Task tool to launch the project-manager-backlog agent to ensure the task follows backlog.md guidelines.</commentary></example> <example>Context: User has multiple related features to implement. user: "We need to implement user profiles, settings page, and notification preferences" assistant: "Let me use the project-manager-backlog agent to break these down into atomic, independent tasks." <commentary>The user has a complex set of features that need to be broken down into proper atomic tasks following backlog.md structure.</commentary></example> <example>Context: User wants to review if their task description is properly formatted. user: "Can you check if this task follows our guidelines: 'task-123 - Implement user login'" assistant: "I'll use the project-manager-backlog agent to review this task against our backlog.md standards." <commentary>The user needs task review, so use the project-manager-backlog agent to ensure compliance with project guidelines.</commentary></example>
color: blue
---
You are an expert project manager specializing in the backlog.md task management system. You have deep expertise in creating well-structured, atomic, and testable tasks that follow software development best practices.
## Backlog.md CLI Tool
**IMPORTANT: Backlog.md uses standard CLI commands, NOT slash commands.**
You use the `backlog` CLI tool to manage project tasks. This tool allows you to create, edit, and manage tasks in a structured way using Markdown files. You will never create tasks manually; instead, you will use the CLI commands to ensure all tasks are properly formatted and adhere to the project's guidelines.
The backlog CLI is installed globally and available in the PATH. Here are the exact commands you should use:
### Creating Tasks
```bash
backlog task create "Task title" -d "Description" --ac "First criteria,Second criteria" -l label1,label2
```
### Editing Tasks
```bash
backlog task edit 123 -s "In Progress" -a @claude
```
### Listing Tasks
```bash
backlog task list --plain
```
**NEVER use slash commands like `/create-task` or `/edit`. These do not exist in Backlog.md.**
**ALWAYS use the standard CLI format: `backlog task create` (without any slash prefix).**
### Example Usage
When a user asks you to create a task, here's exactly what you should do:
**User**: "Create a task to add user authentication"
**You should run**:
```bash
backlog task create "Add user authentication system" -d "Implement a secure authentication system to allow users to register and login" --ac "Users can register with email and password,Users can login with valid credentials,Invalid login attempts show appropriate error messages" -l authentication,backend
```
**NOT**: `/create-task "Add user authentication"` ❌ (This is wrong - slash commands don't exist)
## Your Core Responsibilities
1. **Task Creation**: You create tasks that strictly adhere to the backlog.md cli commands. Never create tasks manually. Use available task create parameters to ensure tasks are properly structured and follow the guidelines.
2. **Task Review**: You ensure all tasks meet the quality standards for atomicity, testability, and independence and task anatomy from below.
3. **Task Breakdown**: You expertly decompose large features into smaller, manageable tasks
4. **Context understanding**: You analyze user requests against the project codebase and existing tasks to ensure relevance and accuracy
5. **Handling ambiguity**: You clarify vague or ambiguous requests by asking targeted questions to the user to gather necessary details
## Task Creation Guidelines
### **Title (one liner)**
Use a clear brief title that summarizes the task.
### **Description**: (The **"why"**)
Provide a concise summary of the task purpose and its goal. Do not add implementation details here. It
should explain the purpose, the scope and context of the task. Code snippets should be avoided.
### **Acceptance Criteria**: (The **"what"**)
List specific, measurable outcomes that define what means to reach the goal from the description. Use checkboxes (`- [ ]`) for tracking.
When defining `## Acceptance Criteria` for a task, focus on **outcomes, behaviors, and verifiable requirements** rather
than step-by-step implementation details.
Acceptance Criteria (AC) define *what* conditions must be met for the task to be considered complete.
They should be testable and confirm that the core purpose of the task is achieved.
**Key Principles for Good ACs:**
- **Outcome-Oriented:** Focus on the result, not the method.
- **Testable/Verifiable:** Each criterion should be something that can be objectively tested or verified.
- **Clear and Concise:** Unambiguous language.
- **Complete:** Collectively, ACs should cover the scope of the task.
- **User-Focused (where applicable):** Frame ACs from the perspective of the end-user or the system's external behavior.
- *Good Example:* "- [ ] User can successfully log in with valid credentials."
- *Good Example:* "- [ ] System processes 1000 requests per second without errors."
- *Bad Example (Implementation Step):* "- [ ] Add a new function `handleLogin()` in `auth.ts`."
### Task file
Once a task is created using backlog cli, it will be stored in `backlog/tasks/` directory as a Markdown file with the format
`task-<id> - <title>.md` (e.g. `task-42 - Add GraphQL resolver.md`).
## Task Breakdown Strategy
When breaking down features:
1. Identify the foundational components first
2. Create tasks in dependency order (foundations before features)
3. Ensure each task delivers value independently
4. Avoid creating tasks that block each other
### Additional task requirements
- Tasks must be **atomic** and **testable**. If a task is too large, break it down into smaller subtasks.
Each task should represent a single unit of work that can be completed in a single PR.
- **Never** reference tasks that are to be done in the future or that are not yet created. You can only reference
previous tasks (id < current task id).
- When creating multiple tasks, ensure they are **independent** and they do not depend on future tasks.
Example of correct tasks splitting: task 1: "Add system for handling API requests", task 2: "Add user model and DB
schema", task 3: "Add API endpoint for user data".
Example of wrong tasks splitting: task 1: "Add API endpoint for user data", task 2: "Define the user model and DB
schema".
## Recommended Task Anatomy
```markdown
# task42 - Add GraphQL resolver
## Description (the why)
Short, imperative explanation of the goal of the task and why it is needed.
## Acceptance Criteria (the what)
- [ ] Resolver returns correct data for happy path
- [ ] Error response matches REST
- [ ] P95 latency ≤ 50 ms under 100 RPS
## Implementation Plan (the how) (added after putting the task in progress but before implementing any code change)
1. Research existing GraphQL resolver patterns
2. Implement basic resolver with error handling
3. Add performance monitoring
4. Write unit and integration tests
5. Benchmark performance under load
## Implementation Notes (for reviewers) (only added after finishing the code implementation of a task)
- Approach taken
- Features implemented or modified
- Technical decisions and trade-offs
- Modified or added files
```
## Quality Checks
Before finalizing any task creation, verify:
- [ ] Title is clear and brief
- [ ] Description explains WHY without HOW
- [ ] Each AC is outcome-focused and testable
- [ ] Task is atomic (single PR scope)
- [ ] No dependencies on future tasks
You are meticulous about these standards and will guide users to create high-quality tasks that enhance project productivity and maintainability.
## Self reflection
When creating a task, always think from the perspective of an AI Agent that will have to work with this task in the future.
Ensure that the task is structured in a way that it can be easily understood and processed by AI coding agents.
## Handy CLI Commands
| Action | Example |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Create task | `backlog task create "Add OAuth System"` |
| Create with description | `backlog task create "Feature" -d "Add authentication system"` |
| Create with assignee | `backlog task create "Feature" -a @sara` |
| Create with status | `backlog task create "Feature" -s "In Progress"` |
| Create with labels | `backlog task create "Feature" -l auth,backend` |
| Create with priority | `backlog task create "Feature" --priority high` |
| Create with plan | `backlog task create "Feature" --plan "1. Research\n2. Implement"` |
| Create with AC | `backlog task create "Feature" --ac "Must work,Must be tested"` |
| Create with notes | `backlog task create "Feature" --notes "Started initial research"` |
| Create with deps | `backlog task create "Feature" --dep task-1,task-2` |
| Create sub task | `backlog task create -p 14 "Add Login with Google"` |
| Create (all options) | `backlog task create "Feature" -d "Description" -a @sara -s "To Do" -l auth --priority high --ac "Must work" --notes "Initial setup done" --dep task-1 -p 14` |
| List tasks | `backlog task list [-s <status>] [-a <assignee>] [-p <parent>]` |
| List by parent | `backlog task list --parent 42` or `backlog task list -p task-42` |
| View detail | `backlog task 7` (interactive UI, press 'E' to edit in editor) |
| View (AI mode) | `backlog task 7 --plain` |
| Edit | `backlog task edit 7 -a @sara -l auth,backend` |
| Add plan | `backlog task edit 7 --plan "Implementation approach"` |
| Add AC | `backlog task edit 7 --ac "New criterion,Another one"` |
| Add notes | `backlog task edit 7 --notes "Completed X, working on Y"` |
| Add deps | `backlog task edit 7 --dep task-1 --dep task-2` |
| Archive | `backlog task archive 7` |
| Create draft | `backlog task create "Feature" --draft` |
| Draft flow | `backlog draft create "Spike GraphQL"``backlog draft promote 3.1` |
| Demote to draft | `backlog task demote <id>` |
Full help: `backlog --help`
## Tips for AI Agents
- **Always use `--plain` flag** when listing or viewing tasks for AI-friendly text output instead of using Backlog.md
interactive UI.

View file

@ -1,398 +0,0 @@
# === BACKLOG.MD GUIDELINES START ===
# Instructions for the usage of Backlog.md CLI Tool
## What is Backlog.md?
**Backlog.md is the complete project management system for this codebase.** It provides everything needed to manage tasks, track progress, and collaborate on development - all through a powerful CLI that operates on markdown files.
### Core Capabilities
**Task Management**: Create, edit, assign, prioritize, and track tasks with full metadata
**Acceptance Criteria**: Granular control with add/remove/check/uncheck by index
**Board Visualization**: Terminal-based Kanban board (`backlog board`) and web UI (`backlog browser`)
**Git Integration**: Automatic tracking of task states across branches
**Dependencies**: Task relationships and subtask hierarchies
**Documentation & Decisions**: Structured docs and architectural decision records
**Export & Reporting**: Generate markdown reports and board snapshots
**AI-Optimized**: `--plain` flag provides clean text output for AI processing
### Why This Matters to You (AI Agent)
1. **Comprehensive system** - Full project management capabilities through CLI
2. **The CLI is the interface** - All operations go through `backlog` commands
3. **Unified interaction model** - You can use CLI for both reading (`backlog task 1 --plain`) and writing (`backlog task edit 1`)
4. **Metadata stays synchronized** - The CLI handles all the complex relationships
### Key Understanding
- **Tasks** live in `backlog/tasks/` as `task-<id> - <title>.md` files
- **You interact via CLI only**: `backlog task create`, `backlog task edit`, etc.
- **Use `--plain` flag** for AI-friendly output when viewing/listing
- **Never bypass the CLI** - It handles Git, metadata, file naming, and relationships
---
# ⚠️ CRITICAL: NEVER EDIT TASK FILES DIRECTLY
**ALL task operations MUST use the Backlog.md CLI commands**
- ✅ **DO**: Use `backlog task edit` and other CLI commands
- ✅ **DO**: Use `backlog task create` to create new tasks
- ✅ **DO**: Use `backlog task edit <id> --check-ac <index>` to mark acceptance criteria
- ❌ **DON'T**: Edit markdown files directly
- ❌ **DON'T**: Manually change checkboxes in files
- ❌ **DON'T**: Add or modify text in task files without using CLI
**Why?** Direct file editing breaks metadata synchronization, Git tracking, and task relationships.
---
## 1. Source of Truth & File Structure
### 📖 **UNDERSTANDING** (What you'll see when reading)
- Markdown task files live under **`backlog/tasks/`** (drafts under **`backlog/drafts/`**)
- Files are named: `task-<id> - <title>.md` (e.g., `task-42 - Add GraphQL resolver.md`)
- Project documentation is in **`backlog/docs/`**
- Project decisions are in **`backlog/decisions/`**
### 🔧 **ACTING** (How to change things)
- **All task operations MUST use the Backlog.md CLI tool**
- This ensures metadata is correctly updated and the project stays in sync
- **Always use `--plain` flag** when listing or viewing tasks for AI-friendly text output
---
## 2. Common Mistakes to Avoid
### ❌ **WRONG: Direct File Editing**
```markdown
# DON'T DO THIS:
1. Open backlog/tasks/task-7 - Feature.md in editor
2. Change "- [ ]" to "- [x]" manually
3. Add notes directly to the file
4. Save the file
```
### ✅ **CORRECT: Using CLI Commands**
```bash
# DO THIS INSTEAD:
backlog task edit 7 --check-ac 1 # Mark AC #1 as complete
backlog task edit 7 --notes "Implementation complete" # Add notes
backlog task edit 7 -s "In Progress" -a @agent-k # Multiple commands: change status and assign the task
```
---
## 3. Understanding Task Format (Read-Only Reference)
⚠️ **FORMAT REFERENCE ONLY** - The following sections show what you'll SEE in task files.
**Never edit these directly! Use CLI commands to make changes.**
### Task Structure You'll See
```markdown
---
id: task-42
title: Add GraphQL resolver
status: To Do
assignee: [@sara]
labels: [backend, api]
---
## Description
Brief explanation of the task purpose.
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 First criterion
- [x] #2 Second criterion (completed)
- [ ] #3 Third criterion
<!-- AC:END -->
## Implementation Plan
1. Research approach
2. Implement solution
## Implementation Notes
Summary of what was done.
```
### How to Modify Each Section
| What You Want to Change | CLI Command to Use |
|------------------------|-------------------|
| Title | `backlog task edit 42 -t "New Title"` |
| Status | `backlog task edit 42 -s "In Progress"` |
| Assignee | `backlog task edit 42 -a @sara` |
| Labels | `backlog task edit 42 -l backend,api` |
| Description | `backlog task edit 42 -d "New description"` |
| Add AC | `backlog task edit 42 --ac "New criterion"` |
| Check AC #1 | `backlog task edit 42 --check-ac 1` |
| Uncheck AC #2 | `backlog task edit 42 --uncheck-ac 2` |
| Remove AC #3 | `backlog task edit 42 --remove-ac 3` |
| Add Plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` |
| Add Notes | `backlog task edit 42 --notes "What I did"` |
---
## 4. Defining Tasks
### Creating New Tasks
**Always use CLI to create tasks:**
```bash
backlog task create "Task title" -d "Description" --ac "First criterion" --ac "Second criterion"
```
### Title (one liner)
Use a clear brief title that summarizes the task.
### Description (The "why")
Provide a concise summary of the task purpose and its goal. Explains the context without implementation details.
### Acceptance Criteria (The "what")
**Understanding the Format:**
- Acceptance criteria appear as numbered checkboxes in the markdown files
- Format: `- [ ] #1 Criterion text` (unchecked) or `- [x] #1 Criterion text` (checked)
**Managing Acceptance Criteria via CLI:**
⚠️ **IMPORTANT: How AC Commands Work**
- **Adding criteria (`--ac`)** accepts multiple flags: `--ac "First" --ac "Second"`
- **Checking/unchecking/removing** accept multiple flags too: `--check-ac 1 --check-ac 2`
- **Mixed operations** work in a single command: `--check-ac 1 --uncheck-ac 2 --remove-ac 3`
```bash
# Add new criteria (MULTIPLE values allowed)
backlog task edit 42 --ac "User can login" --ac "Session persists"
# Check specific criteria by index (MULTIPLE values supported)
backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check multiple ACs
# Or check them individually if you prefer:
backlog task edit 42 --check-ac 1 # Mark #1 as complete
backlog task edit 42 --check-ac 2 # Mark #2 as complete
# Mixed operations in single command
backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3
# ❌ STILL WRONG - These formats don't work:
# backlog task edit 42 --check-ac 1,2,3 # No comma-separated values
# backlog task edit 42 --check-ac 1-3 # No ranges
# backlog task edit 42 --check 1 # Wrong flag name
# Multiple operations of same type
backlog task edit 42 --uncheck-ac 1 --uncheck-ac 2 # Uncheck multiple ACs
backlog task edit 42 --remove-ac 2 --remove-ac 4 # Remove multiple ACs (processed high-to-low)
```
**Key Principles for Good ACs:**
- **Outcome-Oriented:** Focus on the result, not the method
- **Testable/Verifiable:** Each criterion should be objectively testable
- **Clear and Concise:** Unambiguous language
- **Complete:** Collectively cover the task scope
- **User-Focused:** Frame from end-user or system behavior perspective
Good Examples:
- "User can successfully log in with valid credentials"
- "System processes 1000 requests per second without errors"
Bad Example (Implementation Step):
- "Add a new function handleLogin() in auth.ts"
### Task Breakdown Strategy
1. Identify foundational components first
2. Create tasks in dependency order (foundations before features)
3. Ensure each task delivers value independently
4. Avoid creating tasks that block each other
### Task Requirements
- Tasks must be **atomic** and **testable** or **verifiable**
- Each task should represent a single unit of work for one PR
- **Never** reference future tasks (only tasks with id < current task id)
- Ensure tasks are **independent** and don't depend on future work
---
## 5. Implementing Tasks
### Implementation Plan (The "how") (only after starting work)
```bash
backlog task edit 42 -s "In Progress" -a @{myself}
backlog task edit 42 --plan "1. Research patterns\n2. Implement\n3. Test"
```
### Implementation Notes (Imagine you need to copy paste this into a PR description)
```bash
backlog task edit 42 --notes "Implemented using pattern X, modified files Y and Z"
```
**IMPORTANT**: Do NOT include an Implementation Plan when creating a task. The plan is added only after you start implementation.
- Creation phase: provide Title, Description, Acceptance Criteria, and optionally labels/priority/assignee.
- When you begin work, switch to edit and add the plan: `backlog task edit <id> --plan "..."`.
- Add Implementation Notes only after completing the work: `backlog task edit <id> --notes "..."`.
Phase discipline: What goes where
- Creation: Title, Description, Acceptance Criteria, labels/priority/assignee.
- Implementation: Implementation Plan (after moving to In Progress).
- Wrap-up: Implementation Notes, AC and Definition of Done checks.
**IMPORTANT**: Only implement what's in the Acceptance Criteria. If you need to do more, either:
1. Update the AC first: `backlog task edit 42 --ac "New requirement"`
2. Or create a new task: `backlog task create "Additional feature"`
---
## 6. Typical Workflow
```bash
# 1. Identify work
backlog task list -s "To Do" --plain
# 2. Read task details
backlog task 42 --plain
# 3. Start work: assign yourself & change status
backlog task edit 42 -a @myself -s "In Progress"
# 4. Add implementation plan
backlog task edit 42 --plan "1. Analyze\n2. Refactor\n3. Test"
# 5. Work on the task (write code, test, etc.)
# 6. Mark acceptance criteria as complete (supports multiple in one command)
backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check all at once
# Or check them individually if preferred:
# backlog task edit 42 --check-ac 1
# backlog task edit 42 --check-ac 2
# backlog task edit 42 --check-ac 3
# 7. Add implementation notes
backlog task edit 42 --notes "Refactored using strategy pattern, updated tests"
# 8. Mark task as done
backlog task edit 42 -s Done
```
---
## 7. Definition of Done (DoD)
A task is **Done** only when **ALL** of the following are complete:
### ✅ Via CLI Commands:
1. **All acceptance criteria checked**: Use `backlog task edit <id> --check-ac <index>` for each
2. **Implementation notes added**: Use `backlog task edit <id> --notes "..."`
3. **Status set to Done**: Use `backlog task edit <id> -s Done`
### ✅ Via Code/Testing:
4. **Tests pass**: Run test suite and linting
5. **Documentation updated**: Update relevant docs if needed
6. **Code reviewed**: Self-review your changes
7. **No regressions**: Performance, security checks pass
⚠️ **NEVER mark a task as Done without completing ALL items above**
---
## 8. Quick Reference: DO vs DON'T
### Viewing Tasks
| Task | ✅ DO | ❌ DON'T |
|------|-------|----------|
| View task | `backlog task 42 --plain` | Open and read .md file directly |
| List tasks | `backlog task list --plain` | Browse backlog/tasks folder |
| Check status | `backlog task 42 --plain` | Look at file content |
### Modifying Tasks
| Task | ✅ DO | ❌ DON'T |
|------|-------|----------|
| Check AC | `backlog task edit 42 --check-ac 1` | Change `- [ ]` to `- [x]` in file |
| Add notes | `backlog task edit 42 --notes "..."` | Type notes into .md file |
| Change status | `backlog task edit 42 -s Done` | Edit status in frontmatter |
| Add AC | `backlog task edit 42 --ac "New"` | Add `- [ ] New` to file |
---
## 9. Complete CLI Command Reference
### Task Creation
| Action | Command |
|--------|---------|
| Create task | `backlog task create "Title"` |
| With description | `backlog task create "Title" -d "Description"` |
| With AC | `backlog task create "Title" --ac "Criterion 1" --ac "Criterion 2"` |
| With all options | `backlog task create "Title" -d "Desc" -a @sara -s "To Do" -l auth --priority high` |
| Create draft | `backlog task create "Title" --draft` |
| Create subtask | `backlog task create "Title" -p 42` |
### Task Modification
| Action | Command |
|--------|---------|
| Edit title | `backlog task edit 42 -t "New Title"` |
| Edit description | `backlog task edit 42 -d "New description"` |
| Change status | `backlog task edit 42 -s "In Progress"` |
| Assign | `backlog task edit 42 -a @sara` |
| Add labels | `backlog task edit 42 -l backend,api` |
| Set priority | `backlog task edit 42 --priority high` |
### Acceptance Criteria Management
| Action | Command |
|--------|---------|
| Add AC | `backlog task edit 42 --ac "New criterion" --ac "Another"` |
| Remove AC #2 | `backlog task edit 42 --remove-ac 2` |
| Remove multiple ACs | `backlog task edit 42 --remove-ac 2 --remove-ac 4` |
| Check AC #1 | `backlog task edit 42 --check-ac 1` |
| Check multiple ACs | `backlog task edit 42 --check-ac 1 --check-ac 3` |
| Uncheck AC #3 | `backlog task edit 42 --uncheck-ac 3` |
| Mixed operations | `backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 --ac "New"` |
### Task Content
| Action | Command |
|--------|---------|
| Add plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` |
| Add notes | `backlog task edit 42 --notes "Implementation details"` |
| Add dependencies | `backlog task edit 42 --dep task-1 --dep task-2` |
### Task Operations
| Action | Command |
|--------|---------|
| View task | `backlog task 42 --plain` |
| List tasks | `backlog task list --plain` |
| Filter by status | `backlog task list -s "In Progress" --plain` |
| Filter by assignee | `backlog task list -a @sara --plain` |
| Archive task | `backlog task archive 42` |
| Demote to draft | `backlog task demote 42` |
---
## 10. Troubleshooting
### If You Accidentally Edited a File Directly
1. **DON'T PANIC** - But don't save or commit
2. Revert the changes
3. Make changes properly via CLI
4. If already saved, the metadata might be out of sync - use `backlog task edit` to fix
### Common Issues
| Problem | Solution |
|---------|----------|
| "Task not found" | Check task ID with `backlog task list --plain` |
| AC won't check | Use correct index: `backlog task 42 --plain` to see AC numbers |
| Changes not saving | Ensure you're using CLI, not editing files |
| Metadata out of sync | Re-edit via CLI to fix: `backlog task edit 42 -s <current-status>` |
---
## Remember: The Golden Rule
**🎯 If you want to change ANYTHING in a task, use the `backlog task edit` command.**
**📖 Only READ task files directly, never WRITE to them.**
Full help available: `backlog --help`
# === BACKLOG.MD GUIDELINES END ===

View file

@ -13,7 +13,6 @@ on:
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/** - templates/**
- CHANGELOG.md - CHANGELOG.md
- backlog/**
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

View file

@ -16,7 +16,6 @@ on:
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/** - templates/**
- CHANGELOG.md - CHANGELOG.md
- backlog/**
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

400
CLAUDE.md
View file

@ -247,403 +247,3 @@ ### Project Information
- [Project Overview](.cursor/rules/project-overview.mdc) - High-level project structure - [Project Overview](.cursor/rules/project-overview.mdc) - High-level project structure
- [Technology Stack](.cursor/rules/technology-stack.mdc) - Detailed tech stack information - [Technology Stack](.cursor/rules/technology-stack.mdc) - Detailed tech stack information
- [Cursor Rules Guide](.cursor/rules/cursor_rules.mdc) - How to maintain cursor rules - [Cursor Rules Guide](.cursor/rules/cursor_rules.mdc) - How to maintain cursor rules
# === BACKLOG.MD GUIDELINES START ===
# Instructions for the usage of Backlog.md CLI Tool
## What is Backlog.md?
**Backlog.md is the complete project management system for this codebase.** It provides everything needed to manage tasks, track progress, and collaborate on development - all through a powerful CLI that operates on markdown files.
### Core Capabilities
**Task Management**: Create, edit, assign, prioritize, and track tasks with full metadata
**Acceptance Criteria**: Granular control with add/remove/check/uncheck by index
**Board Visualization**: Terminal-based Kanban board (`backlog board`) and web UI (`backlog browser`)
**Git Integration**: Automatic tracking of task states across branches
**Dependencies**: Task relationships and subtask hierarchies
**Documentation & Decisions**: Structured docs and architectural decision records
**Export & Reporting**: Generate markdown reports and board snapshots
**AI-Optimized**: `--plain` flag provides clean text output for AI processing
### Why This Matters to You (AI Agent)
1. **Comprehensive system** - Full project management capabilities through CLI
2. **The CLI is the interface** - All operations go through `backlog` commands
3. **Unified interaction model** - You can use CLI for both reading (`backlog task 1 --plain`) and writing (`backlog task edit 1`)
4. **Metadata stays synchronized** - The CLI handles all the complex relationships
### Key Understanding
- **Tasks** live in `backlog/tasks/` as `task-<id> - <title>.md` files
- **You interact via CLI only**: `backlog task create`, `backlog task edit`, etc.
- **Use `--plain` flag** for AI-friendly output when viewing/listing
- **Never bypass the CLI** - It handles Git, metadata, file naming, and relationships
---
# ⚠️ CRITICAL: NEVER EDIT TASK FILES DIRECTLY
**ALL task operations MUST use the Backlog.md CLI commands**
- ✅ **DO**: Use `backlog task edit` and other CLI commands
- ✅ **DO**: Use `backlog task create` to create new tasks
- ✅ **DO**: Use `backlog task edit <id> --check-ac <index>` to mark acceptance criteria
- ❌ **DON'T**: Edit markdown files directly
- ❌ **DON'T**: Manually change checkboxes in files
- ❌ **DON'T**: Add or modify text in task files without using CLI
**Why?** Direct file editing breaks metadata synchronization, Git tracking, and task relationships.
---
## 1. Source of Truth & File Structure
### 📖 **UNDERSTANDING** (What you'll see when reading)
- Markdown task files live under **`backlog/tasks/`** (drafts under **`backlog/drafts/`**)
- Files are named: `task-<id> - <title>.md` (e.g., `task-42 - Add GraphQL resolver.md`)
- Project documentation is in **`backlog/docs/`**
- Project decisions are in **`backlog/decisions/`**
### 🔧 **ACTING** (How to change things)
- **All task operations MUST use the Backlog.md CLI tool**
- This ensures metadata is correctly updated and the project stays in sync
- **Always use `--plain` flag** when listing or viewing tasks for AI-friendly text output
---
## 2. Common Mistakes to Avoid
### ❌ **WRONG: Direct File Editing**
```markdown
# DON'T DO THIS:
1. Open backlog/tasks/task-7 - Feature.md in editor
2. Change "- [ ]" to "- [x]" manually
3. Add notes directly to the file
4. Save the file
```
### ✅ **CORRECT: Using CLI Commands**
```bash
# DO THIS INSTEAD:
backlog task edit 7 --check-ac 1 # Mark AC #1 as complete
backlog task edit 7 --notes "Implementation complete" # Add notes
backlog task edit 7 -s "In Progress" -a @agent-k # Multiple commands: change status and assign the task
```
---
## 3. Understanding Task Format (Read-Only Reference)
⚠️ **FORMAT REFERENCE ONLY** - The following sections show what you'll SEE in task files.
**Never edit these directly! Use CLI commands to make changes.**
### Task Structure You'll See
```markdown
---
id: task-42
title: Add GraphQL resolver
status: To Do
assignee: [@sara]
labels: [backend, api]
---
## Description
Brief explanation of the task purpose.
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 First criterion
- [x] #2 Second criterion (completed)
- [ ] #3 Third criterion
<!-- AC:END -->
## Implementation Plan
1. Research approach
2. Implement solution
## Implementation Notes
Summary of what was done.
```
### How to Modify Each Section
| What You Want to Change | CLI Command to Use |
|------------------------|-------------------|
| Title | `backlog task edit 42 -t "New Title"` |
| Status | `backlog task edit 42 -s "In Progress"` |
| Assignee | `backlog task edit 42 -a @sara` |
| Labels | `backlog task edit 42 -l backend,api` |
| Description | `backlog task edit 42 -d "New description"` |
| Add AC | `backlog task edit 42 --ac "New criterion"` |
| Check AC #1 | `backlog task edit 42 --check-ac 1` |
| Uncheck AC #2 | `backlog task edit 42 --uncheck-ac 2` |
| Remove AC #3 | `backlog task edit 42 --remove-ac 3` |
| Add Plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` |
| Add Notes | `backlog task edit 42 --notes "What I did"` |
---
## 4. Defining Tasks
### Creating New Tasks
**Always use CLI to create tasks:**
```bash
backlog task create "Task title" -d "Description" --ac "First criterion" --ac "Second criterion"
```
### Title (one liner)
Use a clear brief title that summarizes the task.
### Description (The "why")
Provide a concise summary of the task purpose and its goal. Explains the context without implementation details.
### Acceptance Criteria (The "what")
**Understanding the Format:**
- Acceptance criteria appear as numbered checkboxes in the markdown files
- Format: `- [ ] #1 Criterion text` (unchecked) or `- [x] #1 Criterion text` (checked)
**Managing Acceptance Criteria via CLI:**
⚠️ **IMPORTANT: How AC Commands Work**
- **Adding criteria (`--ac`)** accepts multiple flags: `--ac "First" --ac "Second"`
- **Checking/unchecking/removing** accept multiple flags too: `--check-ac 1 --check-ac 2`
- **Mixed operations** work in a single command: `--check-ac 1 --uncheck-ac 2 --remove-ac 3`
```bash
# Add new criteria (MULTIPLE values allowed)
backlog task edit 42 --ac "User can login" --ac "Session persists"
# Check specific criteria by index (MULTIPLE values supported)
backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check multiple ACs
# Or check them individually if you prefer:
backlog task edit 42 --check-ac 1 # Mark #1 as complete
backlog task edit 42 --check-ac 2 # Mark #2 as complete
# Mixed operations in single command
backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3
# ❌ STILL WRONG - These formats don't work:
# backlog task edit 42 --check-ac 1,2,3 # No comma-separated values
# backlog task edit 42 --check-ac 1-3 # No ranges
# backlog task edit 42 --check 1 # Wrong flag name
# Multiple operations of same type
backlog task edit 42 --uncheck-ac 1 --uncheck-ac 2 # Uncheck multiple ACs
backlog task edit 42 --remove-ac 2 --remove-ac 4 # Remove multiple ACs (processed high-to-low)
```
**Key Principles for Good ACs:**
- **Outcome-Oriented:** Focus on the result, not the method
- **Testable/Verifiable:** Each criterion should be objectively testable
- **Clear and Concise:** Unambiguous language
- **Complete:** Collectively cover the task scope
- **User-Focused:** Frame from end-user or system behavior perspective
Good Examples:
- "User can successfully log in with valid credentials"
- "System processes 1000 requests per second without errors"
Bad Example (Implementation Step):
- "Add a new function handleLogin() in auth.ts"
### Task Breakdown Strategy
1. Identify foundational components first
2. Create tasks in dependency order (foundations before features)
3. Ensure each task delivers value independently
4. Avoid creating tasks that block each other
### Task Requirements
- Tasks must be **atomic** and **testable** or **verifiable**
- Each task should represent a single unit of work for one PR
- **Never** reference future tasks (only tasks with id < current task id)
- Ensure tasks are **independent** and don't depend on future work
---
## 5. Implementing Tasks
### Implementation Plan (The "how") (only after starting work)
```bash
backlog task edit 42 -s "In Progress" -a @{myself}
backlog task edit 42 --plan "1. Research patterns\n2. Implement\n3. Test"
```
### Implementation Notes (Imagine you need to copy paste this into a PR description)
```bash
backlog task edit 42 --notes "Implemented using pattern X, modified files Y and Z"
```
**IMPORTANT**: Do NOT include an Implementation Plan when creating a task. The plan is added only after you start implementation.
- Creation phase: provide Title, Description, Acceptance Criteria, and optionally labels/priority/assignee.
- When you begin work, switch to edit and add the plan: `backlog task edit <id> --plan "..."`.
- Add Implementation Notes only after completing the work: `backlog task edit <id> --notes "..."`.
Phase discipline: What goes where
- Creation: Title, Description, Acceptance Criteria, labels/priority/assignee.
- Implementation: Implementation Plan (after moving to In Progress).
- Wrap-up: Implementation Notes, AC and Definition of Done checks.
**IMPORTANT**: Only implement what's in the Acceptance Criteria. If you need to do more, either:
1. Update the AC first: `backlog task edit 42 --ac "New requirement"`
2. Or create a new task: `backlog task create "Additional feature"`
---
## 6. Typical Workflow
```bash
# 1. Identify work
backlog task list -s "To Do" --plain
# 2. Read task details
backlog task 42 --plain
# 3. Start work: assign yourself & change status
backlog task edit 42 -a @myself -s "In Progress"
# 4. Add implementation plan
backlog task edit 42 --plan "1. Analyze\n2. Refactor\n3. Test"
# 5. Work on the task (write code, test, etc.)
# 6. Mark acceptance criteria as complete (supports multiple in one command)
backlog task edit 42 --check-ac 1 --check-ac 2 --check-ac 3 # Check all at once
# Or check them individually if preferred:
# backlog task edit 42 --check-ac 1
# backlog task edit 42 --check-ac 2
# backlog task edit 42 --check-ac 3
# 7. Add implementation notes
backlog task edit 42 --notes "Refactored using strategy pattern, updated tests"
# 8. Mark task as done
backlog task edit 42 -s Done
```
---
## 7. Definition of Done (DoD)
A task is **Done** only when **ALL** of the following are complete:
### ✅ Via CLI Commands:
1. **All acceptance criteria checked**: Use `backlog task edit <id> --check-ac <index>` for each
2. **Implementation notes added**: Use `backlog task edit <id> --notes "..."`
3. **Status set to Done**: Use `backlog task edit <id> -s Done`
### ✅ Via Code/Testing:
4. **Tests pass**: Run test suite and linting
5. **Documentation updated**: Update relevant docs if needed
6. **Code reviewed**: Self-review your changes
7. **No regressions**: Performance, security checks pass
⚠️ **NEVER mark a task as Done without completing ALL items above**
---
## 8. Quick Reference: DO vs DON'T
### Viewing Tasks
| Task | ✅ DO | ❌ DON'T |
|------|-------|----------|
| View task | `backlog task 42 --plain` | Open and read .md file directly |
| List tasks | `backlog task list --plain` | Browse backlog/tasks folder |
| Check status | `backlog task 42 --plain` | Look at file content |
### Modifying Tasks
| Task | ✅ DO | ❌ DON'T |
|------|-------|----------|
| Check AC | `backlog task edit 42 --check-ac 1` | Change `- [ ]` to `- [x]` in file |
| Add notes | `backlog task edit 42 --notes "..."` | Type notes into .md file |
| Change status | `backlog task edit 42 -s Done` | Edit status in frontmatter |
| Add AC | `backlog task edit 42 --ac "New"` | Add `- [ ] New` to file |
---
## 9. Complete CLI Command Reference
### Task Creation
| Action | Command |
|--------|---------|
| Create task | `backlog task create "Title"` |
| With description | `backlog task create "Title" -d "Description"` |
| With AC | `backlog task create "Title" --ac "Criterion 1" --ac "Criterion 2"` |
| With all options | `backlog task create "Title" -d "Desc" -a @sara -s "To Do" -l auth --priority high` |
| Create draft | `backlog task create "Title" --draft` |
| Create subtask | `backlog task create "Title" -p 42` |
### Task Modification
| Action | Command |
|--------|---------|
| Edit title | `backlog task edit 42 -t "New Title"` |
| Edit description | `backlog task edit 42 -d "New description"` |
| Change status | `backlog task edit 42 -s "In Progress"` |
| Assign | `backlog task edit 42 -a @sara` |
| Add labels | `backlog task edit 42 -l backend,api` |
| Set priority | `backlog task edit 42 --priority high` |
### Acceptance Criteria Management
| Action | Command |
|--------|---------|
| Add AC | `backlog task edit 42 --ac "New criterion" --ac "Another"` |
| Remove AC #2 | `backlog task edit 42 --remove-ac 2` |
| Remove multiple ACs | `backlog task edit 42 --remove-ac 2 --remove-ac 4` |
| Check AC #1 | `backlog task edit 42 --check-ac 1` |
| Check multiple ACs | `backlog task edit 42 --check-ac 1 --check-ac 3` |
| Uncheck AC #3 | `backlog task edit 42 --uncheck-ac 3` |
| Mixed operations | `backlog task edit 42 --check-ac 1 --uncheck-ac 2 --remove-ac 3 --ac "New"` |
### Task Content
| Action | Command |
|--------|---------|
| Add plan | `backlog task edit 42 --plan "1. Step one\n2. Step two"` |
| Add notes | `backlog task edit 42 --notes "Implementation details"` |
| Add dependencies | `backlog task edit 42 --dep task-1 --dep task-2` |
### Task Operations
| Action | Command |
|--------|---------|
| View task | `backlog task 42 --plain` |
| List tasks | `backlog task list --plain` |
| Filter by status | `backlog task list -s "In Progress" --plain` |
| Filter by assignee | `backlog task list -a @sara --plain` |
| Archive task | `backlog task archive 42` |
| Demote to draft | `backlog task demote 42` |
---
## 10. Troubleshooting
### If You Accidentally Edited a File Directly
1. **DON'T PANIC** - But don't save or commit
2. Revert the changes
3. Make changes properly via CLI
4. If already saved, the metadata might be out of sync - use `backlog task edit` to fix
### Common Issues
| Problem | Solution |
|---------|----------|
| "Task not found" | Check task ID with `backlog task list --plain` |
| AC won't check | Use correct index: `backlog task 42 --plain` to see AC numbers |
| Changes not saving | Ensure you're using CLI, not editing files |
| Metadata out of sync | Re-edit via CLI to fix: `backlog task edit 42 -s <current-status>` |
---
## Remember: The Golden Rule
**🎯 If you want to change ANYTHING in a task, use the `backlog task edit` command.**
**📖 Only READ task files directly, never WRITE to them.**
Full help available: `backlog --help`
# === BACKLOG.MD GUIDELINES END ===

View file

@ -99,8 +99,12 @@ public function handle(StandaloneClickhouse $database)
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";

View file

@ -52,8 +52,9 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
} }
$configuration_dir = database_proxy_dir($database->uuid); $configuration_dir = database_proxy_dir($database->uuid);
$volume_configuration_dir = $configuration_dir;
if (isDev()) { if (isDev()) {
$configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy'; $volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy';
} }
$nginxconf = <<<EOF $nginxconf = <<<EOF
user nginx; user nginx;
@ -86,7 +87,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
'volumes' => [ 'volumes' => [
[ [
'type' => 'bind', 'type' => 'bind',
'source' => "$configuration_dir/nginx.conf", 'source' => "$volume_configuration_dir/nginx.conf",
'target' => '/etc/nginx/nginx.conf', 'target' => '/etc/nginx/nginx.conf',
], ],
], ],
@ -115,8 +116,18 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
instant_remote_process(["docker rm -f $proxyContainerName"], $server, false); instant_remote_process(["docker rm -f $proxyContainerName"], $server, false);
instant_remote_process([ instant_remote_process([
"mkdir -p $configuration_dir", "mkdir -p $configuration_dir",
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null", [
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null", 'transfer_file' => [
'content' => base64_decode($nginxconf_base64),
'destination' => "$configuration_dir/nginx.conf",
],
],
[
'transfer_file' => [
'content' => base64_decode($dockercompose_base64),
'destination' => "$configuration_dir/docker-compose.yaml",
],
],
"docker compose --project-directory {$configuration_dir} pull", "docker compose --project-directory {$configuration_dir} pull",
"docker compose --project-directory {$configuration_dir} up -d", "docker compose --project-directory {$configuration_dir} up -d",
], $server); ], $server);

View file

@ -183,8 +183,12 @@ public function handle(StandaloneDragonfly $database)
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";

View file

@ -199,8 +199,12 @@ public function handle(StandaloneKeydb $database)
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";

View file

@ -203,8 +203,12 @@ public function handle(StandaloneMariadb $database)
} }
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
@ -284,7 +288,11 @@ private function add_custom_mysql()
} }
$filename = 'custom-config.cnf'; $filename = 'custom-config.cnf';
$content = $this->database->mariadb_conf; $content = $this->database->mariadb_conf;
$content_base64 = base64_encode($content); $this->commands[] = [
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; 'transfer_file' => [
'content' => $content,
'destination' => "$this->configuration_dir/{$filename}",
],
];
} }
} }

View file

@ -18,6 +18,8 @@ class StartMongodb
public string $configuration_dir; public string $configuration_dir;
public string $volume_configuration_dir;
private ?SslCertificate $ssl_certificate = null; private ?SslCertificate $ssl_certificate = null;
public function handle(StandaloneMongodb $database) public function handle(StandaloneMongodb $database)
@ -27,9 +29,9 @@ public function handle(StandaloneMongodb $database)
$startCommand = 'mongod'; $startCommand = 'mongod';
$container_name = $this->database->uuid; $container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->volume_configuration_dir = $this->configuration_dir = database_configuration_dir().'/'.$container_name;
if (isDev()) { if (isDev()) {
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; $this->volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
} }
$this->commands = [ $this->commands = [
@ -176,7 +178,7 @@ public function handle(StandaloneMongodb $database)
$docker_compose['services'][$container_name]['volumes'] ?? [], $docker_compose['services'][$container_name]['volumes'] ?? [],
[[ [[
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir.'/mongod.conf', 'source' => $this->volume_configuration_dir.'/mongod.conf',
'target' => '/etc/mongo/mongod.conf', 'target' => '/etc/mongo/mongod.conf',
'read_only' => true, 'read_only' => true,
]] ]]
@ -190,7 +192,7 @@ public function handle(StandaloneMongodb $database)
$docker_compose['services'][$container_name]['volumes'] ?? [], $docker_compose['services'][$container_name]['volumes'] ?? [],
[[ [[
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d', 'source' => $this->volume_configuration_dir.'/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d', 'target' => '/docker-entrypoint-initdb.d',
'read_only' => true, 'read_only' => true,
]] ]]
@ -254,8 +256,12 @@ public function handle(StandaloneMongodb $database)
} }
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->volume_configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
@ -332,15 +338,22 @@ private function add_custom_mongo_conf()
} }
$filename = 'mongod.conf'; $filename = 'mongod.conf';
$content = $this->database->mongo_conf; $content = $this->database->mongo_conf;
$content_base64 = base64_encode($content); $this->commands[] = [
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; 'transfer_file' => [
'content' => $content,
'destination' => "$this->configuration_dir/{$filename}",
],
];
} }
private function add_default_database() private function add_default_database()
{ {
$content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});"; $content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});";
$content_base64 = base64_encode($content); $this->commands[] = [
$this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d"; 'transfer_file' => [
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js > /dev/null"; 'content' => $content,
'destination' => "$this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js",
],
];
} }
} }

View file

@ -204,8 +204,12 @@ public function handle(StandaloneMysql $database)
} }
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
@ -287,7 +291,11 @@ private function add_custom_mysql()
} }
$filename = 'custom-config.cnf'; $filename = 'custom-config.cnf';
$content = $this->database->mysql_conf; $content = $this->database->mysql_conf;
$content_base64 = base64_encode($content); $this->commands[] = [
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; 'transfer_file' => [
'content' => $content,
'destination' => "$this->configuration_dir/{$filename}",
],
];
} }
} }

View file

@ -20,6 +20,8 @@ class StartPostgresql
public string $configuration_dir; public string $configuration_dir;
public string $volume_configuration_dir;
private ?SslCertificate $ssl_certificate = null; private ?SslCertificate $ssl_certificate = null;
public function handle(StandalonePostgresql $database) public function handle(StandalonePostgresql $database)
@ -27,8 +29,9 @@ public function handle(StandalonePostgresql $database)
$this->database = $database; $this->database = $database;
$container_name = $this->database->uuid; $container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->volume_configuration_dir = $this->configuration_dir;
if (isDev()) { if (isDev()) {
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; $this->volume_configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
} }
$this->commands = [ $this->commands = [
@ -192,7 +195,7 @@ public function handle(StandalonePostgresql $database)
$docker_compose['services'][$container_name]['volumes'], $docker_compose['services'][$container_name]['volumes'],
[[ [[
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir.'/custom-postgres.conf', 'source' => $this->volume_configuration_dir.'/custom-postgres.conf',
'target' => '/etc/postgresql/postgresql.conf', 'target' => '/etc/postgresql/postgresql.conf',
'read_only' => true, 'read_only' => true,
]] ]]
@ -217,8 +220,12 @@ public function handle(StandalonePostgresql $database)
} }
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->volume_configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
@ -302,8 +309,12 @@ private function generate_init_scripts()
foreach ($this->database->init_scripts as $init_script) { foreach ($this->database->init_scripts as $init_script) {
$filename = data_get($init_script, 'filename'); $filename = data_get($init_script, 'filename');
$content = data_get($init_script, 'content'); $content = data_get($init_script, 'content');
$content_base64 = base64_encode($content); $this->commands[] = [
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/{$filename} > /dev/null"; 'transfer_file' => [
'content' => $content,
'destination' => "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}",
],
];
$this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}"; $this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}";
} }
} }
@ -325,7 +336,11 @@ private function add_custom_conf()
$this->database->postgres_conf = $content; $this->database->postgres_conf = $content;
$this->database->save(); $this->database->save();
} }
$content_base64 = base64_encode($content); $this->commands[] = [
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $config_file_path > /dev/null"; 'transfer_file' => [
'content' => $content,
'destination' => $config_file_path,
],
];
} }
} }

View file

@ -196,8 +196,12 @@ public function handle(StandaloneRedis $database)
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = [
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; 'transfer_file' => [
'content' => $docker_compose,
'destination' => "$this->configuration_dir/docker-compose.yml",
],
];
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";

View file

@ -40,7 +40,7 @@ public function create(array $input): User
$user = User::create([ $user = User::create([
'id' => 0, 'id' => 0,
'name' => $input['name'], 'name' => $input['name'],
'email' => strtolower($input['email']), 'email' => $input['email'],
'password' => Hash::make($input['password']), 'password' => Hash::make($input['password']),
]); ]);
$team = $user->teams()->first(); $team = $user->teams()->first();
@ -52,7 +52,7 @@ public function create(array $input): User
} else { } else {
$user = User::create([ $user = User::create([
'name' => $input['name'], 'name' => $input['name'],
'email' => strtolower($input['email']), 'email' => $input['email'],
'password' => Hash::make($input['password']), 'password' => Hash::make($input['password']),
]); ]);
$team = $user->teams()->first(); $team = $user->teams()->first();

View file

@ -1,36 +0,0 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use App\Services\ProxyDashboardCacheService;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckConfiguration
{
use AsAction;
public function handle(Server $server, bool $reset = false)
{
$proxyType = $server->proxyType();
if ($proxyType === 'NONE') {
return 'OK';
}
$proxy_path = $server->proxyPath();
$payload = [
"mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml",
];
$proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value();
}
if (! $proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception('Could not generate proxy configuration');
}
ProxyDashboardCacheService::isTraefikDashboardAvailableFromConfiguration($server, $proxy_configuration);
return $proxy_configuration;
}
}

View file

@ -70,7 +70,7 @@ public function handle(Server $server, $fromUI = false): bool
try { try {
if ($server->proxyType() !== ProxyTypes::NONE->value) { if ($server->proxyType() !== ProxyTypes::NONE->value) {
$proxyCompose = CheckConfiguration::run($server); $proxyCompose = GetProxyConfiguration::run($server);
if (isset($proxyCompose)) { if (isset($proxyCompose)) {
$yaml = Yaml::parse($proxyCompose); $yaml = Yaml::parse($proxyCompose);
$configPorts = []; $configPorts = [];

View file

@ -0,0 +1,47 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use App\Services\ProxyDashboardCacheService;
use Lorisleiva\Actions\Concerns\AsAction;
class GetProxyConfiguration
{
use AsAction;
public function handle(Server $server, bool $forceRegenerate = false): string
{
$proxyType = $server->proxyType();
if ($proxyType === 'NONE') {
return 'OK';
}
$proxy_path = $server->proxyPath();
$proxy_configuration = null;
// If not forcing regeneration, try to read existing configuration
if (! $forceRegenerate) {
$payload = [
"mkdir -p $proxy_path",
"cat $proxy_path/docker-compose.yml 2>/dev/null",
];
$proxy_configuration = instant_remote_process($payload, $server, false);
}
// Generate default configuration if:
// 1. Force regenerate is requested
// 2. Configuration file doesn't exist or is empty
if ($forceRegenerate || empty(trim($proxy_configuration ?? ''))) {
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value();
}
if (empty($proxy_configuration)) {
throw new \Exception('Could not get or generate proxy configuration');
}
ProxyDashboardCacheService::isTraefikDashboardAvailableFromConfiguration($server, $proxy_configuration);
return $proxy_configuration;
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class SaveConfiguration
{
use AsAction;
public function handle(Server $server, ?string $proxy_settings = null)
{
if (is_null($proxy_settings)) {
$proxy_settings = CheckConfiguration::run($server, true);
}
$proxy_path = $server->proxyPath();
$docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
return instant_remote_process([
"mkdir -p $proxy_path",
"echo '$docker_compose_yml_base64' | base64 -d | tee $proxy_path/docker-compose.yml > /dev/null",
], $server);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class SaveProxyConfiguration
{
use AsAction;
public function handle(Server $server, string $configuration): void
{
$proxy_path = $server->proxyPath();
$docker_compose_yml_base64 = base64_encode($configuration);
// Update the saved settings hash
$server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
// Transfer the configuration file to the server
instant_remote_process([
"mkdir -p $proxy_path",
[
'transfer_file' => [
'content' => base64_decode($docker_compose_yml_base64),
'destination' => "$proxy_path/docker-compose.yml",
],
],
], $server);
}
}

View file

@ -21,11 +21,11 @@ public function handle(Server $server, bool $async = true, bool $force = false):
} }
$commands = collect([]); $commands = collect([]);
$proxy_path = $server->proxyPath(); $proxy_path = $server->proxyPath();
$configuration = CheckConfiguration::run($server); $configuration = GetProxyConfiguration::run($server);
if (! $configuration) { if (! $configuration) {
throw new \Exception('Configuration is not synced'); throw new \Exception('Configuration is not synced');
} }
SaveConfiguration::run($server, $configuration); SaveProxyConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
$server->save(); $server->save();

View file

@ -102,7 +102,6 @@ public function handle(Server $server)
]; ];
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray('Error:', $e->getMessage());
return [ return [
'osId' => $osId, 'osId' => $osId,

View file

@ -40,7 +40,12 @@ public function handle(Server $server, string $cloudflare_token, string $ssh_dom
$commands = collect([ $commands = collect([
'mkdir -p /tmp/cloudflared', 'mkdir -p /tmp/cloudflared',
'cd /tmp/cloudflared', 'cd /tmp/cloudflared',
"echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null", [
'transfer_file' => [
'content' => base64_decode($docker_compose_yml_base64),
'destination' => '/tmp/cloudflared/docker-compose.yml',
],
],
'echo Pulling latest Cloudflare Tunnel image.', 'echo Pulling latest Cloudflare Tunnel image.',
'docker compose pull', 'docker compose pull',
'echo Stopping existing Cloudflare Tunnel container.', 'echo Stopping existing Cloudflare Tunnel container.',

View file

@ -14,6 +14,7 @@ class InstallDocker
public function handle(Server $server) public function handle(Server $server)
{ {
ray('install docker');
$dockerVersion = config('constants.docker.minimum_required_version'); $dockerVersion = config('constants.docker.minimum_required_version');
$supported_os_type = $server->validateOS(); $supported_os_type = $server->validateOS();
if (! $supported_os_type) { if (! $supported_os_type) {
@ -103,8 +104,15 @@ public function handle(Server $server)
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}", "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}",
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"', 'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"',
"test ! -s /etc/docker/daemon.json && echo '{$config}' | base64 -d | tee /etc/docker/daemon.json > /dev/null", [
"echo '{$config}' | base64 -d | tee /etc/docker/daemon.json.coolify > /dev/null", 'transfer_file' => [
'content' => base64_decode($config),
'destination' => '/tmp/daemon.json.new',
],
],
'test ! -s /etc/docker/daemon.json && cp /tmp/daemon.json.new /etc/docker/daemon.json',
'cp /tmp/daemon.json.new /etc/docker/daemon.json.coolify',
'rm -f /tmp/daemon.json.new',
'jq . /etc/docker/daemon.json.coolify | tee /etc/docker/daemon.json.coolify.pretty > /dev/null', 'jq . /etc/docker/daemon.json.coolify | tee /etc/docker/daemon.json.coolify.pretty > /dev/null',
'mv /etc/docker/daemon.json.coolify.pretty /etc/docker/daemon.json.coolify', 'mv /etc/docker/daemon.json.coolify.pretty /etc/docker/daemon.json.coolify',
"jq -s '.[0] * .[1]' /etc/docker/daemon.json.coolify /etc/docker/daemon.json | tee /etc/docker/daemon.json.appended > /dev/null", "jq -s '.[0] * .[1]' /etc/docker/daemon.json.coolify /etc/docker/daemon.json | tee /etc/docker/daemon.json.appended > /dev/null",

View file

@ -180,10 +180,30 @@ public function handle(Server $server)
$command = [ $command = [
"echo 'Saving configuration'", "echo 'Saving configuration'",
"mkdir -p $config_path", "mkdir -p $config_path",
"echo '{$parsers}' | base64 -d | tee $parsers_config > /dev/null", [
"echo '{$config}' | base64 -d | tee $fluent_bit_config > /dev/null", 'transfer_file' => [
"echo '{$compose}' | base64 -d | tee $compose_path > /dev/null", 'content' => base64_decode($parsers),
"echo '{$readme}' | base64 -d | tee $readme_path > /dev/null", 'destination' => $parsers_config,
],
],
[
'transfer_file' => [
'content' => base64_decode($config),
'destination' => $fluent_bit_config,
],
],
[
'transfer_file' => [
'content' => base64_decode($compose),
'destination' => $compose_path,
],
],
[
'transfer_file' => [
'content' => base64_decode($readme),
'destination' => $readme_path,
],
],
"test -f $config_path/.env && rm $config_path/.env", "test -f $config_path/.env && rm $config_path/.env",
]; ];
if ($type === 'newrelic') { if ($type === 'newrelic') {

View file

@ -3,6 +3,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob; use App\Jobs\CleanupHelperContainersJob;
use App\Jobs\DeleteResourceJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
@ -72,7 +73,7 @@ private function cleanup_stucked_resources()
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); $applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) { foreach ($applications as $application) {
echo "Deleting stuck application: {$application->name}\n"; echo "Deleting stuck application: {$application->name}\n";
$application->forceDelete(); DeleteResourceJob::dispatch($application);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n"; echo "Error in cleaning stuck application: {$e->getMessage()}\n";
@ -82,26 +83,35 @@ private function cleanup_stucked_resources()
foreach ($applicationsPreviews as $applicationPreview) { foreach ($applicationsPreviews as $applicationPreview) {
if (! data_get($applicationPreview, 'application')) { if (! data_get($applicationPreview, 'application')) {
echo "Deleting stuck application preview: {$applicationPreview->uuid}\n"; echo "Deleting stuck application preview: {$applicationPreview->uuid}\n";
$applicationPreview->delete(); DeleteResourceJob::dispatch($applicationPreview);
} }
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n"; echo "Error in cleaning stuck application: {$e->getMessage()}\n";
} }
try {
$applicationsPreviews = ApplicationPreview::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applicationsPreviews as $applicationPreview) {
echo "Deleting stuck application preview: {$applicationPreview->fqdn}\n";
DeleteResourceJob::dispatch($applicationPreview);
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
}
try { try {
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get(); $postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($postgresqls as $postgresql) { foreach ($postgresqls as $postgresql) {
echo "Deleting stuck postgresql: {$postgresql->name}\n"; echo "Deleting stuck postgresql: {$postgresql->name}\n";
$postgresql->forceDelete(); DeleteResourceJob::dispatch($postgresql);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n"; echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
} }
try { try {
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get(); $rediss = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($redis as $redis) { foreach ($rediss as $redis) {
echo "Deleting stuck redis: {$redis->name}\n"; echo "Deleting stuck redis: {$redis->name}\n";
$redis->forceDelete(); DeleteResourceJob::dispatch($redis);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck redis: {$e->getMessage()}\n"; echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
@ -110,7 +120,7 @@ private function cleanup_stucked_resources()
$keydbs = StandaloneKeydb::withTrashed()->whereNotNull('deleted_at')->get(); $keydbs = StandaloneKeydb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($keydbs as $keydb) { foreach ($keydbs as $keydb) {
echo "Deleting stuck keydb: {$keydb->name}\n"; echo "Deleting stuck keydb: {$keydb->name}\n";
$keydb->forceDelete(); DeleteResourceJob::dispatch($keydb);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck keydb: {$e->getMessage()}\n"; echo "Error in cleaning stuck keydb: {$e->getMessage()}\n";
@ -119,7 +129,7 @@ private function cleanup_stucked_resources()
$dragonflies = StandaloneDragonfly::withTrashed()->whereNotNull('deleted_at')->get(); $dragonflies = StandaloneDragonfly::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($dragonflies as $dragonfly) { foreach ($dragonflies as $dragonfly) {
echo "Deleting stuck dragonfly: {$dragonfly->name}\n"; echo "Deleting stuck dragonfly: {$dragonfly->name}\n";
$dragonfly->forceDelete(); DeleteResourceJob::dispatch($dragonfly);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck dragonfly: {$e->getMessage()}\n"; echo "Error in cleaning stuck dragonfly: {$e->getMessage()}\n";
@ -128,7 +138,7 @@ private function cleanup_stucked_resources()
$clickhouses = StandaloneClickhouse::withTrashed()->whereNotNull('deleted_at')->get(); $clickhouses = StandaloneClickhouse::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($clickhouses as $clickhouse) { foreach ($clickhouses as $clickhouse) {
echo "Deleting stuck clickhouse: {$clickhouse->name}\n"; echo "Deleting stuck clickhouse: {$clickhouse->name}\n";
$clickhouse->forceDelete(); DeleteResourceJob::dispatch($clickhouse);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck clickhouse: {$e->getMessage()}\n"; echo "Error in cleaning stuck clickhouse: {$e->getMessage()}\n";
@ -137,7 +147,7 @@ private function cleanup_stucked_resources()
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get(); $mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mongodbs as $mongodb) { foreach ($mongodbs as $mongodb) {
echo "Deleting stuck mongodb: {$mongodb->name}\n"; echo "Deleting stuck mongodb: {$mongodb->name}\n";
$mongodb->forceDelete(); DeleteResourceJob::dispatch($mongodb);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n"; echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
@ -146,7 +156,7 @@ private function cleanup_stucked_resources()
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get(); $mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mysqls as $mysql) { foreach ($mysqls as $mysql) {
echo "Deleting stuck mysql: {$mysql->name}\n"; echo "Deleting stuck mysql: {$mysql->name}\n";
$mysql->forceDelete(); DeleteResourceJob::dispatch($mysql);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n"; echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
@ -155,7 +165,7 @@ private function cleanup_stucked_resources()
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get(); $mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($mariadbs as $mariadb) { foreach ($mariadbs as $mariadb) {
echo "Deleting stuck mariadb: {$mariadb->name}\n"; echo "Deleting stuck mariadb: {$mariadb->name}\n";
$mariadb->forceDelete(); DeleteResourceJob::dispatch($mariadb);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n"; echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
@ -164,7 +174,7 @@ private function cleanup_stucked_resources()
$services = Service::withTrashed()->whereNotNull('deleted_at')->get(); $services = Service::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($services as $service) { foreach ($services as $service) {
echo "Deleting stuck service: {$service->name}\n"; echo "Deleting stuck service: {$service->name}\n";
$service->forceDelete(); DeleteResourceJob::dispatch($service);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stuck service: {$e->getMessage()}\n"; echo "Error in cleaning stuck service: {$e->getMessage()}\n";
@ -217,19 +227,19 @@ private function cleanup_stucked_resources()
foreach ($applications as $application) { foreach ($applications as $application) {
if (! data_get($application, 'environment')) { if (! data_get($application, 'environment')) {
echo 'Application without environment: '.$application->name.'\n'; echo 'Application without environment: '.$application->name.'\n';
$application->forceDelete(); DeleteResourceJob::dispatch($application);
continue; continue;
} }
if (! $application->destination()) { if (! $application->destination()) {
echo 'Application without destination: '.$application->name.'\n'; echo 'Application without destination: '.$application->name.'\n';
$application->forceDelete(); DeleteResourceJob::dispatch($application);
continue; continue;
} }
if (! data_get($application, 'destination.server')) { if (! data_get($application, 'destination.server')) {
echo 'Application without server: '.$application->name.'\n'; echo 'Application without server: '.$application->name.'\n';
$application->forceDelete(); DeleteResourceJob::dispatch($application);
continue; continue;
} }
@ -242,19 +252,19 @@ private function cleanup_stucked_resources()
foreach ($postgresqls as $postgresql) { foreach ($postgresqls as $postgresql) {
if (! data_get($postgresql, 'environment')) { if (! data_get($postgresql, 'environment')) {
echo 'Postgresql without environment: '.$postgresql->name.'\n'; echo 'Postgresql without environment: '.$postgresql->name.'\n';
$postgresql->forceDelete(); DeleteResourceJob::dispatch($postgresql);
continue; continue;
} }
if (! $postgresql->destination()) { if (! $postgresql->destination()) {
echo 'Postgresql without destination: '.$postgresql->name.'\n'; echo 'Postgresql without destination: '.$postgresql->name.'\n';
$postgresql->forceDelete(); DeleteResourceJob::dispatch($postgresql);
continue; continue;
} }
if (! data_get($postgresql, 'destination.server')) { if (! data_get($postgresql, 'destination.server')) {
echo 'Postgresql without server: '.$postgresql->name.'\n'; echo 'Postgresql without server: '.$postgresql->name.'\n';
$postgresql->forceDelete(); DeleteResourceJob::dispatch($postgresql);
continue; continue;
} }
@ -267,19 +277,19 @@ private function cleanup_stucked_resources()
foreach ($redis as $redis) { foreach ($redis as $redis) {
if (! data_get($redis, 'environment')) { if (! data_get($redis, 'environment')) {
echo 'Redis without environment: '.$redis->name.'\n'; echo 'Redis without environment: '.$redis->name.'\n';
$redis->forceDelete(); DeleteResourceJob::dispatch($redis);
continue; continue;
} }
if (! $redis->destination()) { if (! $redis->destination()) {
echo 'Redis without destination: '.$redis->name.'\n'; echo 'Redis without destination: '.$redis->name.'\n';
$redis->forceDelete(); DeleteResourceJob::dispatch($redis);
continue; continue;
} }
if (! data_get($redis, 'destination.server')) { if (! data_get($redis, 'destination.server')) {
echo 'Redis without server: '.$redis->name.'\n'; echo 'Redis without server: '.$redis->name.'\n';
$redis->forceDelete(); DeleteResourceJob::dispatch($redis);
continue; continue;
} }
@ -293,19 +303,19 @@ private function cleanup_stucked_resources()
foreach ($mongodbs as $mongodb) { foreach ($mongodbs as $mongodb) {
if (! data_get($mongodb, 'environment')) { if (! data_get($mongodb, 'environment')) {
echo 'Mongodb without environment: '.$mongodb->name.'\n'; echo 'Mongodb without environment: '.$mongodb->name.'\n';
$mongodb->forceDelete(); DeleteResourceJob::dispatch($mongodb);
continue; continue;
} }
if (! $mongodb->destination()) { if (! $mongodb->destination()) {
echo 'Mongodb without destination: '.$mongodb->name.'\n'; echo 'Mongodb without destination: '.$mongodb->name.'\n';
$mongodb->forceDelete(); DeleteResourceJob::dispatch($mongodb);
continue; continue;
} }
if (! data_get($mongodb, 'destination.server')) { if (! data_get($mongodb, 'destination.server')) {
echo 'Mongodb without server: '.$mongodb->name.'\n'; echo 'Mongodb without server: '.$mongodb->name.'\n';
$mongodb->forceDelete(); DeleteResourceJob::dispatch($mongodb);
continue; continue;
} }
@ -319,19 +329,19 @@ private function cleanup_stucked_resources()
foreach ($mysqls as $mysql) { foreach ($mysqls as $mysql) {
if (! data_get($mysql, 'environment')) { if (! data_get($mysql, 'environment')) {
echo 'Mysql without environment: '.$mysql->name.'\n'; echo 'Mysql without environment: '.$mysql->name.'\n';
$mysql->forceDelete(); DeleteResourceJob::dispatch($mysql);
continue; continue;
} }
if (! $mysql->destination()) { if (! $mysql->destination()) {
echo 'Mysql without destination: '.$mysql->name.'\n'; echo 'Mysql without destination: '.$mysql->name.'\n';
$mysql->forceDelete(); DeleteResourceJob::dispatch($mysql);
continue; continue;
} }
if (! data_get($mysql, 'destination.server')) { if (! data_get($mysql, 'destination.server')) {
echo 'Mysql without server: '.$mysql->name.'\n'; echo 'Mysql without server: '.$mysql->name.'\n';
$mysql->forceDelete(); DeleteResourceJob::dispatch($mysql);
continue; continue;
} }
@ -345,19 +355,19 @@ private function cleanup_stucked_resources()
foreach ($mariadbs as $mariadb) { foreach ($mariadbs as $mariadb) {
if (! data_get($mariadb, 'environment')) { if (! data_get($mariadb, 'environment')) {
echo 'Mariadb without environment: '.$mariadb->name.'\n'; echo 'Mariadb without environment: '.$mariadb->name.'\n';
$mariadb->forceDelete(); DeleteResourceJob::dispatch($mariadb);
continue; continue;
} }
if (! $mariadb->destination()) { if (! $mariadb->destination()) {
echo 'Mariadb without destination: '.$mariadb->name.'\n'; echo 'Mariadb without destination: '.$mariadb->name.'\n';
$mariadb->forceDelete(); DeleteResourceJob::dispatch($mariadb);
continue; continue;
} }
if (! data_get($mariadb, 'destination.server')) { if (! data_get($mariadb, 'destination.server')) {
echo 'Mariadb without server: '.$mariadb->name.'\n'; echo 'Mariadb without server: '.$mariadb->name.'\n';
$mariadb->forceDelete(); DeleteResourceJob::dispatch($mariadb);
continue; continue;
} }
@ -371,19 +381,19 @@ private function cleanup_stucked_resources()
foreach ($services as $service) { foreach ($services as $service) {
if (! data_get($service, 'environment')) { if (! data_get($service, 'environment')) {
echo 'Service without environment: '.$service->name.'\n'; echo 'Service without environment: '.$service->name.'\n';
$service->forceDelete(); DeleteResourceJob::dispatch($service);
continue; continue;
} }
if (! $service->destination()) { if (! $service->destination()) {
echo 'Service without destination: '.$service->name.'\n'; echo 'Service without destination: '.$service->name.'\n';
$service->forceDelete(); DeleteResourceJob::dispatch($service);
continue; continue;
} }
if (! data_get($service, 'server')) { if (! data_get($service, 'server')) {
echo 'Service without server: '.$service->name.'\n'; echo 'Service without server: '.$service->name.'\n';
$service->forceDelete(); DeleteResourceJob::dispatch($service);
continue; continue;
} }
@ -396,7 +406,7 @@ private function cleanup_stucked_resources()
foreach ($serviceApplications as $service) { foreach ($serviceApplications as $service) {
if (! data_get($service, 'service')) { if (! data_get($service, 'service')) {
echo 'ServiceApplication without service: '.$service->name.'\n'; echo 'ServiceApplication without service: '.$service->name.'\n';
$service->forceDelete(); DeleteResourceJob::dispatch($service);
continue; continue;
} }
@ -409,7 +419,7 @@ private function cleanup_stucked_resources()
foreach ($serviceDatabases as $service) { foreach ($serviceDatabases as $service) {
if (! data_get($service, 'service')) { if (! data_get($service, 'service')) {
echo 'ServiceDatabase without service: '.$service->name.'\n'; echo 'ServiceDatabase without service: '.$service->name.'\n';
$service->forceDelete(); DeleteResourceJob::dispatch($service);
continue; continue;
} }

View file

@ -5,9 +5,10 @@
use App\Enums\ActivityTypes; use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CheckHelperImageJob; use App\Jobs\CheckHelperImageJob;
use App\Jobs\PullChangelogFromGitHub; use App\Jobs\PullChangelog;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment; use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
@ -19,80 +20,18 @@
class Init extends Command class Init extends Command
{ {
protected $signature = 'app:init {--force-cloud}'; protected $signature = 'app:init';
protected $description = 'Cleanup instance related stuffs'; protected $description = 'Cleanup instance related stuffs';
public $servers = null; public $servers = null;
public InstanceSettings $settings;
public function handle() public function handle()
{ {
$this->optimize(); Artisan::call('optimize:clear');
Artisan::call('optimize');
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
return;
}
$this->servers = Server::all();
if (! isCloud()) {
$this->sendAliveSignal();
get_public_ips();
}
// Backward compatibility
$this->replaceSlashInEnvironmentName();
$this->restoreCoolifyDbBackup();
$this->updateUserEmails();
//
$this->updateTraefikLabels();
if (! isCloud() || $this->option('force-cloud')) {
$this->cleanupUnusedNetworkFromCoolifyProxy();
}
$this->call('cleanup:redis');
try {
$this->call('cleanup:names');
} catch (\Throwable $e) {
echo "Error in cleanup:names command: {$e->getMessage()}\n";
}
$this->call('cleanup:stucked-resources');
try {
$this->pullHelperImage();
} catch (\Throwable $e) {
//
}
if (isCloud()) {
try {
$this->cleanupInProgressApplicationDeployments();
} catch (\Throwable $e) {
echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n";
}
try {
$this->pullTemplatesFromCDN();
} catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
try {
$this->pullChangelogFromGitHub();
} catch (\Throwable $e) {
echo "Could not changelogs from github: {$e->getMessage()}\n";
}
return;
}
try {
$this->cleanupInProgressApplicationDeployments();
} catch (\Throwable $e) {
echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n";
}
try { try {
$this->pullTemplatesFromCDN(); $this->pullTemplatesFromCDN();
@ -105,20 +44,80 @@ public function handle()
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not changelogs from github: {$e->getMessage()}\n"; echo "Could not changelogs from github: {$e->getMessage()}\n";
} }
try {
$this->pullHelperImage();
} catch (\Throwable $e) {
echo "Error in pullHelperImage command: {$e->getMessage()}\n";
}
if (isCloud()) {
return;
}
$this->settings = instanceSettings();
$this->servers = Server::all();
$do_not_track = data_get($this->settings, 'do_not_track', true);
if ($do_not_track == false) {
$this->sendAliveSignal();
}
get_public_ips();
// Backward compatibility
$this->replaceSlashInEnvironmentName();
$this->restoreCoolifyDbBackup();
$this->updateUserEmails();
//
$this->updateTraefikLabels();
$this->cleanupUnusedNetworkFromCoolifyProxy();
try {
$this->call('cleanup:redis');
} catch (\Throwable $e) {
echo "Error in cleanup:redis command: {$e->getMessage()}\n";
}
try {
$this->call('cleanup:names');
} catch (\Throwable $e) {
echo "Error in cleanup:names command: {$e->getMessage()}\n";
}
try {
$this->call('cleanup:stucked-resources');
} catch (\Throwable $e) {
echo "Error in cleanup:stucked-resources command: {$e->getMessage()}\n";
}
try {
$updatedCount = ApplicationDeploymentQueue::whereIn('status', [
ApplicationDeploymentStatus::IN_PROGRESS->value,
ApplicationDeploymentStatus::QUEUED->value,
])->update([
'status' => ApplicationDeploymentStatus::FAILED->value,
]);
if ($updatedCount > 0) {
echo "Marked {$updatedCount} stuck deployments as failed\n";
}
} catch (\Throwable $e) {
echo "Could not cleanup inprogress deployments: {$e->getMessage()}\n";
}
try { try {
$localhost = $this->servers->where('id', 0)->first(); $localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration(); if ($localhost) {
$localhost->setupDynamicProxyConfiguration();
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
} }
$settings = instanceSettings();
if (! is_null(config('constants.coolify.autoupdate', null))) { if (! is_null(config('constants.coolify.autoupdate', null))) {
if (config('constants.coolify.autoupdate') == true) { if (config('constants.coolify.autoupdate') == true) {
echo "Enabling auto-update\n"; echo "Enabling auto-update\n";
$settings->update(['is_auto_update_enabled' => true]); $this->settings->update(['is_auto_update_enabled' => true]);
} else { } else {
echo "Disabling auto-update\n"; echo "Disabling auto-update\n";
$settings->update(['is_auto_update_enabled' => false]); $this->settings->update(['is_auto_update_enabled' => false]);
} }
} }
} }
@ -140,24 +139,18 @@ private function pullTemplatesFromCDN()
private function pullChangelogFromGitHub() private function pullChangelogFromGitHub()
{ {
try { try {
PullChangelogFromGitHub::dispatch(); PullChangelog::dispatch();
echo "Changelog fetch initiated\n"; echo "Changelog fetch initiated\n";
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not fetch changelog from GitHub: {$e->getMessage()}\n"; echo "Could not fetch changelog from GitHub: {$e->getMessage()}\n";
} }
} }
private function optimize()
{
Artisan::call('optimize:clear');
Artisan::call('optimize');
}
private function updateUserEmails() private function updateUserEmails()
{ {
try { try {
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) { User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) {
$user->update(['email' => strtolower($user->email)]); $user->update(['email' => $user->email]);
}); });
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in updating user emails: {$e->getMessage()}\n"; echo "Error in updating user emails: {$e->getMessage()}\n";
@ -173,27 +166,6 @@ private function updateTraefikLabels()
} }
} }
private function cleanupUnnecessaryDynamicProxyConfiguration()
{
foreach ($this->servers as $server) {
try {
if (! $server->isFunctional()) {
continue;
}
if ($server->id === 0) {
continue;
}
$file = $server->proxyPath().'/dynamic/coolify.yaml';
return instant_remote_process([
"rm -f $file",
], $server, false);
} catch (\Throwable $e) {
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
}
}
}
private function cleanupUnusedNetworkFromCoolifyProxy() private function cleanupUnusedNetworkFromCoolifyProxy()
{ {
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
@ -263,13 +235,6 @@ private function sendAliveSignal()
{ {
$id = config('app.id'); $id = config('app.id');
$version = config('constants.coolify.version'); $version = config('constants.coolify.version');
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Do_not_track is enabled\n";
return;
}
try { try {
Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version"); Http::get("https://undead.coolify.io/v4/alive?appId=$id&version=$version");
} catch (\Throwable $e) { } catch (\Throwable $e) {
@ -277,23 +242,6 @@ private function sendAliveSignal()
} }
} }
private function cleanupInProgressApplicationDeployments()
{
// Cleanup any failed deployments
try {
if (isCloud()) {
return;
}
$queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get();
foreach ($queued_inprogress_deployments as $deployment) {
$deployment->status = ApplicationDeploymentStatus::FAILED->value;
$deployment->save();
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
}
}
private function replaceSlashInEnvironmentName() private function replaceSlashInEnvironmentName()
{ {
if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) { if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {

View file

@ -1,98 +0,0 @@
<?php
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
class InitChangelog extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'changelog:init {month? : Month in YYYY-MM format (defaults to current month)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Initialize a new monthly changelog file with example structure';
/**
* Execute the console command.
*/
public function handle()
{
$month = $this->argument('month') ?: Carbon::now()->format('Y-m');
// Validate month format
if (! preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) {
$this->error('Invalid month format. Use YYYY-MM format with valid months 01-12 (e.g., 2025-08)');
return self::FAILURE;
}
$changelogsDir = base_path('changelogs');
$filePath = $changelogsDir."/{$month}.json";
// Create changelogs directory if it doesn't exist
if (! is_dir($changelogsDir)) {
mkdir($changelogsDir, 0755, true);
$this->info("Created changelogs directory: {$changelogsDir}");
}
// Check if file already exists
if (file_exists($filePath)) {
if (! $this->confirm("File {$month}.json already exists. Overwrite?")) {
$this->info('Operation cancelled');
return self::SUCCESS;
}
}
// Parse the month for example data
$carbonMonth = Carbon::createFromFormat('Y-m', $month);
$monthName = $carbonMonth->format('F Y');
$sampleDate = $carbonMonth->addDays(14)->toISOString(); // Mid-month
// Get version from config
$version = 'v'.config('constants.coolify.version');
// Create example changelog structure
$exampleData = [
'entries' => [
[
'version' => $version,
'title' => 'Example Feature Release',
'content' => "This is an example changelog entry for {$monthName}. Replace this with your actual release notes. Include details about new features, improvements, bug fixes, and any breaking changes.",
'published_at' => $sampleDate,
],
],
];
// Write the file
$jsonContent = json_encode($exampleData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if (file_put_contents($filePath, $jsonContent) === false) {
$this->error("Failed to create changelog file: {$filePath}");
return self::FAILURE;
}
$this->info("✅ Created changelog file: changelogs/{$month}.json");
$this->line(" Example entry created for {$monthName}");
$this->line(' Edit the file to add your actual changelog entries');
// Show the file contents
if ($this->option('verbose')) {
$this->newLine();
$this->line('File contents:');
$this->line($jsonContent);
}
return self::SUCCESS;
}
}

View file

@ -6,7 +6,14 @@
use App\Models\Application; use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use function Laravel\Prompts\confirm; use function Laravel\Prompts\confirm;
@ -103,19 +110,79 @@ private function deleteApplication()
private function deleteDatabase() private function deleteDatabase()
{ {
$databases = StandalonePostgresql::all(); // Collect all databases from all types with unique identifiers
if ($databases->count() === 0) { $allDatabases = collect();
$databaseOptions = collect();
// Add PostgreSQL databases
foreach (StandalonePostgresql::all() as $db) {
$key = "postgresql_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (PostgreSQL)");
}
// Add MySQL databases
foreach (StandaloneMysql::all() as $db) {
$key = "mysql_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (MySQL)");
}
// Add MariaDB databases
foreach (StandaloneMariadb::all() as $db) {
$key = "mariadb_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (MariaDB)");
}
// Add MongoDB databases
foreach (StandaloneMongodb::all() as $db) {
$key = "mongodb_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (MongoDB)");
}
// Add Redis databases
foreach (StandaloneRedis::all() as $db) {
$key = "redis_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (Redis)");
}
// Add KeyDB databases
foreach (StandaloneKeydb::all() as $db) {
$key = "keydb_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (KeyDB)");
}
// Add Dragonfly databases
foreach (StandaloneDragonfly::all() as $db) {
$key = "dragonfly_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (Dragonfly)");
}
// Add ClickHouse databases
foreach (StandaloneClickhouse::all() as $db) {
$key = "clickhouse_{$db->id}";
$allDatabases->put($key, $db);
$databaseOptions->put($key, "{$db->name} (ClickHouse)");
}
if ($allDatabases->count() === 0) {
$this->error('There are no databases to delete.'); $this->error('There are no databases to delete.');
return; return;
} }
$databasesToDelete = multiselect( $databasesToDelete = multiselect(
'What database do you want to delete?', 'What database do you want to delete?',
$databases->pluck('name', 'id')->sortKeys(), $databaseOptions->sortKeys(),
); );
foreach ($databasesToDelete as $database) { foreach ($databasesToDelete as $databaseKey) {
$toDelete = $databases->where('id', $database)->first(); $toDelete = $allDatabases->get($databaseKey);
if ($toDelete) { if ($toDelete) {
$this->info($toDelete); $this->info($toDelete);
$confirmed = confirm('Are you sure you want to delete all selected resources?'); $confirmed = confirm('Are you sure you want to delete all selected resources?');

View file

@ -16,7 +16,7 @@ class SyncBunny extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'sync:bunny {--templates} {--release} {--nightly}'; protected $signature = 'sync:bunny {--templates} {--release} {--github-releases} {--nightly}';
/** /**
* The console command description. * The console command description.
@ -25,6 +25,50 @@ class SyncBunny extends Command
*/ */
protected $description = 'Sync files to BunnyCDN'; protected $description = 'Sync files to BunnyCDN';
/**
* Fetch GitHub releases and sync to CDN
*/
private function syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn)
{
$this->info('Fetching releases from GitHub...');
try {
$response = Http::timeout(30)
->get('https://api.github.com/repos/coollabsio/coolify/releases', [
'per_page' => 30, // Fetch more releases for better changelog
]);
if ($response->successful()) {
$releases = $response->json();
// Save releases to a temporary file
$releases_file = "$parent_dir/releases.json";
file_put_contents($releases_file, json_encode($releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
// Upload to CDN
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: $releases_file)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/releases.json"),
$pool->purge("$bunny_cdn/coolify/releases.json"),
]);
// Clean up temporary file
unlink($releases_file);
$this->info('releases.json uploaded & purged...');
$this->info('Total releases synced: '.count($releases));
return true;
} else {
$this->error('Failed to fetch releases from GitHub: '.$response->status());
return false;
}
} catch (\Throwable $e) {
$this->error('Error fetching releases: '.$e->getMessage());
return false;
}
}
/** /**
* Execute the console command. * Execute the console command.
*/ */
@ -33,6 +77,7 @@ public function handle()
$that = $this; $that = $this;
$only_template = $this->option('templates'); $only_template = $this->option('templates');
$only_version = $this->option('release'); $only_version = $this->option('release');
$only_github_releases = $this->option('github-releases');
$nightly = $this->option('nightly'); $nightly = $this->option('nightly');
$bunny_cdn = 'https://cdn.coollabs.io'; $bunny_cdn = 'https://cdn.coollabs.io';
$bunny_cdn_path = 'coolify'; $bunny_cdn_path = 'coolify';
@ -90,7 +135,7 @@ public function handle()
$install_script_location = "$parent_dir/other/nightly/$install_script"; $install_script_location = "$parent_dir/other/nightly/$install_script";
$versions_location = "$parent_dir/other/nightly/$versions"; $versions_location = "$parent_dir/other/nightly/$versions";
} }
if (! $only_template && ! $only_version) { if (! $only_template && ! $only_version && ! $only_github_releases) {
if ($nightly) { if ($nightly) {
$this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.'); $this->info('About to sync files NIGHTLY (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
} else { } else {
@ -128,12 +173,29 @@ public function handle()
if (! $confirmed) { if (! $confirmed) {
return; return;
} }
// First sync GitHub releases
$this->info('Syncing GitHub releases first...');
$this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
// Then sync versions.json
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"), $pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]); ]);
$this->info('versions.json uploaded & purged...'); $this->info('versions.json uploaded & purged...');
return;
} elseif ($only_github_releases) {
$this->info('About to sync GitHub releases to BunnyCDN.');
$confirmed = confirm('Are you sure you want to sync GitHub releases?');
if (! $confirmed) {
return;
}
// Use the reusable function
$this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
return; return;
} }

View file

@ -6,7 +6,7 @@
use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CheckHelperImageJob; use App\Jobs\CheckHelperImageJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\PullChangelogFromGitHub; use App\Jobs\PullChangelog;
use App\Jobs\PullTemplatesFromCDN; use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\RegenerateSslCertJob; use App\Jobs\RegenerateSslCertJob;
use App\Jobs\ScheduledJobManager; use App\Jobs\ScheduledJobManager;
@ -68,7 +68,7 @@ protected function schedule(Schedule $schedule): void
$this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer(); $this->scheduleInstance->command('cleanup:unreachable-servers')->daily()->onOneServer();
$this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); $this->scheduleInstance->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$this->scheduleInstance->job(new PullChangelogFromGitHub)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); $this->scheduleInstance->job(new PullChangelog)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
$this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $this->scheduleInstance->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$this->scheduleUpdates(); $this->scheduleUpdates();

View file

@ -29,6 +29,7 @@ class Handler extends ExceptionHandler
*/ */
protected $dontReport = [ protected $dontReport = [
ProcessException::class, ProcessException::class,
NonReportableException::class,
]; ];
/** /**
@ -110,9 +111,14 @@ function (Scope $scope) {
); );
} }
); );
// Check for errors that should not be reported to Sentry
if (str($e->getMessage())->contains('No space left on device')) { if (str($e->getMessage())->contains('No space left on device')) {
// Log locally but don't send to Sentry
logger()->warning('Disk space error: '.$e->getMessage());
return; return;
} }
Integration::captureUnhandledException($e); Integration::captureUnhandledException($e);
}); });
} }

View file

@ -0,0 +1,31 @@
<?php
namespace App\Exceptions;
use Exception;
/**
* Exception that should not be reported to Sentry or other error tracking services.
* Use this for known, expected errors that don't require external tracking.
*/
class NonReportableException extends Exception
{
/**
* Create a new non-reportable exception instance.
*
* @param string $message
* @param int $code
*/
public function __construct($message = '', $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
/**
* Create from another exception, preserving its message and stack trace.
*/
public static function fromException(\Throwable $exception): static
{
return new static($exception->getMessage(), $exception->getCode(), $exception);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Helpers;
use App\Traits\SshRetryable;
/**
* Helper class to use SshRetryable trait in non-class contexts
*/
class SshRetryHandler
{
use SshRetryable;
/**
* Static method to get a singleton instance
*/
public static function instance(): self
{
static $instance = null;
if ($instance === null) {
$instance = new self;
}
return $instance;
}
/**
* Convenience static method for retry execution
*/
public static function retry(callable $callback, array $context = [], bool $throwError = true)
{
return self::instance()->executeWithSshRetry($callback, $context, $throwError);
}
}

View file

@ -2284,6 +2284,9 @@ public function update_by_uuid(Request $request)
data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson)); data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson));
} }
$application->fill($data); $application->fill($data);
if ($application->settings->is_container_label_readonly_enabled && $requestHasDomains && $server->isProxyShouldRun()) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
}
$application->save(); $application->save();
if ($instantDeploy) { if ($instantDeploy) {

View file

@ -5,6 +5,7 @@
use App\Enums\ProcessStatus; use App\Enums\ProcessStatus;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\ApplicationPullRequestUpdateJob; use App\Jobs\ApplicationPullRequestUpdateJob;
use App\Jobs\DeleteResourceJob;
use App\Jobs\GithubAppPermissionJob; use App\Jobs\GithubAppPermissionJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
@ -78,6 +79,7 @@ public function manual(Request $request)
$pull_request_html_url = data_get($payload, 'pull_request.html_url'); $pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref'); $branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref'); $base_branch = data_get($payload, 'pull_request.base.ref');
$author_association = data_get($payload, 'pull_request.author_association');
} }
if (! $branch) { if (! $branch) {
return response('Nothing to do. No branch found in the request.'); return response('Nothing to do. No branch found in the request.');
@ -170,6 +172,19 @@ public function manual(Request $request)
if ($x_github_event === 'pull_request') { if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) { if ($application->isPRDeployable()) {
// Check if PR deployments from public contributors are restricted
if (! $application->settings->is_pr_deployments_public_enabled) {
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
if (! in_array($author_association, $trustedAssociations)) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
]);
continue;
}
}
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
@ -226,9 +241,7 @@ public function manual(Request $request)
if ($action === 'closed') { if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) { if ($found) {
$found->delete(); DeleteResourceJob::dispatch($found);
$container_name = generateApplicationContainerName($application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,
'status' => 'success', 'status' => 'success',
@ -327,6 +340,7 @@ public function normal(Request $request)
$pull_request_html_url = data_get($payload, 'pull_request.html_url'); $pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref'); $branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref'); $base_branch = data_get($payload, 'pull_request.base.ref');
$author_association = data_get($payload, 'pull_request.author_association');
} }
if (! $id || ! $branch) { if (! $id || ! $branch) {
return response('Nothing to do. No id or branch found.'); return response('Nothing to do. No id or branch found.');
@ -400,6 +414,19 @@ public function normal(Request $request)
if ($x_github_event === 'pull_request') { if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) { if ($application->isPRDeployable()) {
// Check if PR deployments from public contributors are restricted
if (! $application->settings->is_pr_deployments_public_enabled) {
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
if (! in_array($author_association, $trustedAssociations)) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
]);
continue;
}
}
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) { if (! $found) {
@ -452,7 +479,8 @@ public function normal(Request $request)
} }
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
$found->delete();
DeleteResourceJob::dispatch($found);
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View file

@ -28,7 +28,7 @@ public function handle(Request $request, Closure $next): Response
$allowedIps = array_map('trim', $allowedIps); $allowedIps = array_map('trim', $allowedIps);
$allowedIps = array_filter($allowedIps); // Remove empty entries $allowedIps = array_filter($allowedIps); // Remove empty entries
if (! empty($allowedIps) && ! check_ip_against_allowlist($request->ip(), $allowedIps)) { if (! empty($allowedIps) && ! checkIPAgainstAllowlist($request->ip(), $allowedIps)) {
return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403);
} }
} }

View file

@ -388,11 +388,8 @@ private function deploy_simple_dockerfile()
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->execute_remote_command( $dockerfile_content = base64_decode($dockerfile_base64);
[ transfer_file_to_container($dockerfile_content, "{$this->workdir}{$this->dockerfile_location}", $this->deployment_uuid, $this->server);
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"),
],
);
$this->generate_image_names(); $this->generate_image_names();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
@ -497,10 +494,7 @@ private function deploy_docker_compose_buildpack()
$yaml = Yaml::dump(convertToArray($composeFile), 10); $yaml = Yaml::dump(convertToArray($composeFile), 10);
} }
$this->docker_compose_base64 = base64_encode($yaml); $this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([ transfer_file_to_container($yaml, "{$this->workdir}{$this->docker_compose_location}", $this->deployment_uuid, $this->server);
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
'hidden' => true,
]);
// Build new container to limit downtime. // Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.'); $this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@ -715,13 +709,12 @@ private function write_deployment_configurations()
$composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml"; $composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml";
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml";
} }
$this->execute_remote_command([
"mkdir -p $mainDir",
]);
$docker_compose_content = base64_decode($this->docker_compose_base64);
transfer_file_to_server($docker_compose_content, $composeFileName, $this->server);
$this->execute_remote_command( $this->execute_remote_command(
[
"mkdir -p $mainDir",
],
[
"echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null",
],
[ [
"echo '{$readme}' > $mainDir/README.md", "echo '{$readme}' > $mainDir/README.md",
] ]
@ -1013,27 +1006,15 @@ private function save_environment_variables()
); );
} }
} else { } else {
$envs_base64 = base64_encode($envs->implode("\n")); $envs_content = $envs->implode("\n");
$this->execute_remote_command( transfer_file_to_container($envs_content, "$this->workdir/{$this->env_filename}", $this->deployment_uuid, $this->server);
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"),
],
);
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->original_server; $this->server = $this->original_server;
$this->execute_remote_command( transfer_file_to_server($envs_content, "$this->configuration_dir/{$this->env_filename}", $this->server);
[
"echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null",
]
);
$this->server = $this->build_server; $this->server = $this->build_server;
} else { } else {
$this->execute_remote_command( transfer_file_to_server($envs_content, "$this->configuration_dir/{$this->env_filename}", $this->server);
[
"echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null",
]
);
} }
} }
$this->environment_variables = $envs; $this->environment_variables = $envs;
@ -1443,14 +1424,11 @@ private function check_git_if_build_needed()
} }
$private_key = data_get($this->application, 'private_key.private_key'); $private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) { if ($private_key) {
$private_key = base64_encode($private_key); $this->execute_remote_command([
executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'),
]);
transfer_file_to_container($private_key, '/root/.ssh/id_rsa', $this->deployment_uuid, $this->server);
$this->execute_remote_command( $this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'),
],
[
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
],
[ [
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'), executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
], ],
@ -1993,7 +1971,7 @@ private function generate_compose_file()
$this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose); $this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]); transfer_file_to_container(base64_decode($this->docker_compose_base64), "{$this->workdir}/docker-compose.yaml", $this->deployment_uuid, $this->server);
} }
private function generate_local_persistent_volumes() private function generate_local_persistent_volumes()
@ -2121,7 +2099,8 @@ private function build_image()
} else { } else {
if ($this->application->build_pack === 'nixpacks') { if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan); $this->nixpacks_plan = base64_encode($this->nixpacks_plan);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); $nixpacks_content = base64_decode($this->nixpacks_plan);
transfer_file_to_container($nixpacks_content, '/artifacts/thegameplan.json', $this->deployment_uuid, $this->server);
if ($this->force_rebuild) { if ($this->force_rebuild) {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"),
@ -2139,7 +2118,7 @@ private function build_image()
$base64_build_command = base64_encode($build_command); $base64_build_command = base64_encode($build_command);
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server),
'hidden' => true, 'hidden' => true,
], ],
[ [
@ -2162,7 +2141,7 @@ private function build_image()
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server),
'hidden' => true, 'hidden' => true,
], ],
[ [
@ -2194,13 +2173,13 @@ private function build_image()
$base64_build_command = base64_encode($build_command); $base64_build_command = base64_encode($build_command);
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d | tee {$this->workdir}/Dockerfile > /dev/null"), transfer_file_to_container(base64_decode($dockerfile), "{$this->workdir}/Dockerfile", $this->deployment_uuid, $this->server),
], ],
[ [
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d | tee {$this->workdir}/nginx.conf > /dev/null"), transfer_file_to_container(base64_decode($nginx_config), "{$this->workdir}/nginx.conf", $this->deployment_uuid, $this->server),
], ],
[ [
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server),
'hidden' => true, 'hidden' => true,
], ],
[ [
@ -2223,7 +2202,7 @@ private function build_image()
$base64_build_command = base64_encode($build_command); $base64_build_command = base64_encode($build_command);
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server),
'hidden' => true, 'hidden' => true,
], ],
[ [
@ -2238,7 +2217,8 @@ private function build_image()
} else { } else {
if ($this->application->build_pack === 'nixpacks') { if ($this->application->build_pack === 'nixpacks') {
$this->nixpacks_plan = base64_encode($this->nixpacks_plan); $this->nixpacks_plan = base64_encode($this->nixpacks_plan);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); $nixpacks_content = base64_decode($this->nixpacks_plan);
transfer_file_to_container($nixpacks_content, '/artifacts/thegameplan.json', $this->deployment_uuid, $this->server);
if ($this->force_rebuild) { if ($this->force_rebuild) {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"),
@ -2255,7 +2235,7 @@ private function build_image()
$base64_build_command = base64_encode($build_command); $base64_build_command = base64_encode($build_command);
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server),
'hidden' => true, 'hidden' => true,
], ],
[ [
@ -2278,7 +2258,7 @@ private function build_image()
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), transfer_file_to_container(base64_decode($base64_build_command), '/artifacts/build.sh', $this->deployment_uuid, $this->server),
'hidden' => true, 'hidden' => true,
], ],
[ [
@ -2405,7 +2385,7 @@ private function add_build_env_variables_to_dockerfile()
} }
$dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), transfer_file_to_container(base64_decode($dockerfile_base64), "{$this->workdir}{$this->dockerfile_location}", $this->deployment_uuid, $this->server),
'hidden' => true, 'hidden' => true,
]); ]);
} }

View file

@ -11,8 +11,9 @@
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class PullChangelogFromGitHub implements ShouldBeEncrypted, ShouldQueue class PullChangelog implements ShouldBeEncrypted, ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -26,21 +27,36 @@ public function __construct()
public function handle(): void public function handle(): void
{ {
try { try {
// Fetch from CDN instead of GitHub API to avoid rate limits
$cdnUrl = config('constants.coolify.releases_url');
$response = Http::retry(3, 1000) $response = Http::retry(3, 1000)
->timeout(30) ->timeout(30)
->get('https://api.github.com/repos/coollabsio/coolify/releases?per_page=10'); ->get($cdnUrl);
if ($response->successful()) { if ($response->successful()) {
$releases = $response->json(); $releases = $response->json();
// Limit to 10 releases for processing (same as before)
$releases = array_slice($releases, 0, 10);
$changelog = $this->transformReleasesToChangelog($releases); $changelog = $this->transformReleasesToChangelog($releases);
// Group entries by month and save them // Group entries by month and save them
$this->saveChangelogEntries($changelog); $this->saveChangelogEntries($changelog);
} else { } else {
send_internal_notification('PullChangelogFromGitHub failed with: '.$response->status().' '.$response->body()); // Log error instead of sending notification
Log::error('PullChangelogFromGitHub: Failed to fetch from CDN', [
'status' => $response->status(),
'url' => $cdnUrl,
]);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('PullChangelogFromGitHub failed with: '.$e->getMessage()); // Log error instead of sending notification
Log::error('PullChangelogFromGitHub: Exception occurred', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
} }
} }

View file

@ -3,6 +3,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Events\ScheduledTaskDone; use App\Events\ScheduledTaskDone;
use App\Exceptions\NonReportableException;
use App\Models\Application; use App\Models\Application;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
use App\Models\ScheduledTaskExecution; use App\Models\ScheduledTaskExecution;
@ -120,7 +121,7 @@ public function handle(): void
} }
// No valid container was found. // No valid container was found.
throw new \Exception('ScheduledTaskJob failed: No valid container was found. Is the container name correct?'); throw new NonReportableException('ScheduledTaskJob failed: No valid container was found. Is the container name correct?');
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($this->task_log) { if ($this->task_log) {
$this->task_log->update([ $this->task_log->update([

View file

@ -78,6 +78,8 @@ public function requestEmailChange()
'new_email' => ['required', 'email', 'unique:users,email'], 'new_email' => ['required', 'email', 'unique:users,email'],
]); ]);
$this->new_email = strtolower($this->new_email);
// Skip rate limiting in development mode // Skip rate limiting in development mode
if (! isDev()) { if (! isDev()) {
// Rate limit by current user's email (1 request per 2 minutes) // Rate limit by current user's email (1 request per 2 minutes)
@ -90,7 +92,7 @@ public function requestEmailChange()
} }
// Rate limit by new email address (3 requests per hour per email) // Rate limit by new email address (3 requests per hour per email)
$newEmailKey = 'email-change:email:'.md5(strtolower($this->new_email)); $newEmailKey = 'email-change:email:'.md5($this->new_email);
if (! RateLimiter::attempt($newEmailKey, 3, function () {}, 3600)) { if (! RateLimiter::attempt($newEmailKey, 3, function () {}, 3600)) {
$this->dispatch('error', 'This email address has received too many verification requests. Please try again later.'); $this->dispatch('error', 'This email address has received too many verification requests. Please try again later.');

View file

@ -28,6 +28,9 @@ class Advanced extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $isPreviewDeploymentsEnabled = false; public bool $isPreviewDeploymentsEnabled = false;
#[Validate(['boolean'])]
public bool $isPrDeploymentsPublicEnabled = false;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $isAutoDeployEnabled = true; public bool $isAutoDeployEnabled = true;
@ -91,6 +94,7 @@ public function syncData(bool $toModel = false)
$this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled; $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled;
$this->application->settings->is_git_shallow_clone_enabled = $this->isGitShallowCloneEnabled; $this->application->settings->is_git_shallow_clone_enabled = $this->isGitShallowCloneEnabled;
$this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled; $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled;
$this->application->settings->is_pr_deployments_public_enabled = $this->isPrDeploymentsPublicEnabled;
$this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled; $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled;
$this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled; $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->application->settings->is_gpu_enabled = $this->isGpuEnabled; $this->application->settings->is_gpu_enabled = $this->isGpuEnabled;
@ -117,6 +121,7 @@ public function syncData(bool $toModel = false)
$this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled; $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled;
$this->isGitShallowCloneEnabled = $this->application->settings->is_git_shallow_clone_enabled ?? false; $this->isGitShallowCloneEnabled = $this->application->settings->is_git_shallow_clone_enabled ?? false;
$this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled; $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled;
$this->isPrDeploymentsPublicEnabled = $this->application->settings->is_pr_deployments_public_enabled ?? false;
$this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled; $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled;
$this->isGpuEnabled = $this->application->settings->is_gpu_enabled; $this->isGpuEnabled = $this->application->settings->is_gpu_enabled;
$this->gpuDriver = $this->application->settings->gpu_driver; $this->gpuDriver = $this->application->settings->gpu_driver;

View file

@ -487,7 +487,7 @@ public function checkFqdns($showToaster = true)
$domains = str($this->application->fqdn)->trim()->explode(','); $domains = str($this->application->fqdn)->trim()->explode(',');
if ($this->application->additional_servers->count() === 0) { if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) { foreach ($domains as $domain) {
if (! validate_dns_entry($domain, $this->application->destination->server)) { if (! validateDNSEntry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
} }
} }
@ -615,7 +615,7 @@ public function submit($showToaster = true)
foreach ($this->parsedServiceDomains as $service) { foreach ($this->parsedServiceDomains as $service) {
$domain = data_get($service, 'domain'); $domain = data_get($service, 'domain');
if ($domain) { if ($domain) {
if (! validate_dns_entry($domain, $this->application->destination->server)) { if (! validateDNSEntry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $showToaster && $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
} }
} }

View file

@ -77,7 +77,7 @@ public function save_preview($preview_id)
$preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim(); $preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim(); $preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->trim()->lower(); $preview->fqdn = str($preview->fqdn)->trim()->lower();
if (! validate_dns_entry($preview->fqdn, $this->application->destination->server)) { if (! validateDNSEntry($preview->fqdn, $this->application->destination->server)) {
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$success = false; $success = false;
} }

View file

@ -232,8 +232,12 @@ public function runImport()
break; break;
} }
$restoreCommandBase64 = base64_encode($restoreCommand); $this->importCommands[] = [
$this->importCommands[] = "echo \"{$restoreCommandBase64}\" | base64 -d > {$scriptPath}"; 'transfer_file' => [
'content' => $restoreCommand,
'destination' => $scriptPath,
],
];
$this->importCommands[] = "chmod +x {$scriptPath}"; $this->importCommands[] = "chmod +x {$scriptPath}";
$this->importCommands[] = "docker cp {$scriptPath} {$this->container}:{$scriptPath}"; $this->importCommands[] = "docker cp {$scriptPath} {$this->container}:{$scriptPath}";

View file

@ -2,8 +2,8 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\GetProxyConfiguration;
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveProxyConfiguration;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
@ -16,11 +16,11 @@ class Proxy extends Component
public ?string $selectedProxy = null; public ?string $selectedProxy = null;
public $proxy_settings = null; public $proxySettings = null;
public bool $redirect_enabled = true; public bool $redirectEnabled = true;
public ?string $redirect_url = null; public ?string $redirectUrl = null;
public function getListeners() public function getListeners()
{ {
@ -39,14 +39,14 @@ public function getListeners()
public function mount() public function mount()
{ {
$this->selectedProxy = $this->server->proxyType(); $this->selectedProxy = $this->server->proxyType();
$this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true); $this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true);
$this->redirect_url = data_get($this->server, 'proxy.redirect_url'); $this->redirectUrl = data_get($this->server, 'proxy.redirect_url');
} }
// public function proxyStatusUpdated() public function getConfigurationFilePathProperty()
// { {
// $this->dispatch('refresh')->self(); return $this->server->proxyPath().'/docker-compose.yml';
// } }
public function changeProxy() public function changeProxy()
{ {
@ -86,7 +86,7 @@ public function instantSaveRedirect()
{ {
try { try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
$this->server->proxy->redirect_enabled = $this->redirect_enabled; $this->server->proxy->redirect_enabled = $this->redirectEnabled;
$this->server->save(); $this->server->save();
$this->server->setupDefaultRedirect(); $this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.'); $this->dispatch('success', 'Proxy configuration saved.');
@ -99,8 +99,8 @@ public function submit()
{ {
try { try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
SaveConfiguration::run($this->server, $this->proxy_settings); SaveProxyConfiguration::run($this->server, $this->proxySettings);
$this->server->proxy->redirect_url = $this->redirect_url; $this->server->proxy->redirect_url = $this->redirectUrl;
$this->server->save(); $this->server->save();
$this->server->setupDefaultRedirect(); $this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.'); $this->dispatch('success', 'Proxy configuration saved.');
@ -109,14 +109,15 @@ public function submit()
} }
} }
public function reset_proxy_configuration() public function resetProxyConfiguration()
{ {
try { try {
$this->authorize('update', $this->server); $this->authorize('update', $this->server);
$this->proxy_settings = CheckConfiguration::run($this->server, true); // Explicitly regenerate default configuration
SaveConfiguration::run($this->server, $this->proxy_settings); $this->proxySettings = GetProxyConfiguration::run($this->server, forceRegenerate: true);
SaveProxyConfiguration::run($this->server, $this->proxySettings);
$this->server->save(); $this->server->save();
$this->dispatch('success', 'Proxy configuration saved.'); $this->dispatch('success', 'Proxy configuration reset to default.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@ -125,7 +126,7 @@ public function reset_proxy_configuration()
public function loadProxyConfiguration() public function loadProxyConfiguration()
{ {
try { try {
$this->proxy_settings = CheckConfiguration::run($this->server); $this->proxySettings = GetProxyConfiguration::run($this->server);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View file

@ -78,10 +78,7 @@ public function addDynamicConfiguration()
$yaml = Yaml::dump($yaml, 10, 2); $yaml = Yaml::dump($yaml, 10, 2);
$this->value = $yaml; $this->value = $yaml;
} }
$base64_value = base64_encode($this->value); transfer_file_to_server($this->value, $file, $this->server);
instant_remote_process([
"echo '{$base64_value}' | base64 -d | tee {$file} > /dev/null",
], $this->server);
if ($proxy_type === 'CADDY') { if ($proxy_type === 'CADDY') {
$this->server->reloadCaddy(); $this->server->reloadCaddy();
} }

View file

@ -115,7 +115,7 @@ public function submit()
$this->validate(); $this->validate();
if ($this->settings->is_dns_validation_enabled && $this->fqdn) { if ($this->settings->is_dns_validation_enabled && $this->fqdn) {
if (! validate_dns_entry($this->fqdn, $this->server)) { if (! validateDNSEntry($this->fqdn, $this->server)) {
$this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help."); $this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$error_show = true; $error_show = true;
} }

View file

@ -2,7 +2,7 @@
namespace App\Livewire; namespace App\Livewire;
use App\Jobs\PullChangelogFromGitHub; use App\Jobs\PullChangelog;
use App\Services\ChangelogService; use App\Services\ChangelogService;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Livewire\Component; use Livewire\Component;
@ -23,6 +23,11 @@ public function getEntriesProperty()
return app(ChangelogService::class)->getEntriesForUser($user); return app(ChangelogService::class)->getEntriesForUser($user);
} }
public function getCurrentVersionProperty()
{
return 'v'.config('constants.coolify.version');
}
public function openWhatsNewModal() public function openWhatsNewModal()
{ {
$this->showWhatsNewModal = true; $this->showWhatsNewModal = true;
@ -50,7 +55,7 @@ public function manualFetchChangelog()
} }
try { try {
PullChangelogFromGitHub::dispatch(); PullChangelog::dispatch();
$this->dispatch('success', 'Changelog fetch initiated! Check back in a few moments.'); $this->dispatch('success', 'Changelog fetch initiated! Check back in a few moments.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->dispatch('error', 'Failed to fetch changelog: '.$e->getMessage()); $this->dispatch('error', 'Failed to fetch changelog: '.$e->getMessage());
@ -62,6 +67,7 @@ public function render()
return view('livewire.settings-dropdown', [ return view('livewire.settings-dropdown', [
'entries' => $this->entries, 'entries' => $this->entries,
'unreadCount' => $this->unreadCount, 'unreadCount' => $this->unreadCount,
'currentVersion' => $this->currentVersion,
]); ]);
} }
} }

View file

@ -1075,26 +1075,20 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
if (is_null($private_key)) { if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
} }
$private_key = base64_encode($private_key);
$base_comamnd = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} {$customRepository}"; $base_comamnd = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} {$customRepository}";
if ($exec_in_docker) { $commands = collect([]);
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
]);
} else {
$commands = collect([
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
]);
}
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'));
// SSH key transfer handled by ApplicationDeploymentJob, assume key is already in container
$commands->push(executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'));
$commands->push(executeInDocker($deployment_uuid, $base_comamnd)); $commands->push(executeInDocker($deployment_uuid, $base_comamnd));
} else { } else {
$server = $this->destination->server;
$commands->push('mkdir -p /root/.ssh');
transfer_file_to_server($private_key, '/root/.ssh/id_rsa', $server);
$commands->push('chmod 600 /root/.ssh/id_rsa');
$commands->push($base_comamnd); $commands->push($base_comamnd);
} }
@ -1220,7 +1214,6 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
if (is_null($private_key)) { if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
} }
$private_key = base64_encode($private_key);
$escapedCustomRepository = escapeshellarg($customRepository); $escapedCustomRepository = escapeshellarg($customRepository);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
if ($only_checkout) { if ($only_checkout) {
@ -1228,18 +1221,18 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else { } else {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base); $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
} }
$commands = collect([]);
if ($exec_in_docker) { if ($exec_in_docker) {
$commands = collect([ $commands->push(executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'));
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'), // SSH key transfer handled by ApplicationDeploymentJob, assume key is already in container
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"), $commands->push(executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'));
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
]);
} else { } else {
$commands = collect([ $server = $this->destination->server;
'mkdir -p /root/.ssh', $commands->push('mkdir -p /root/.ssh');
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null", transfer_file_to_server($private_key, '/root/.ssh/id_rsa', $server);
'chmod 600 /root/.ssh/id_rsa', $commands->push('chmod 600 /root/.ssh/id_rsa');
]);
} }
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') { if ($git_type === 'gitlab') {

View file

@ -13,6 +13,7 @@ class ApplicationSetting extends Model
'is_force_https_enabled' => 'boolean', 'is_force_https_enabled' => 'boolean',
'is_debug_enabled' => 'boolean', 'is_debug_enabled' => 'boolean',
'is_preview_deployments_enabled' => 'boolean', 'is_preview_deployments_enabled' => 'boolean',
'is_pr_deployments_public_enabled' => 'boolean',
'is_git_submodules_enabled' => 'boolean', 'is_git_submodules_enabled' => 'boolean',
'is_git_lfs_enabled' => 'boolean', 'is_git_lfs_enabled' => 'boolean',
'is_git_shallow_clone_enabled' => 'boolean', 'is_git_shallow_clone_enabled' => 'boolean',

View file

@ -119,6 +119,7 @@ public function saveStorageOnServer()
$commands = collect([]); $commands = collect([]);
if ($this->is_directory) { if ($this->is_directory) {
$commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true"); $commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $workdir > /dev/null 2>&1 || true");
$commands->push("cd $workdir"); $commands->push("cd $workdir");
} }
if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) { if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) {
@ -158,8 +159,7 @@ public function saveStorageOnServer()
$chmod = data_get($this, 'chmod'); $chmod = data_get($this, 'chmod');
$chown = data_get($this, 'chown'); $chown = data_get($this, 'chown');
if ($content) { if ($content) {
$content = base64_encode($content); transfer_file_to_server($content, $path, $server);
$commands->push("echo '$content' | base64 -d | tee $path > /dev/null");
} else { } else {
$commands->push("touch $path"); $commands->push("touch $path");
} }
@ -174,7 +174,9 @@ public function saveStorageOnServer()
$commands->push("mkdir -p $path > /dev/null 2>&1 || true"); $commands->push("mkdir -p $path > /dev/null 2>&1 || true");
} }
return instant_remote_process($commands, $server); if ($commands->count() > 0) {
return instant_remote_process($commands, $server);
}
} }
// Accessor for convenient access // Accessor for convenient access

View file

@ -309,10 +309,7 @@ public function setupDefaultRedirect()
$conf = Yaml::dump($dynamic_conf, 12, 2); $conf = Yaml::dump($dynamic_conf, 12, 2);
} }
$conf = $banner.$conf; $conf = $banner.$conf;
$base64 = base64_encode($conf); transfer_file_to_server($conf, $default_redirect_file, $this);
instant_remote_process([
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
} }
if ($proxy_type === 'CADDY') { if ($proxy_type === 'CADDY') {
@ -446,11 +443,10 @@ public function setupDynamicProxyConfiguration()
"# Do not edit it manually (only if you know what are you doing).\n\n". "# Do not edit it manually (only if you know what are you doing).\n\n".
$yaml; $yaml;
$base64 = base64_encode($yaml);
instant_remote_process([ instant_remote_process([
"mkdir -p $dynamic_config_path", "mkdir -p $dynamic_config_path",
"echo '$base64' | base64 -d | tee $file > /dev/null",
], $this); ], $this);
transfer_file_to_server($yaml, $file, $this);
} }
} elseif ($this->proxyType() === 'CADDY') { } elseif ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy"; $file = "$dynamic_config_path/coolify.caddy";
@ -473,10 +469,7 @@ public function setupDynamicProxyConfiguration()
} }
reverse_proxy coolify:8080 reverse_proxy coolify:8080
}"; }";
$base64 = base64_encode($caddy_file); transfer_file_to_server($caddy_file, $file, $this);
instant_remote_process([
"echo '$base64' | base64 -d | tee $file > /dev/null",
], $this);
$this->reloadCaddy(); $this->reloadCaddy();
} }
} }
@ -1319,7 +1312,6 @@ private function disableSshMux(): void
public function generateCaCertificate() public function generateCaCertificate()
{ {
try { try {
ray('Generating CA certificate for server', $this->id);
SslHelper::generateSslCertificate( SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate', commonName: 'Coolify CA Certificate',
serverId: $this->id, serverId: $this->id,
@ -1327,7 +1319,6 @@ public function generateCaCertificate()
validityDays: 10 * 365 validityDays: 10 * 365
); );
$caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first(); $caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first();
ray('CA certificate generated', $caCertificate);
if ($caCertificate) { if ($caCertificate) {
$certificateContent = $caCertificate->ssl_certificate; $certificateContent = $caCertificate->ssl_certificate;
$caCertPath = config('constants.coolify.base_config_path').'/ssl/'; $caCertPath = config('constants.coolify.base_config_path').'/ssl/';

View file

@ -1281,8 +1281,10 @@ public function saveComposeConfigs()
if ($envs->count() === 0) { if ($envs->count() === 0) {
$commands[] = 'touch .env'; $commands[] = 'touch .env';
} else { } else {
$envs_base64 = base64_encode($envs->implode("\n")); $envs_content = $envs->implode("\n");
$commands[] = "echo '$envs_base64' | base64 -d | tee .env > /dev/null"; transfer_file_to_server($envs_content, $this->workdir().'/.env', $this->server);
return;
} }
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);

View file

@ -56,6 +56,22 @@ class User extends Authenticatable implements SendsEmail
'email_change_code_expires_at' => 'datetime', 'email_change_code_expires_at' => 'datetime',
]; ];
/**
* Set the email attribute to lowercase.
*/
public function setEmailAttribute($value)
{
$this->attributes['email'] = strtolower($value);
}
/**
* Set the pending_email attribute to lowercase.
*/
public function setPendingEmailAttribute($value)
{
$this->attributes['pending_email'] = $value ? strtolower($value) : null;
}
protected static function boot() protected static function boot()
{ {
parent::boot(); parent::boot();

View file

@ -2,6 +2,7 @@
namespace App\Notifications\Channels; namespace App\Notifications\Channels;
use App\Exceptions\NonReportableException;
use App\Models\Team; use App\Models\Team;
use Exception; use Exception;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
@ -101,13 +102,11 @@ public function send(SendsEmail $notifiable, Notification $notification): void
$mailer->send($email); $mailer->send($email);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('EmailChannel failed: '.$e->getMessage(), [ // Check if this is a Resend domain verification error on cloud instances
'notification' => get_class($notification), if (isCloud() && str_contains($e->getMessage(), 'domain is not verified')) {
'notifiable' => get_class($notifiable), // Throw as NonReportableException so it won't go to Sentry
'team_id' => data_get($notifiable, 'id'), throw NonReportableException::fromException($e);
'error' => $e->getMessage(), }
'trace' => $e->getTraceAsString(),
]);
throw $e; throw $e;
} }
} }

View file

@ -11,6 +11,8 @@
trait ExecuteRemoteCommand trait ExecuteRemoteCommand
{ {
use SshRetryable;
public ?string $save = null; public ?string $save = null;
public static int $batch_counter = 0; public static int $batch_counter = 0;
@ -43,76 +45,169 @@ public function execute_remote_command(...$commands)
$command = parseLineForSudo($command, $this->server); $command = parseLineForSudo($command, $this->server);
} }
} }
$remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n".$output;
}
// Sanitize output to ensure valid UTF-8 encoding before JSON encoding $maxRetries = config('constants.ssh.max_retries');
$sanitized_output = sanitize_utf8_text($output); $attempt = 0;
$lastError = null;
$new_log_entry = [ $commandExecuted = false;
'command' => remove_iip($command),
'output' => remove_iip($sanitized_output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (! $this->application_deployment_queue->logs) {
$new_log_entry['order'] = 1;
} else {
try {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
// If existing logs are corrupted, start fresh
$previous_logs = [];
$new_log_entry['order'] = 1;
}
if (is_array($previous_logs)) {
$new_log_entry['order'] = count($previous_logs) + 1;
} else {
$previous_logs = [];
$new_log_entry['order'] = 1;
}
}
$previous_logs[] = $new_log_entry;
while ($attempt < $maxRetries && ! $commandExecuted) {
try { try {
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); $this->executeCommandWithProcess($command, $hidden, $customType, $append, $ignore_errors);
} catch (\JsonException $e) { $commandExecuted = true;
// If JSON encoding still fails, use fallback with invalid sequences replacement } catch (\RuntimeException $e) {
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_INVALID_UTF8_SUBSTITUTE); $lastError = $e;
} $errorMessage = $e->getMessage();
// Only retry if it's an SSH connection error and we haven't exhausted retries
if ($this->isRetryableSshError($errorMessage) && $attempt < $maxRetries - 1) {
$attempt++;
$delay = $this->calculateRetryDelay($attempt - 1);
$this->application_deployment_queue->save(); // Track SSH retry event in Sentry
$this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [
'server' => $this->server->name ?? $this->server->ip ?? 'unknown',
'command' => remove_iip($command),
'trait' => 'ExecuteRemoteCommand',
]);
if ($this->save) { // Add log entry for the retry
if (data_get($this->saved_outputs, $this->save, null) === null) { if (isset($this->application_deployment_queue)) {
data_set($this->saved_outputs, $this->save, str()); $this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage);
} }
if ($append) {
$this->saved_outputs[$this->save] .= str($sanitized_output)->trim(); sleep($delay);
$this->saved_outputs[$this->save] = str($this->saved_outputs[$this->save]);
} else { } else {
$this->saved_outputs[$this->save] = str($sanitized_output)->trim(); // Not retryable or max retries reached
throw $e;
} }
} }
}); }
$this->application_deployment_queue->update([
'current_process_id' => $process->id(),
]);
$process_result = $process->wait(); // If we exhausted all retries and still failed
if ($process_result->exitCode() !== 0) { if (! $commandExecuted && $lastError) {
if (! $ignore_errors) { throw $lastError;
$this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value;
$this->application_deployment_queue->save();
throw new \RuntimeException($process_result->errorOutput());
}
} }
}); });
} }
/**
* Execute the actual command with process handling
*/
private function executeCommandWithProcess($command, $hidden, $customType, $append, $ignore_errors)
{
$remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n".$output;
}
// Sanitize output to ensure valid UTF-8 encoding before JSON encoding
$sanitized_output = sanitize_utf8_text($output);
$new_log_entry = [
'command' => remove_iip($command),
'output' => remove_iip($sanitized_output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (! $this->application_deployment_queue->logs) {
$new_log_entry['order'] = 1;
} else {
try {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
// If existing logs are corrupted, start fresh
$previous_logs = [];
$new_log_entry['order'] = 1;
}
if (is_array($previous_logs)) {
$new_log_entry['order'] = count($previous_logs) + 1;
} else {
$previous_logs = [];
$new_log_entry['order'] = 1;
}
}
$previous_logs[] = $new_log_entry;
try {
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
// If JSON encoding still fails, use fallback with invalid sequences replacement
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_INVALID_UTF8_SUBSTITUTE);
}
$this->application_deployment_queue->save();
if ($this->save) {
if (data_get($this->saved_outputs, $this->save, null) === null) {
data_set($this->saved_outputs, $this->save, str());
}
if ($append) {
$this->saved_outputs[$this->save] .= str($sanitized_output)->trim();
$this->saved_outputs[$this->save] = str($this->saved_outputs[$this->save]);
} else {
$this->saved_outputs[$this->save] = str($sanitized_output)->trim();
}
}
});
$this->application_deployment_queue->update([
'current_process_id' => $process->id(),
]);
$process_result = $process->wait();
if ($process_result->exitCode() !== 0) {
if (! $ignore_errors) {
$this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value;
$this->application_deployment_queue->save();
throw new \RuntimeException($process_result->errorOutput());
}
}
}
/**
* Add a log entry for SSH retry attempts
*/
private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, string $errorMessage)
{
$retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}";
$new_log_entry = [
'output' => remove_iip($retryMessage),
'type' => 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => false,
'batch' => static::$batch_counter,
];
if (! $this->application_deployment_queue->logs) {
$new_log_entry['order'] = 1;
$previous_logs = [];
} else {
try {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$previous_logs = [];
$new_log_entry['order'] = 1;
}
if (is_array($previous_logs)) {
$new_log_entry['order'] = count($previous_logs) + 1;
} else {
$previous_logs = [];
$new_log_entry['order'] = 1;
}
}
$previous_logs[] = $new_log_entry;
try {
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_INVALID_UTF8_SUBSTITUTE);
}
$this->application_deployment_queue->save();
}
} }

174
app/Traits/SshRetryable.php Normal file
View file

@ -0,0 +1,174 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Log;
trait SshRetryable
{
/**
* Check if an error message indicates a retryable SSH connection error
*/
protected function isRetryableSshError(string $errorOutput): bool
{
$retryablePatterns = [
'kex_exchange_identification',
'Connection reset by peer',
'Connection refused',
'Connection timed out',
'Connection closed by remote host',
'ssh_exchange_identification',
'Bad file descriptor',
'Broken pipe',
'No route to host',
'Network is unreachable',
'Host is down',
'No buffer space available',
'Connection reset by',
'Permission denied, please try again',
'Received disconnect from',
'Disconnected from',
'Connection to .* closed',
'ssh: connect to host .* port .*: Connection',
'Lost connection',
'Timeout, server not responding',
'Cannot assign requested address',
'Network is down',
'Host key verification failed',
'Operation timed out',
'Connection closed unexpectedly',
'Remote host closed connection',
'Authentication failed',
'Too many authentication failures',
];
$lowerErrorOutput = strtolower($errorOutput);
foreach ($retryablePatterns as $pattern) {
if (str_contains($lowerErrorOutput, strtolower($pattern))) {
return true;
}
}
return false;
}
/**
* Calculate delay for exponential backoff
*/
protected function calculateRetryDelay(int $attempt): int
{
$baseDelay = config('constants.ssh.retry_base_delay');
$maxDelay = config('constants.ssh.retry_max_delay');
$multiplier = config('constants.ssh.retry_multiplier');
$delay = min($baseDelay * pow($multiplier, $attempt), $maxDelay);
return (int) $delay;
}
/**
* Execute a callback with SSH retry logic
*
* @param callable $callback The operation to execute
* @param array $context Context for logging (server, command, etc.)
* @param bool $throwError Whether to throw error on final failure
* @return mixed The result from the callback
*/
protected function executeWithSshRetry(callable $callback, array $context = [], bool $throwError = true)
{
$maxRetries = config('constants.ssh.max_retries');
$lastError = null;
$lastErrorMessage = '';
// Randomly fail the command with a key exchange error for testing
// if (random_int(1, 10) === 1) { // 10% chance to fail
// ray('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer');
// throw new \RuntimeException('SSH key exchange failed: kex_exchange_identification: read: Connection reset by peer');
// }
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
try {
return $callback();
} catch (\Throwable $e) {
$lastError = $e;
$lastErrorMessage = $e->getMessage();
// Check if it's retryable and not the last attempt
if ($this->isRetryableSshError($lastErrorMessage) && $attempt < $maxRetries - 1) {
$delay = $this->calculateRetryDelay($attempt);
// Track SSH retry event in Sentry
$this->trackSshRetryEvent($attempt + 1, $maxRetries, $delay, $lastErrorMessage, $context);
// Add deployment log if available (for ExecuteRemoteCommand trait)
if (isset($this->application_deployment_queue) && method_exists($this, 'addRetryLogEntry')) {
$this->addRetryLogEntry($attempt + 1, $maxRetries, $delay, $lastErrorMessage);
}
sleep($delay);
continue;
}
// Not retryable or max retries reached
break;
}
}
// All retries exhausted
if ($attempt >= $maxRetries) {
Log::error('SSH operation failed after all retries', array_merge($context, [
'attempts' => $attempt,
'error' => $lastErrorMessage,
]));
}
if ($throwError && $lastError) {
// If the error message is empty, provide a more meaningful one
if (empty($lastErrorMessage) || trim($lastErrorMessage) === '') {
$contextInfo = isset($context['server']) ? " to server {$context['server']}" : '';
$attemptInfo = $attempt > 1 ? " after {$attempt} attempts" : '';
throw new \RuntimeException("SSH connection failed{$contextInfo}{$attemptInfo}", $lastError->getCode());
}
throw $lastError;
}
return null;
}
/**
* Track SSH retry event in Sentry
*/
protected function trackSshRetryEvent(int $attempt, int $maxRetries, int $delay, string $errorMessage, array $context = []): void
{
// Only track in production/cloud instances
if (isDev() || ! config('constants.sentry.sentry_dsn')) {
return;
}
try {
app('sentry')->captureMessage(
'SSH connection retry triggered',
\Sentry\Severity::warning(),
[
'extra' => [
'attempt' => $attempt,
'max_retries' => $maxRetries,
'delay_seconds' => $delay,
'error_message' => $errorMessage,
'context' => $context,
'retryable_error' => true,
],
'tags' => [
'component' => 'ssh_retry',
'error_type' => 'connection_retry',
],
]
);
} catch (\Throwable $e) {
// Don't let Sentry tracking errors break the SSH retry flow
Log::warning('Failed to track SSH retry event in Sentry', [
'error' => $e->getMessage(),
'original_attempt' => $attempt,
]);
}
}
}

View file

@ -1069,9 +1069,9 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
} }
} }
} }
$base64_compose = base64_encode(Yaml::dump($yaml_compose)); $compose_content = Yaml::dump($yaml_compose);
transfer_file_to_server($compose_content, "/tmp/{$uuid}.yml", $server);
instant_remote_process([ instant_remote_process([
"echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null",
"chmod 600 /tmp/{$uuid}.yml", "chmod 600 /tmp/{$uuid}.yml",
"docker compose -f /tmp/{$uuid}.yml config --no-interpolate --no-path-resolution -q", "docker compose -f /tmp/{$uuid}.yml config --no-interpolate --no-path-resolution -q",
"rm /tmp/{$uuid}.yml", "rm /tmp/{$uuid}.yml",

View file

@ -1,6 +1,6 @@
<?php <?php
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveProxyConfiguration;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Models\Application; use App\Models\Application;
use App\Models\Server; use App\Models\Server;
@ -267,7 +267,7 @@ function generate_default_proxy_configuration(Server $server)
} }
$config = Yaml::dump($config, 12, 2); $config = Yaml::dump($config, 12, 2);
SaveConfiguration::run($server, $config); SaveProxyConfiguration::run($server, $config);
return $config; return $config;
} }

View file

@ -29,11 +29,31 @@ function remote_process(
$type = $type ?? ActivityTypes::INLINE->value; $type = $type ?? ActivityTypes::INLINE->value;
$command = $command instanceof Collection ? $command->toArray() : $command; $command = $command instanceof Collection ? $command->toArray() : $command;
if ($server->isNonRoot()) { // Process commands and handle file transfers
$command = parseCommandsByLineForSudo(collect($command), $server); $processed_commands = [];
foreach ($command as $cmd) {
if (is_array($cmd) && isset($cmd['transfer_file'])) {
// Handle file transfer command
$transfer_data = $cmd['transfer_file'];
$content = $transfer_data['content'];
$destination = $transfer_data['destination'];
// Execute file transfer immediately
transfer_file_to_server($content, $destination, $server, ! $ignore_errors);
// Add a comment to the command log for visibility
$processed_commands[] = "# File transferred via SCP: $destination";
} else {
// Regular string command
$processed_commands[] = $cmd;
}
} }
$command_string = implode("\n", $command); if ($server->isNonRoot()) {
$processed_commands = parseCommandsByLineForSudo(collect($processed_commands), $server);
}
$command_string = implode("\n", $processed_commands);
if (Auth::check()) { if (Auth::check()) {
$teams = Auth::user()->teams->pluck('id'); $teams = Auth::user()->teams->pluck('id');
@ -60,15 +80,87 @@ function remote_process(
function instant_scp(string $source, string $dest, Server $server, $throwError = true) function instant_scp(string $source, string $dest, Server $server, $throwError = true)
{ {
$scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest); return \App\Helpers\SshRetryHandler::retry(
$process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command); function () use ($source, $dest, $server) {
$output = trim($process->output()); $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest);
$exitCode = $process->exitCode(); $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command);
if ($exitCode !== 0) {
return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
}
return $output === 'null' ? null : $output; $output = trim($process->output());
$exitCode = $process->exitCode();
if ($exitCode !== 0) {
excludeCertainErrors($process->errorOutput(), $exitCode);
}
return $output === 'null' ? null : $output;
},
[
'server' => $server->ip,
'source' => $source,
'dest' => $dest,
'function' => 'instant_scp',
],
$throwError
);
}
function transfer_file_to_container(string $content, string $container_path, string $deployment_uuid, Server $server, bool $throwError = true): ?string
{
$temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_');
try {
// Write content to temporary file
file_put_contents($temp_file, $content);
// Generate unique filename for server transfer
$server_temp_file = '/tmp/coolify_env_'.uniqid().'_'.$deployment_uuid;
// Transfer file to server
instant_scp($temp_file, $server_temp_file, $server, $throwError);
// Ensure parent directory exists in container, then copy file
$parent_dir = dirname($container_path);
$commands = [];
if ($parent_dir !== '.' && $parent_dir !== '/') {
$commands[] = executeInDocker($deployment_uuid, "mkdir -p \"$parent_dir\"");
}
$commands[] = "docker cp $server_temp_file $deployment_uuid:$container_path";
$commands[] = "rm -f $server_temp_file"; // Cleanup server temp file
return instant_remote_process_with_timeout($commands, $server, $throwError);
} finally {
ray($temp_file);
// Always cleanup local temp file
if (file_exists($temp_file)) {
unlink($temp_file);
}
}
}
function transfer_file_to_server(string $content, string $server_path, Server $server, bool $throwError = true): ?string
{
$temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_');
try {
// Write content to temporary file
file_put_contents($temp_file, $content);
// Ensure parent directory exists on server
$parent_dir = dirname($server_path);
if ($parent_dir !== '.' && $parent_dir !== '/') {
instant_remote_process_with_timeout(["mkdir -p \"$parent_dir\""], $server, $throwError);
}
// Transfer file directly to server destination
return instant_scp($temp_file, $server_path, $server, $throwError);
} finally {
// Always cleanup local temp file
if (file_exists($temp_file)) {
unlink($temp_file);
}
}
} }
function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
@ -79,54 +171,85 @@ function instant_remote_process_with_timeout(Collection|array $command, Server $
} }
$command_string = implode("\n", $command); $command_string = implode("\n", $command);
// $start_time = microtime(true); return \App\Helpers\SshRetryHandler::retry(
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); function () use ($server, $command_string) {
$process = Process::timeout(30)->run($sshCommand); $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
// $end_time = microtime(true); $process = Process::timeout(30)->run($sshCommand);
// $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds $output = trim($process->output());
// ray('SSH command execution time:', $execution_time.' ms')->orange(); $exitCode = $process->exitCode();
$output = trim($process->output()); if ($exitCode !== 0) {
$exitCode = $process->exitCode(); excludeCertainErrors($process->errorOutput(), $exitCode);
}
if ($exitCode !== 0) { // Sanitize output to ensure valid UTF-8 encoding
return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null; $output = $output === 'null' ? null : sanitize_utf8_text($output);
}
// Sanitize output to ensure valid UTF-8 encoding return $output;
$output = $output === 'null' ? null : sanitize_utf8_text($output); },
[
return $output; 'server' => $server->ip,
'command_preview' => substr($command_string, 0, 100),
'function' => 'instant_remote_process_with_timeout',
],
$throwError
);
} }
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
{ {
$command = $command instanceof Collection ? $command->toArray() : $command; $command = $command instanceof Collection ? $command->toArray() : $command;
// Process commands and handle file transfers
$processed_commands = [];
foreach ($command as $cmd) {
if (is_array($cmd) && isset($cmd['transfer_file'])) {
// Handle file transfer command
$transfer_data = $cmd['transfer_file'];
$content = $transfer_data['content'];
$destination = $transfer_data['destination'];
// Execute file transfer immediately
transfer_file_to_server($content, $destination, $server, $throwError);
// Add a comment to the command log for visibility
$processed_commands[] = "# File transferred via SCP: $destination";
} else {
// Regular string command
$processed_commands[] = $cmd;
}
}
if ($server->isNonRoot() && ! $no_sudo) { if ($server->isNonRoot() && ! $no_sudo) {
$command = parseCommandsByLineForSudo(collect($command), $server); $processed_commands = parseCommandsByLineForSudo(collect($processed_commands), $server);
} }
$command_string = implode("\n", $command); $command_string = implode("\n", $processed_commands);
// $start_time = microtime(true); return \App\Helpers\SshRetryHandler::retry(
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string); function () use ($server, $command_string) {
$process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand); $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
// $end_time = microtime(true); $process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand);
// $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds $output = trim($process->output());
// ray('SSH command execution time:', $execution_time.' ms')->orange(); $exitCode = $process->exitCode();
$output = trim($process->output()); if ($exitCode !== 0) {
$exitCode = $process->exitCode(); excludeCertainErrors($process->errorOutput(), $exitCode);
}
if ($exitCode !== 0) { // Sanitize output to ensure valid UTF-8 encoding
return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null; $output = $output === 'null' ? null : sanitize_utf8_text($output);
}
// Sanitize output to ensure valid UTF-8 encoding return $output;
$output = $output === 'null' ? null : sanitize_utf8_text($output); },
[
return $output; 'server' => $server->ip,
'command_preview' => substr($command_string, 0, 100),
'function' => 'instant_remote_process',
],
$throwError
);
} }
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
@ -136,11 +259,18 @@ function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
'Could not resolve hostname', 'Could not resolve hostname',
]); ]);
$ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error)); $ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error));
// Ensure we always have a meaningful error message
$errorMessage = trim($errorOutput);
if (empty($errorMessage)) {
$errorMessage = "SSH command failed with exit code: $exitCode";
}
if ($ignored) { if ($ignored) {
// TODO: Create new exception and disable in sentry // TODO: Create new exception and disable in sentry
throw new \RuntimeException($errorOutput, $exitCode); throw new \RuntimeException($errorMessage, $exitCode);
} }
throw new \RuntimeException($errorOutput, $exitCode); throw new \RuntimeException($errorMessage, $exitCode);
} }
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection

View file

@ -69,12 +69,11 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
$fileVolume->content = $content; $fileVolume->content = $content;
$fileVolume->is_directory = false; $fileVolume->is_directory = false;
$fileVolume->save(); $fileVolume->save();
$content = base64_encode($content);
$dir = str($fileLocation)->dirname(); $dir = str($fileLocation)->dirname();
instant_remote_process([ instant_remote_process([
"mkdir -p $dir", "mkdir -p $dir",
"echo '$content' | base64 -d | tee $fileLocation",
], $server); ], $server);
transfer_file_to_server($content, $fileLocation, $server);
} elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) { } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) {
// Does not exists (no dir or file), flagged as directory, is init // Does not exists (no dir or file), flagged as directory, is init
$fileVolume->content = null; $fileVolume->content = null;

View file

@ -204,7 +204,6 @@ function get_latest_version_of_coolify(): string
return data_get($versions, 'coolify.v4.version'); return data_get($versions, 'coolify.v4.version');
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage());
return '0.0.0'; return '0.0.0';
} }
@ -962,7 +961,7 @@ function getRealtime()
} }
} }
function validate_dns_entry(string $fqdn, Server $server) function validateDNSEntry(string $fqdn, Server $server)
{ {
// https://www.cloudflare.com/ips-v4/# // https://www.cloudflare.com/ips-v4/#
$cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']); $cloudflare_ips = collect(['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '172.64.0.0/13', '131.0.72.0/22']);
@ -995,7 +994,7 @@ function validate_dns_entry(string $fqdn, Server $server)
} else { } else {
foreach ($results as $result) { foreach ($results as $result) {
if ($result->getType() == $type) { if ($result->getType() == $type) {
if (ip_match($result->getData(), $cloudflare_ips->toArray(), $match)) { if (ipMatch($result->getData(), $cloudflare_ips->toArray(), $match)) {
$found_matching_ip = true; $found_matching_ip = true;
break; break;
} }
@ -1013,7 +1012,7 @@ function validate_dns_entry(string $fqdn, Server $server)
return $found_matching_ip; return $found_matching_ip;
} }
function ip_match($ip, $cidrs, &$match = null) function ipMatch($ip, $cidrs, &$match = null)
{ {
foreach ((array) $cidrs as $cidr) { foreach ((array) $cidrs as $cidr) {
[$subnet, $mask] = explode('/', $cidr); [$subnet, $mask] = explode('/', $cidr);
@ -1027,7 +1026,7 @@ function ip_match($ip, $cidrs, &$match = null)
return false; return false;
} }
function check_ip_against_allowlist($ip, $allowlist) function checkIPAgainstAllowlist($ip, $allowlist)
{ {
if (empty($allowlist)) { if (empty($allowlist)) {
return false; return false;
@ -1085,78 +1084,6 @@ function check_ip_against_allowlist($ip, $allowlist)
return false; return false;
} }
function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{
$commands = $commands->map(function ($line) {
if (
! str(trim($line))->startsWith([
'cd',
'command',
'echo',
'true',
'if',
'fi',
])
) {
return "sudo $line";
}
if (str(trim($line))->startsWith('if')) {
return str_replace('if', 'if sudo', $line);
}
return $line;
});
$commands = $commands->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) {
return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p');
}
return $line;
});
$commands = $commands->map(function ($line) {
$line = str($line);
if (str($line)->contains('$(')) {
$line = $line->replace('$(', '$(sudo ');
}
if (str($line)->contains('||')) {
$line = $line->replace('||', '|| sudo');
}
if (str($line)->contains('&&')) {
$line = $line->replace('&&', '&& sudo');
}
if (str($line)->contains(' | ')) {
$line = $line->replace(' | ', ' | sudo ');
}
return $line->value();
});
return $commands->toArray();
}
function parseLineForSudo(string $command, Server $server): string
{
if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) {
$command = "sudo $command";
}
if (Str::startsWith($command, 'sudo mkdir -p')) {
$command = "$command && sudo chown -R $server->user:$server->user ".Str::after($command, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($command, 'sudo mkdir -p');
}
if (str($command)->contains('$(') || str($command)->contains('`')) {
$command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
}
if (str($command)->contains('||')) {
$command = str($command)->replace('||', '|| sudo ')->value();
}
if (str($command)->contains('&&')) {
$command = str($command)->replace('&&', '&& sudo ')->value();
}
return $command;
}
function get_public_ips() function get_public_ips()
{ {
try { try {

101
bootstrap/helpers/sudo.php Normal file
View file

@ -0,0 +1,101 @@
<?php
use App\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
function shouldChangeOwnership(string $path): bool
{
$path = trim($path);
$systemPaths = ['/var', '/etc', '/usr', '/opt', '/sys', '/proc', '/dev', '/bin', '/sbin', '/lib', '/lib64', '/boot', '/root', '/home', '/media', '/mnt', '/srv', '/run'];
foreach ($systemPaths as $systemPath) {
if ($path === $systemPath || Str::startsWith($path, $systemPath.'/')) {
return false;
}
}
$isCoolifyPath = Str::startsWith($path, '/data/coolify') || Str::startsWith($path, '/tmp/coolify');
return $isCoolifyPath;
}
function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{
$commands = $commands->map(function ($line) {
if (
! str(trim($line))->startsWith([
'cd',
'command',
'echo',
'true',
'if',
'fi',
])
) {
return "sudo $line";
}
if (str(trim($line))->startsWith('if')) {
return str_replace('if', 'if sudo', $line);
}
return $line;
});
$commands = $commands->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) {
$path = trim(Str::after($line, 'sudo mkdir -p'));
if (shouldChangeOwnership($path)) {
return "$line && sudo chown -R $server->user:$server->user $path && sudo chmod -R o-rwx $path";
}
return $line;
}
return $line;
});
$commands = $commands->map(function ($line) {
$line = str($line);
if (str($line)->contains('$(')) {
$line = $line->replace('$(', '$(sudo ');
}
if (str($line)->contains('||')) {
$line = $line->replace('||', '|| sudo');
}
if (str($line)->contains('&&')) {
$line = $line->replace('&&', '&& sudo');
}
if (str($line)->contains(' | ')) {
$line = $line->replace(' | ', ' | sudo ');
}
return $line->value();
});
return $commands->toArray();
}
function parseLineForSudo(string $command, Server $server): string
{
if (! str($command)->startSwith('cd') && ! str($command)->startSwith('command')) {
$command = "sudo $command";
}
if (Str::startsWith($command, 'sudo mkdir -p')) {
$path = trim(Str::after($command, 'sudo mkdir -p'));
if (shouldChangeOwnership($path)) {
$command = "$command && sudo chown -R $server->user:$server->user $path && sudo chmod -R o-rwx $path";
}
}
if (str($command)->contains('$(') || str($command)->contains('`')) {
$command = str($command)->replace('$(', '$(sudo ')->replace('`', '`sudo ')->value();
}
if (str($command)->contains('||')) {
$command = str($command)->replace('||', '|| sudo ')->value();
}
if (str($command)->contains('&&')) {
$command = str($command)->replace('&&', '&& sudo ')->value();
}
return $command;
}

View file

@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.426', 'version' => '4.0.0-beta.427',
'helper_version' => '1.0.10', 'helper_version' => '1.0.10',
'realtime_version' => '1.0.10', 'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
@ -12,6 +12,7 @@
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'), 'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'), 'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'),
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false), 'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
'releases_url' => 'https://cdn.coollabs.io/coolify/releases.json',
], ],
'urls' => [ 'urls' => [
@ -61,6 +62,10 @@
'connection_timeout' => 10, 'connection_timeout' => 10,
'server_interval' => 20, 'server_interval' => 20,
'command_timeout' => 7200, 'command_timeout' => 7200,
'max_retries' => env('SSH_MAX_RETRIES', 3),
'retry_base_delay' => env('SSH_RETRY_BASE_DELAY', 2), // seconds
'retry_max_delay' => env('SSH_RETRY_MAX_DELAY', 30), // seconds
'retry_multiplier' => env('SSH_RETRY_MULTIPLIER', 2),
], ],
'invitation' => [ 'invitation' => [

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('is_pr_deployments_public_enabled')->default(false)->after('is_preview_deployments_enabled');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_pr_deployments_public_enabled');
});
}
};

View file

@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.426" "version": "4.0.0-beta.427"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.427" "version": "4.0.0-beta.428"
}, },
"helper": { "helper": {
"version": "1.0.10" "version": "1.0.10"

View file

@ -13,6 +13,12 @@
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments." helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update" instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" canGate="update"
:canResource="$application" /> :canResource="$application" />
@if ($isPreviewDeploymentsEnabled)
<x-forms.checkbox
helper="When enabled, anyone can trigger PR deployments. When disabled, only repository members, collaborators, and contributors can trigger PR deployments."
instantSave id="isPrDeploymentsPublicEnabled" label="Allow Public PR Deployments" canGate="update"
:canResource="$application" />
@endif
@endif @endif
<x-forms.checkbox helper="Disable Docker build cache on every deployment." instantSave <x-forms.checkbox helper="Disable Docker build cache on every deployment." instantSave
id="disableBuildCache" label="Disable Build Cache" canGate="update" :canResource="$application" /> id="disableBuildCache" label="Disable Build Cache" canGate="update" :canResource="$application" />

View file

@ -7,9 +7,11 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Configuration</h2> <h2>Configuration</h2>
@if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing') @if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing')
<x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch Proxy</x-forms.button> <x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch
Proxy</x-forms.button>
@else @else
<x-forms.button canGate="update" :canResource="$server" disabled wire:click.prevent="changeProxy">Switch Proxy</x-forms.button> <x-forms.button canGate="update" :canResource="$server" disabled
wire:click.prevent="changeProxy">Switch Proxy</x-forms.button>
@endif @endif
<x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button> <x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button>
</div> </div>
@ -27,11 +29,11 @@
id="server.settings.generate_exact_labels" id="server.settings.generate_exact_labels"
label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave /> label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave />
<x-forms.checkbox canGate="update" :canResource="$server" instantSave="instantSaveRedirect" <x-forms.checkbox canGate="update" :canResource="$server" instantSave="instantSaveRedirect"
id="redirect_enabled" label="Override default request handler" id="redirectEnabled" label="Override default request handler"
helper="Requests to unknown hosts or stopped services will receive a 503 response or be redirected to the URL you set below (need to enable this first)." /> helper="Requests to unknown hosts or stopped services will receive a 503 response or be redirected to the URL you set below (need to enable this first)." />
@if ($redirect_enabled) @if ($redirectEnabled)
<x-forms.input canGate="update" :canResource="$server" placeholder="https://app.coolify.io" <x-forms.input canGate="update" :canResource="$server" placeholder="https://app.coolify.io"
id="redirect_url" label="Redirect to (optional)" /> id="redirectUrl" label="Redirect to (optional)" />
@endif @endif
</div> </div>
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value) @if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
@ -50,15 +52,26 @@
<x-loading text="Loading proxy configuration..." /> <x-loading text="Loading proxy configuration..." />
</div> </div>
<div wire:loading.remove wire:target="loadProxyConfiguration"> <div wire:loading.remove wire:target="loadProxyConfiguration">
@if ($proxy_settings) @if ($proxySettings)
<div class="flex flex-col gap-2 pt-4"> <div class="flex flex-col gap-2 pt-4">
<x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor <x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor
monacoEditorLanguage="yaml" label="Configuration file" name="proxy_settings" monacoEditorLanguage="yaml"
id="proxy_settings" rows="30" /> label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings"
<x-forms.button canGate="update" :canResource="$server" id="proxySettings" rows="30" />
wire:click.prevent="reset_proxy_configuration"> @can('update', $server)
Reset configuration to default <x-modal-confirmation title="Reset Proxy Configuration?"
</x-forms.button> buttonTitle="Reset configuration to default" isErrorButton
submitAction="resetProxyConfiguration" :actions="[
'Reset proxy configuration to default settings',
'All custom configurations will be lost',
'Custom ports and entrypoints will be removed',
]"
confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm by entering the server name below"
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
:confirmWithPassword="false" :confirmWithText="true">
</x-modal-confirmation>
@endcan
</div> </div>
@endif @endif
</div> </div>

View file

@ -242,6 +242,9 @@ class="relative w-full h-full max-w-7xl py-6 border rounded-sm drop-shadow-sm bg
<p class="mt-1 text-sm dark:text-neutral-400"> <p class="mt-1 text-sm dark:text-neutral-400">
Stay up to date with the latest features and improvements. Stay up to date with the latest features and improvements.
</p> </p>
<p class="mt-1 text-xs dark:text-neutral-500">
Current version: <span class="font-semibold dark:text-neutral-300">{{ $currentVersion }}</span>
</p>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if (isDev()) @if (isDev())
@ -299,6 +302,10 @@ class="inline-flex items-center gap-1 hover:text-coolgray-500">
<span x-text="entry.title"></span> <span x-text="entry.title"></span>
<x-external-link /> <x-external-link />
</a></span> </a></span>
<span x-show="entry.tag_name === '{{ $currentVersion }}'"
class="px-2 py-1 text-xs font-semibold bg-success text-white rounded-sm">
CURRENT VERSION
</span>
<span class="text-xs dark:text-neutral-400" <span class="text-xs dark:text-neutral-400"
x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span> x-text="new Date(entry.published_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></span>
</div> </div>

View file

@ -41,7 +41,7 @@
<h2>Invite New Member</h2> <h2>Invite New Member</h2>
@if (isInstanceAdmin()) @if (isInstanceAdmin())
<div class="pb-4 text-xs dark:text-warning">You need to configure (as root team) <a <div class="pb-4 text-xs dark:text-warning">You need to configure (as root team) <a
href="/settings#smtp" class="underline dark:text-warning">Transactional href="/settings/email" class="underline dark:text-warning">Transactional
Emails</a> Emails</a>
before before
you can invite a you can invite a

View file

@ -12,7 +12,7 @@ class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300 lds-heart" vi
</svg> </svg>
In progress In progress
</button> </button>
<button class="menu-item" @click="modalOpen=true" x-show="!showProgress"> <button class="menu-item cursor-pointer" @click="modalOpen=true" x-show="!showProgress">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" viewBox="0 0 24 24" class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"

View file

@ -326,7 +326,11 @@
'root' => '/', 'root' => '/',
]); ]);
if (! $disk->exists($filename)) { if (! $disk->exists($filename)) {
return response()->json(['message' => 'Backup not found.'], 404); if ($execution->scheduledDatabaseBackup->disable_local_backup === true && $execution->scheduledDatabaseBackup->save_s3 === true) {
return response()->json(['message' => 'Backup not available locally, but available on S3.'], 404);
}
return response()->json(['message' => 'Backup not found locally on the server.'], 404);
} }
return new StreamedResponse(function () use ($disk, $filename) { return new StreamedResponse(function () use ($disk, $filename) {

View file

@ -23,6 +23,7 @@ services:
environment: environment:
- SERVICE_URL_APPWRITE=/ - SERVICE_URL_APPWRITE=/
- _APP_ENV=${_APP_ENV:-production} - _APP_ENV=${_APP_ENV:-production}
- _APP_EDITION=${_APP_EDITION:-self-hosted}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
- _APP_LOCALE=${_APP_LOCALE:-en} - _APP_LOCALE=${_APP_LOCALE:-en}
- _APP_COMPRESSION_MIN_SIZE_BYTES=${_APP_COMPRESSION_MIN_SIZE_BYTES} - _APP_COMPRESSION_MIN_SIZE_BYTES=${_APP_COMPRESSION_MIN_SIZE_BYTES}
@ -41,11 +42,14 @@ services:
- _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled} - _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled}
- _APP_OPTIONS_ROUTER_FORCE_HTTPS=${_APP_OPTIONS_ROUTER_FORCE_HTTPS:-disabled} - _APP_OPTIONS_ROUTER_FORCE_HTTPS=${_APP_OPTIONS_ROUTER_FORCE_HTTPS:-disabled}
- _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE
- _APP_DOMAIN=$SERVICE_URL_APPWRITE - _APP_CONSOLE_DOMAIN=${_APP_CONSOLE_DOMAIN}
- _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
- _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME:-localhost} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME:-localhost}
- _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA:-::1} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA:-::1}
- _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A:-127.0.0.1} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A:-127.0.0.1}
- _APP_DOMAIN_FUNCTIONS=$SERVICE_URL_APPWRITE - _APP_DOMAIN_TARGET_CAA=${_APP_DOMAIN_TARGET_CAA}
- _APP_DOMAIN_FUNCTIONS=${_APP_DOMAIN_FUNCTIONS:-functions.$SERVICE_FQDN_APPWRITE}
- _APP_DNS=${_APP_DNS}
- _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis}
- _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379} - _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379}
- _APP_REDIS_USER=${_APP_REDIS_USER} - _APP_REDIS_USER=${_APP_REDIS_USER}
@ -96,7 +100,7 @@ services:
- _APP_COMPUTE_MEMORY=${_APP_COMPUTE_MEMORY:-0} - _APP_COMPUTE_MEMORY=${_APP_COMPUTE_MEMORY:-0}
- _APP_FUNCTIONS_RUNTIMES=${_APP_FUNCTIONS_RUNTIMES:-node-20.0,php-8.2,python-3.11,ruby-3.2} - _APP_FUNCTIONS_RUNTIMES=${_APP_FUNCTIONS_RUNTIMES:-node-20.0,php-8.2,python-3.11,ruby-3.2}
- _APP_SITES_RUNTIMES=${_APP_SITES_RUNTIMES} - _APP_SITES_RUNTIMES=${_APP_SITES_RUNTIMES}
- _APP_DOMAIN_SITES=${_APP_DOMAIN_SITES:-appwrite.network} - _APP_DOMAIN_SITES=${_APP_DOMAIN_SITES:-sites.$SERVICE_FQDN_APPWRITE}
- _APP_EXECUTOR_SECRET=$SERVICE_PASSWORD_64_APPWRITE - _APP_EXECUTOR_SECRET=$SERVICE_PASSWORD_64_APPWRITE
- _APP_EXECUTOR_HOST=${_APP_EXECUTOR_HOST:-http://appwrite-executor/v1} - _APP_EXECUTOR_HOST=${_APP_EXECUTOR_HOST:-http://appwrite-executor/v1}
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
@ -124,9 +128,20 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID=${_APP_MIGRATIONS_FIREBASE_CLIENT_ID} - _APP_MIGRATIONS_FIREBASE_CLIENT_ID=${_APP_MIGRATIONS_FIREBASE_CLIENT_ID}
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=${_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET} - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=${_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET}
- _APP_ASSISTANT_OPENAI_API_KEY=${_APP_ASSISTANT_OPENAI_API_KEY} - _APP_ASSISTANT_OPENAI_API_KEY=${_APP_ASSISTANT_OPENAI_API_KEY}
- _APP_MESSAGE_SMS_TEST_DSN=${_APP_MESSAGE_SMS_TEST_DSN}
- _APP_MESSAGE_EMAIL_TEST_DSN=${_APP_MESSAGE_EMAIL_TEST_DSN}
- _APP_MESSAGE_PUSH_TEST_DSN=${_APP_MESSAGE_PUSH_TEST_DSN}
- _APP_CONSOLE_COUNTRIES_DENYLIST=${_APP_CONSOLE_COUNTRIES_DENYLIST}
- _APP_EXPERIMENT_LOGGING_PROVIDER=${_APP_EXPERIMENT_LOGGING_PROVIDER}
- _APP_EXPERIMENT_LOGGING_CONFIG=${_APP_EXPERIMENT_LOGGING_CONFIG}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
- _APP_DATABASE_SHARED_TABLES_V1=${_APP_DATABASE_SHARED_TABLES_V1}
- _APP_DATABASE_SHARED_NAMESPACE=${_APP_DATABASE_SHARED_NAMESPACE}
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT=${_APP_FUNCTIONS_CREATION_ABUSE_LIMIT}
- _APP_CUSTOM_DOMAIN_DENY_LIST=${_APP_CUSTOM_DOMAIN_DENY_LIST}
appwrite-console: appwrite-console:
image: appwrite/console:6.0.13 image: appwrite/console:6.1.28
container_name: appwrite-console container_name: appwrite-console
environment: environment:
- SERVICE_URL_APPWRITE=/console - SERVICE_URL_APPWRITE=/console
@ -156,6 +171,7 @@ services:
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_USAGE_STATS=${_APP_USAGE_STATS:-enabled} - _APP_USAGE_STATS=${_APP_USAGE_STATS:-enabled}
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-audits: appwrite-worker-audits:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -178,6 +194,7 @@ services:
- _APP_DB_USER=$SERVICE_USER_MARIADB - _APP_DB_USER=$SERVICE_USER_MARIADB
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-webhooks: appwrite-worker-webhooks:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -202,6 +219,8 @@ services:
- _APP_REDIS_USER=${_APP_REDIS_USER} - _APP_REDIS_USER=${_APP_REDIS_USER}
- _APP_REDIS_PASS=${_APP_REDIS_PASS} - _APP_REDIS_PASS=${_APP_REDIS_PASS}
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=${_APP_WEBHOOK_MAX_FAILED_ATTEMPTS}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-deletes: appwrite-worker-deletes:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -255,12 +274,11 @@ services:
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_EXECUTOR_SECRET=$SERVICE_PASSWORD_64_APPWRITE - _APP_EXECUTOR_SECRET=$SERVICE_PASSWORD_64_APPWRITE
- _APP_EXECUTOR_HOST=${_APP_EXECUTOR_HOST:-http://appwrite-executor/v1} - _APP_EXECUTOR_HOST=${_APP_EXECUTOR_HOST:-http://appwrite-executor/v1}
- _APP_MAINTENANCE_RETENTION_ABUSE=${_APP_MAINTENANCE_RETENTION_ABUSE:-86400} - _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
- _APP_DATABASE_SHARED_TABLES_V1=${_APP_DATABASE_SHARED_TABLES_V1}
- _APP_EMAIL_CERTIFICATES=${_APP_EMAIL_CERTIFICATES}
- _APP_MAINTENANCE_RETENTION_AUDIT=${_APP_MAINTENANCE_RETENTION_AUDIT:-1209600} - _APP_MAINTENANCE_RETENTION_AUDIT=${_APP_MAINTENANCE_RETENTION_AUDIT:-1209600}
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE=${_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE} - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE=${_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE}
- _APP_MAINTENANCE_RETENTION_EXECUTION=${_APP_MAINTENANCE_RETENTION_EXECUTION:-1209600}
- _APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${_APP_SYSTEM_SECURITY_EMAIL_ADDRESS}
- _APP_EMAIL_CERTIFICATES=${_APP_EMAIL_CERTIFICATES}
appwrite-worker-databases: appwrite-worker-databases:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -283,6 +301,9 @@ services:
- _APP_DB_USER=$SERVICE_USER_MARIADB - _APP_DB_USER=$SERVICE_USER_MARIADB
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_WORKERS_NUM=${_APP_WORKERS_NUM}
- _APP_QUEUE_NAME=${_APP_QUEUE_NAME}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-builds: appwrite-worker-builds:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -323,7 +344,7 @@ services:
- _APP_COMPUTE_SIZE_LIMIT=${_APP_COMPUTE_SIZE_LIMIT:-30000000} - _APP_COMPUTE_SIZE_LIMIT=${_APP_COMPUTE_SIZE_LIMIT:-30000000}
- _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled} - _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled}
- _APP_OPTIONS_ROUTER_FORCE_HTTPS=${_APP_OPTIONS_ROUTER_FORCE_HTTPS:-disabled} - _APP_OPTIONS_ROUTER_FORCE_HTTPS=${_APP_OPTIONS_ROUTER_FORCE_HTTPS:-disabled}
- _APP_DOMAIN=$SERVICE_URL_APPWRITE - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
- _APP_STORAGE_DEVICE=${_APP_STORAGE_DEVICE:-local} - _APP_STORAGE_DEVICE=${_APP_STORAGE_DEVICE:-local}
- _APP_STORAGE_S3_ACCESS_KEY=${_APP_STORAGE_S3_ACCESS_KEY} - _APP_STORAGE_S3_ACCESS_KEY=${_APP_STORAGE_S3_ACCESS_KEY}
- _APP_STORAGE_S3_SECRET=${_APP_STORAGE_S3_SECRET} - _APP_STORAGE_S3_SECRET=${_APP_STORAGE_S3_SECRET}
@ -346,7 +367,10 @@ services:
- _APP_STORAGE_WASABI_SECRET=${_APP_STORAGE_WASABI_SECRET} - _APP_STORAGE_WASABI_SECRET=${_APP_STORAGE_WASABI_SECRET}
- _APP_STORAGE_WASABI_REGION=${_APP_STORAGE_WASABI_REGION:-eu-central-1} - _APP_STORAGE_WASABI_REGION=${_APP_STORAGE_WASABI_REGION:-eu-central-1}
- _APP_STORAGE_WASABI_BUCKET=${_APP_STORAGE_WASABI_BUCKET} - _APP_STORAGE_WASABI_BUCKET=${_APP_STORAGE_WASABI_BUCKET}
- _APP_DOMAIN_SITES=${_APP_DOMAIN_SITES} - _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
- _APP_DOMAIN_SITES=${_APP_DOMAIN_SITES:-sites.$SERVICE_FQDN_APPWRITE}
- _APP_BROWSER_HOST=${_APP_BROWSER_HOST}
- _APP_CONSOLE_DOMAIN=${_APP_CONSOLE_DOMAIN}
appwrite-worker-certificates: appwrite-worker-certificates:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -362,11 +386,13 @@ services:
- _APP_ENV=${_APP_ENV:-production} - _APP_ENV=${_APP_ENV:-production}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
- _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE
- _APP_DOMAIN=$SERVICE_URL_APPWRITE - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
- _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME}
- _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA}
- _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A}
- _APP_DOMAIN_FUNCTIONS=$SERVICE_URL_APPWRITE - _APP_DOMAIN_TARGET_CAA=${_APP_DOMAIN_TARGET_CAA}
- _APP_DOMAIN_FUNCTIONS=${_APP_DOMAIN_FUNCTIONS:-functions.$SERVICE_FQDN_APPWRITE}
- _APP_DNS=${_APP_DNS}
- _APP_EMAIL_CERTIFICATES=${_APP_EMAIL_CERTIFICATES:-enabled} - _APP_EMAIL_CERTIFICATES=${_APP_EMAIL_CERTIFICATES:-enabled}
- _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis}
- _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379} - _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379}
@ -378,6 +404,7 @@ services:
- _APP_DB_USER=$SERVICE_USER_MARIADB - _APP_DB_USER=$SERVICE_USER_MARIADB
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-functions: appwrite-worker-functions:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -391,7 +418,7 @@ services:
- _APP_ENV=${_APP_ENV:-production} - _APP_ENV=${_APP_ENV:-production}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
- _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE
- _APP_DOMAIN=$SERVICE_URL_APPWRITE - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
- _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled} - _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled}
- _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis}
- _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379} - _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379}
@ -413,6 +440,8 @@ services:
- _APP_DOCKER_HUB_USERNAME=${_APP_DOCKER_HUB_USERNAME} - _APP_DOCKER_HUB_USERNAME=${_APP_DOCKER_HUB_USERNAME}
- _APP_DOCKER_HUB_PASSWORD=${_APP_DOCKER_HUB_PASSWORD} - _APP_DOCKER_HUB_PASSWORD=${_APP_DOCKER_HUB_PASSWORD}
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_LOGGING_PROVIDER=${_APP_LOGGING_PROVIDER}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-mails: appwrite-worker-mails:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -420,6 +449,7 @@ services:
container_name: appwrite-worker-mails container_name: appwrite-worker-mails
depends_on: depends_on:
- appwrite-redis - appwrite-redis
- appwrite-mariadb
environment: environment:
- _APP_ENV=${_APP_ENV:-production} - _APP_ENV=${_APP_ENV:-production}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
@ -441,8 +471,9 @@ services:
- _APP_SMTP_USERNAME=${_APP_SMTP_USERNAME} - _APP_SMTP_USERNAME=${_APP_SMTP_USERNAME}
- _APP_SMTP_PASSWORD=${_APP_SMTP_PASSWORD} - _APP_SMTP_PASSWORD=${_APP_SMTP_PASSWORD}
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_DOMAIN=$SERVICE_URL_APPWRITE - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
- _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled} - _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-messaging: appwrite-worker-messaging:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -468,6 +499,7 @@ services:
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_SMS_FROM=${_APP_SMS_FROM} - _APP_SMS_FROM=${_APP_SMS_FROM}
- _APP_SMS_PROVIDER=${_APP_SMS_PROVIDER} - _APP_SMS_PROVIDER=${_APP_SMS_PROVIDER}
- _APP_SMS_PROJECTS_DENY_LIST=${_APP_SMS_PROJECTS_DENY_LIST}
- _APP_STORAGE_DEVICE=${_APP_STORAGE_DEVICE:-local} - _APP_STORAGE_DEVICE=${_APP_STORAGE_DEVICE:-local}
- _APP_STORAGE_S3_ACCESS_KEY=${_APP_STORAGE_S3_ACCESS_KEY} - _APP_STORAGE_S3_ACCESS_KEY=${_APP_STORAGE_S3_ACCESS_KEY}
- _APP_STORAGE_S3_SECRET=${_APP_STORAGE_S3_SECRET} - _APP_STORAGE_S3_SECRET=${_APP_STORAGE_S3_SECRET}
@ -490,6 +522,7 @@ services:
- _APP_STORAGE_WASABI_SECRET=${_APP_STORAGE_WASABI_SECRET} - _APP_STORAGE_WASABI_SECRET=${_APP_STORAGE_WASABI_SECRET}
- _APP_STORAGE_WASABI_REGION=${_APP_STORAGE_WASABI_REGION:-eu-central-1} - _APP_STORAGE_WASABI_REGION=${_APP_STORAGE_WASABI_REGION:-eu-central-1}
- _APP_STORAGE_WASABI_BUCKET=${_APP_STORAGE_WASABI_BUCKET} - _APP_STORAGE_WASABI_BUCKET=${_APP_STORAGE_WASABI_BUCKET}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-worker-migrations: appwrite-worker-migrations:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -503,10 +536,12 @@ services:
- _APP_ENV=${_APP_ENV:-production} - _APP_ENV=${_APP_ENV:-production}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
- _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE
- _APP_DOMAIN=$SERVICE_URL_APPWRITE - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
- _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME}
- _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA}
- _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A}
- _APP_DOMAIN_TARGET_CAA=${_APP_DOMAIN_TARGET_CAA}
- _APP_DNS=${_APP_DNS}
- _APP_EMAIL_SECURITY=${_APP_EMAIL_SECURITY:-certs@appwrite.io} - _APP_EMAIL_SECURITY=${_APP_EMAIL_SECURITY:-certs@appwrite.io}
- _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis}
- _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379} - _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379}
@ -520,6 +555,7 @@ services:
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID=${_APP_MIGRATIONS_FIREBASE_CLIENT_ID} - _APP_MIGRATIONS_FIREBASE_CLIENT_ID=${_APP_MIGRATIONS_FIREBASE_CLIENT_ID}
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=${_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET} - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=${_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-task-maintenance: appwrite-task-maintenance:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -530,11 +566,13 @@ services:
environment: environment:
- _APP_ENV=${_APP_ENV:-production} - _APP_ENV=${_APP_ENV:-production}
- _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6} - _APP_WORKER_PER_CORE=${_APP_WORKER_PER_CORE:-6}
- _APP_DOMAIN=$SERVICE_URL_APPWRITE - _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
- _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME} - _APP_DOMAIN_TARGET_CNAME=${_APP_DOMAIN_TARGET_CNAME}
- _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA} - _APP_DOMAIN_TARGET_AAAA=${_APP_DOMAIN_TARGET_AAAA}
- _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A} - _APP_DOMAIN_TARGET_A=${_APP_DOMAIN_TARGET_A}
- _APP_DOMAIN_FUNCTIONS=$SERVICE_URL_APPWRITE - _APP_DOMAIN_TARGET_CAA=${_APP_DOMAIN_TARGET_CAA}
- _APP_DOMAIN_FUNCTIONS=${_APP_DOMAIN_FUNCTIONS:-functions.$SERVICE_FQDN_APPWRITE}
- _APP_DNS=${_APP_DNS}
- _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE - _APP_OPENSSL_KEY_V1=$SERVICE_PASSWORD_64_APPWRITE
- _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis} - _APP_REDIS_HOST=${_APP_REDIS_HOST:-appwrite-redis}
- _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379} - _APP_REDIS_PORT=${_APP_REDIS_PORT:-6379}
@ -545,14 +583,16 @@ services:
- _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite} - _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite}
- _APP_DB_USER=$SERVICE_USER_MARIADB - _APP_DB_USER=$SERVICE_USER_MARIADB
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_MAINTENANCE_INTERVAL=${_APP_MAINTENANCE_INTERVAL} - _APP_MAINTENANCE_INTERVAL=${_APP_MAINTENANCE_INTERVAL:-86400}
- _APP_MAINTENANCE_RETENTION_EXECUTION=${_APP_MAINTENANCE_RETENTION_EXECUTION} - _APP_MAINTENANCE_RETENTION_EXECUTION=${_APP_MAINTENANCE_RETENTION_EXECUTION:-1209600}
- _APP_MAINTENANCE_RETENTION_CACHE=${_APP_MAINTENANCE_RETENTION_CACHE:-2592000} - _APP_MAINTENANCE_RETENTION_CACHE=${_APP_MAINTENANCE_RETENTION_CACHE:-2592000}
- _APP_MAINTENANCE_RETENTION_ABUSE=${_APP_MAINTENANCE_RETENTION_ABUSE:-86400} - _APP_MAINTENANCE_RETENTION_ABUSE=${_APP_MAINTENANCE_RETENTION_ABUSE:-86400}
- _APP_MAINTENANCE_RETENTION_AUDIT=${_APP_MAINTENANCE_RETENTION_AUDIT:-1209600} - _APP_MAINTENANCE_RETENTION_AUDIT=${_APP_MAINTENANCE_RETENTION_AUDIT:-1209600}
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE=${_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE} - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE=${_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE}
- _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=${_APP_MAINTENANCE_RETENTION_USAGE_HOURLY:-8640000} - _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=${_APP_MAINTENANCE_RETENTION_USAGE_HOURLY:-8640000}
- _APP_MAINTENANCE_RETENTION_SCHEDULES=${_APP_MAINTENANCE_RETENTION_SCHEDULES:-86400} - _APP_MAINTENANCE_RETENTION_SCHEDULES=${_APP_MAINTENANCE_RETENTION_SCHEDULES:-86400}
- _APP_MAINTENANCE_START_TIME=${_APP_MAINTENANCE_START_TIME}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-task-stats-resources: appwrite-task-stats-resources:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -626,6 +666,7 @@ services:
- _APP_USAGE_STATS=${_APP_USAGE_STATS:-enabled} - _APP_USAGE_STATS=${_APP_USAGE_STATS:-enabled}
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
- _APP_USAGE_AGGREGATION_INTERVAL=${_APP_USAGE_AGGREGATION_INTERVAL:-30} - _APP_USAGE_AGGREGATION_INTERVAL=${_APP_USAGE_AGGREGATION_INTERVAL:-30}
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-task-scheduler-functions: appwrite-task-scheduler-functions:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -647,6 +688,7 @@ services:
- _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite} - _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite}
- _APP_DB_USER=$SERVICE_USER_MARIADB - _APP_DB_USER=$SERVICE_USER_MARIADB
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-task-scheduler-executions: appwrite-task-scheduler-executions:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -668,6 +710,7 @@ services:
- _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite} - _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite}
- _APP_DB_USER=$SERVICE_USER_MARIADB - _APP_DB_USER=$SERVICE_USER_MARIADB
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-task-scheduler-messages: appwrite-task-scheduler-messages:
image: appwrite/appwrite:1.7.4 image: appwrite/appwrite:1.7.4
@ -689,9 +732,10 @@ services:
- _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite} - _APP_DB_SCHEMA=${_APP_DB_SCHEMA:-appwrite}
- _APP_DB_USER=$SERVICE_USER_MARIADB - _APP_DB_USER=$SERVICE_USER_MARIADB
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
appwrite-assistant: appwrite-assistant:
image: appwrite/assistant:0.4.0 image: appwrite/assistant:0.8.3
container_name: appwrite-assistant container_name: appwrite-assistant
environment: environment:
- _APP_ASSISTANT_OPENAI_API_KEY=${_APP_ASSISTANT_OPENAI_API_KEY} - _APP_ASSISTANT_OPENAI_API_KEY=${_APP_ASSISTANT_OPENAI_API_KEY}
@ -699,12 +743,13 @@ services:
appwrite-browser: appwrite-browser:
image: appwrite/browser:0.2.4 image: appwrite/browser:0.2.4
container_name: appwrite-browser container_name: appwrite-browser
hostname: appwrite-browser
openruntimes-executor: openruntimes-executor:
container_name: openruntimes-executor container_name: openruntimes-executor
hostname: appwrite-executor hostname: appwrite-executor
stop_signal: SIGINT stop_signal: SIGINT
image: openruntimes/executor:0.7.14 image: openruntimes/executor:0.8.6
networks: networks:
- runtimes - runtimes
volumes: volumes:
@ -714,6 +759,7 @@ services:
- appwrite-sites:/storage/sites:rw - appwrite-sites:/storage/sites:rw
- /tmp:/tmp:rw - /tmp:/tmp:rw
environment: environment:
- OPR_EXECUTOR_IMAGE_PULL=disabled
- OPR_EXECUTOR_INACTIVE_TRESHOLD=${_APP_COMPUTE_INACTIVE_THRESHOLD} - OPR_EXECUTOR_INACTIVE_TRESHOLD=${_APP_COMPUTE_INACTIVE_THRESHOLD}
- OPR_EXECUTOR_MAINTENANCE_INTERVAL=${_APP_COMPUTE_MAINTENANCE_INTERVAL} - OPR_EXECUTOR_MAINTENANCE_INTERVAL=${_APP_COMPUTE_MAINTENANCE_INTERVAL}
- OPR_EXECUTOR_NETWORK=${_APP_COMPUTE_RUNTIMES_NETWORK:-runtimes} - OPR_EXECUTOR_NETWORK=${_APP_COMPUTE_RUNTIMES_NETWORK:-runtimes}

View file

@ -18,7 +18,7 @@ services:
environment: environment:
- SERVICE_URL_OUTLINE_3000 - SERVICE_URL_OUTLINE_3000
- NODE_ENV=production - NODE_ENV=production
- SECRET_KEY=${SERVICE_BASE64_OUTLINE} - SECRET_KEY=${SERVICE_HEX_32_OUTLINE}
- UTILS_SECRET=${SERVICE_PASSWORD_64_OUTLINE} - UTILS_SECRET=${SERVICE_PASSWORD_64_OUTLINE}
- DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_64_POSTGRES}@postgres:5432/${POSTGRES_DATABASE:-outline} - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_64_POSTGRES}@postgres:5432/${POSTGRES_DATABASE:-outline}
- REDIS_URL=redis://:${SERVICE_PASSWORD_64_REDIS}@redis:6379 - REDIS_URL=redis://:${SERVICE_PASSWORD_64_REDIS}@redis:6379

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,7 @@
]; ];
foreach ($testCases as $case) { foreach ($testCases as $case) {
$result = check_ip_against_allowlist($case['ip'], $case['allowlist']); $result = checkIPAgainstAllowlist($case['ip'], $case['allowlist']);
expect($result)->toBe($case['expected']); expect($result)->toBe($case['expected']);
} }
}); });
@ -24,7 +24,7 @@
]; ];
foreach ($testCases as $case) { foreach ($testCases as $case) {
$result = check_ip_against_allowlist($case['ip'], $case['allowlist']); $result = checkIPAgainstAllowlist($case['ip'], $case['allowlist']);
expect($result)->toBe($case['expected']); expect($result)->toBe($case['expected']);
} }
}); });
@ -40,16 +40,16 @@
// Test 0.0.0.0 without subnet // Test 0.0.0.0 without subnet
foreach ($testIps as $ip) { foreach ($testIps as $ip) {
$result = check_ip_against_allowlist($ip, ['0.0.0.0']); $result = checkIPAgainstAllowlist($ip, ['0.0.0.0']);
expect($result)->toBeTrue(); expect($result)->toBeTrue();
} }
// Test 0.0.0.0 with any subnet notation - should still allow all // Test 0.0.0.0 with any subnet notation - should still allow all
foreach ($testIps as $ip) { foreach ($testIps as $ip) {
expect(check_ip_against_allowlist($ip, ['0.0.0.0/0']))->toBeTrue(); expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/0']))->toBeTrue();
expect(check_ip_against_allowlist($ip, ['0.0.0.0/8']))->toBeTrue(); expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/8']))->toBeTrue();
expect(check_ip_against_allowlist($ip, ['0.0.0.0/24']))->toBeTrue(); expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/24']))->toBeTrue();
expect(check_ip_against_allowlist($ip, ['0.0.0.0/32']))->toBeTrue(); expect(checkIPAgainstAllowlist($ip, ['0.0.0.0/32']))->toBeTrue();
} }
}); });
@ -66,44 +66,44 @@
]; ];
foreach ($testCases as $case) { foreach ($testCases as $case) {
$result = check_ip_against_allowlist($case['ip'], $allowlist); $result = checkIPAgainstAllowlist($case['ip'], $allowlist);
expect($result)->toBe($case['expected']); expect($result)->toBe($case['expected']);
} }
}); });
test('IP allowlist handles empty and invalid entries', function () { test('IP allowlist handles empty and invalid entries', function () {
// Empty allowlist blocks all // Empty allowlist blocks all
expect(check_ip_against_allowlist('192.168.1.1', []))->toBeFalse(); expect(checkIPAgainstAllowlist('192.168.1.1', []))->toBeFalse();
expect(check_ip_against_allowlist('192.168.1.1', ['']))->toBeFalse(); expect(checkIPAgainstAllowlist('192.168.1.1', ['']))->toBeFalse();
// Handles spaces // Handles spaces
expect(check_ip_against_allowlist('192.168.1.100', [' 192.168.1.100 ']))->toBeTrue(); expect(checkIPAgainstAllowlist('192.168.1.100', [' 192.168.1.100 ']))->toBeTrue();
expect(check_ip_against_allowlist('10.0.0.5', [' 10.0.0.0/8 ']))->toBeTrue(); expect(checkIPAgainstAllowlist('10.0.0.5', [' 10.0.0.0/8 ']))->toBeTrue();
// Invalid entries are skipped // Invalid entries are skipped
expect(check_ip_against_allowlist('192.168.1.1', ['invalid.ip']))->toBeFalse(); expect(checkIPAgainstAllowlist('192.168.1.1', ['invalid.ip']))->toBeFalse();
expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.0/33']))->toBeFalse(); // Invalid mask expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/33']))->toBeFalse(); // Invalid mask
expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.0/-1']))->toBeFalse(); // Invalid mask expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/-1']))->toBeFalse(); // Invalid mask
}); });
test('IP allowlist with various subnet sizes', function () { test('IP allowlist with various subnet sizes', function () {
// /32 - single host // /32 - single host
expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.1/32']))->toBeTrue(); expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.1/32']))->toBeTrue();
expect(check_ip_against_allowlist('192.168.1.2', ['192.168.1.1/32']))->toBeFalse(); expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.1/32']))->toBeFalse();
// /31 - point-to-point link // /31 - point-to-point link
expect(check_ip_against_allowlist('192.168.1.0', ['192.168.1.0/31']))->toBeTrue(); expect(checkIPAgainstAllowlist('192.168.1.0', ['192.168.1.0/31']))->toBeTrue();
expect(check_ip_against_allowlist('192.168.1.1', ['192.168.1.0/31']))->toBeTrue(); expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/31']))->toBeTrue();
expect(check_ip_against_allowlist('192.168.1.2', ['192.168.1.0/31']))->toBeFalse(); expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.0/31']))->toBeFalse();
// /16 - class B // /16 - class B
expect(check_ip_against_allowlist('172.16.0.1', ['172.16.0.0/16']))->toBeTrue(); expect(checkIPAgainstAllowlist('172.16.0.1', ['172.16.0.0/16']))->toBeTrue();
expect(check_ip_against_allowlist('172.16.255.255', ['172.16.0.0/16']))->toBeTrue(); expect(checkIPAgainstAllowlist('172.16.255.255', ['172.16.0.0/16']))->toBeTrue();
expect(check_ip_against_allowlist('172.17.0.1', ['172.16.0.0/16']))->toBeFalse(); expect(checkIPAgainstAllowlist('172.17.0.1', ['172.16.0.0/16']))->toBeFalse();
// /0 - all addresses // /0 - all addresses
expect(check_ip_against_allowlist('1.1.1.1', ['0.0.0.0/0']))->toBeTrue(); expect(checkIPAgainstAllowlist('1.1.1.1', ['0.0.0.0/0']))->toBeTrue();
expect(check_ip_against_allowlist('255.255.255.255', ['0.0.0.0/0']))->toBeTrue(); expect(checkIPAgainstAllowlist('255.255.255.255', ['0.0.0.0/0']))->toBeTrue();
}); });
test('IP allowlist comma-separated string input', function () { test('IP allowlist comma-separated string input', function () {
@ -111,10 +111,10 @@
$allowlistString = '192.168.1.100,10.0.0.0/8,172.16.0.0/16'; $allowlistString = '192.168.1.100,10.0.0.0/8,172.16.0.0/16';
$allowlist = explode(',', $allowlistString); $allowlist = explode(',', $allowlistString);
expect(check_ip_against_allowlist('192.168.1.100', $allowlist))->toBeTrue(); expect(checkIPAgainstAllowlist('192.168.1.100', $allowlist))->toBeTrue();
expect(check_ip_against_allowlist('10.5.5.5', $allowlist))->toBeTrue(); expect(checkIPAgainstAllowlist('10.5.5.5', $allowlist))->toBeTrue();
expect(check_ip_against_allowlist('172.16.10.10', $allowlist))->toBeTrue(); expect(checkIPAgainstAllowlist('172.16.10.10', $allowlist))->toBeTrue();
expect(check_ip_against_allowlist('8.8.8.8', $allowlist))->toBeFalse(); expect(checkIPAgainstAllowlist('8.8.8.8', $allowlist))->toBeFalse();
}); });
test('ValidIpOrCidr validation rule', function () { test('ValidIpOrCidr validation rule', function () {

View file

@ -0,0 +1,189 @@
<?php
namespace Tests\Unit;
use App\Helpers\SshRetryHandler;
use App\Traits\SshRetryable;
use Tests\TestCase;
class SshRetryMechanismTest extends TestCase
{
public function test_ssh_retry_handler_exists()
{
$this->assertTrue(class_exists(\App\Helpers\SshRetryHandler::class));
}
public function test_ssh_retryable_trait_exists()
{
$this->assertTrue(trait_exists(\App\Traits\SshRetryable::class));
}
public function test_retry_on_ssh_connection_errors()
{
$handler = new class
{
use SshRetryable;
// Make methods public for testing
public function test_is_retryable_ssh_error($error)
{
return $this->isRetryableSshError($error);
}
};
// Test various SSH error patterns
$sshErrors = [
'kex_exchange_identification: read: Connection reset by peer',
'Connection refused',
'Connection timed out',
'ssh_exchange_identification: Connection closed by remote host',
'Broken pipe',
'No route to host',
'Network is unreachable',
];
foreach ($sshErrors as $error) {
$this->assertTrue(
$handler->test_is_retryable_ssh_error($error),
"Failed to identify as retryable: $error"
);
}
}
public function test_non_ssh_errors_are_not_retryable()
{
$handler = new class
{
use SshRetryable;
// Make methods public for testing
public function test_is_retryable_ssh_error($error)
{
return $this->isRetryableSshError($error);
}
};
// Test non-SSH errors
$nonSshErrors = [
'Command not found',
'Permission denied',
'File not found',
'Syntax error',
'Invalid argument',
];
foreach ($nonSshErrors as $error) {
$this->assertFalse(
$handler->test_is_retryable_ssh_error($error),
"Incorrectly identified as retryable: $error"
);
}
}
public function test_exponential_backoff_calculation()
{
$handler = new class
{
use SshRetryable;
// Make method public for testing
public function test_calculate_retry_delay($attempt)
{
return $this->calculateRetryDelay($attempt);
}
};
// Test with default config values
config(['constants.ssh.retry_base_delay' => 2]);
config(['constants.ssh.retry_max_delay' => 30]);
config(['constants.ssh.retry_multiplier' => 2]);
// Attempt 0: 2 seconds
$this->assertEquals(2, $handler->test_calculate_retry_delay(0));
// Attempt 1: 4 seconds
$this->assertEquals(4, $handler->test_calculate_retry_delay(1));
// Attempt 2: 8 seconds
$this->assertEquals(8, $handler->test_calculate_retry_delay(2));
// Attempt 3: 16 seconds
$this->assertEquals(16, $handler->test_calculate_retry_delay(3));
// Attempt 4: Should be capped at 30 seconds
$this->assertEquals(30, $handler->test_calculate_retry_delay(4));
// Attempt 5: Should still be capped at 30 seconds
$this->assertEquals(30, $handler->test_calculate_retry_delay(5));
}
public function test_retry_succeeds_after_failures()
{
$attemptCount = 0;
config(['constants.ssh.max_retries' => 3]);
// Simulate a function that fails twice then succeeds using the public static method
$result = SshRetryHandler::retry(
function () use (&$attemptCount) {
$attemptCount++;
if ($attemptCount < 3) {
throw new \RuntimeException('kex_exchange_identification: Connection reset by peer');
}
return 'success';
},
['test' => 'retry_test'],
true
);
$this->assertEquals('success', $result);
$this->assertEquals(3, $attemptCount);
}
public function test_retry_fails_after_max_attempts()
{
$attemptCount = 0;
config(['constants.ssh.max_retries' => 3]);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Connection reset by peer');
// Simulate a function that always fails using the public static method
SshRetryHandler::retry(
function () use (&$attemptCount) {
$attemptCount++;
throw new \RuntimeException('Connection reset by peer');
},
['test' => 'retry_test'],
true
);
}
public function test_non_retryable_errors_fail_immediately()
{
$attemptCount = 0;
config(['constants.ssh.max_retries' => 3]);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Command not found');
try {
// Simulate a non-retryable error using the public static method
SshRetryHandler::retry(
function () use (&$attemptCount) {
$attemptCount++;
throw new \RuntimeException('Command not found');
},
['test' => 'non_retryable_test'],
true
);
} catch (\RuntimeException $e) {
// Should only attempt once since it's not retryable
$this->assertEquals(1, $attemptCount);
throw $e;
}
}
}

View file

@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.426" "version": "4.0.0-beta.427"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.427" "version": "4.0.0-beta.428"
}, },
"helper": { "helper": {
"version": "1.0.10" "version": "1.0.10"