8 Commits
14 changed files with 104 additions and 360 deletions
-61
View File
@@ -1,61 +0,0 @@
name: Deploy Coolify to VPS
on:
push:
branches:
- main
- master
jobs:
deploy:
runs-on: ubuntu-latest
env:
# Inject secrets directly into Terraform via TF_VAR_ environment variables
TF_VAR_ovh_endpoint: "ovh-eu"
TF_VAR_ovh_application_key: ${{ secrets.OVH_APPLICATION_KEY }}
TF_VAR_ovh_application_secret: ${{ secrets.OVH_APPLICATION_SECRET }}
TF_VAR_ovh_consumer_key: ${{ secrets.OVH_CONSUMER_KEY }}
TF_VAR_ovh_service_name: ${{ secrets.OVH_SERVICE_NAME }}
TF_VAR_ssh_public_key: ${{ secrets.SSH_PUBLIC_KEY }}
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: "1.5.0"
- name: Terraform Init
run: |
terraform init \
-backend-config="address=${{ github.server_url }}/api/v1/packages/${{ github.repository_owner }}/terraform/state/coolify" \
-backend-config="username=${{ github.actor }}" \
-backend-config="password=${{ secrets.GITHUB_TOKEN }}"
working-directory: terraform
- name: Terraform Plan
run: terraform plan
working-directory: terraform
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: terraform
- name: Install Ansible
run: |
sudo apt-get update
sudo apt-get install -y ansible
- name: Configure SSH Private Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Run Ansible Playbook
run: |
ansible-playbook playbook.yml --private-key ~/.ssh/id_rsa
working-directory: ansible
+1
View File
@@ -8,6 +8,7 @@
# Ansible # Ansible
ansible/inventory.ini ansible/inventory.ini
.vault_pass
# Private Keys & Secrets # Private Keys & Secrets
*.pem *.pem
-163
View File
@@ -1,163 +0,0 @@
# Deploy Coolify to OVH VPS with Terraform, Ansible, and Gitea Actions
This repository contains all the configuration needed to dynamically provision a virtual private server (VPS) on OVH Cloud using **Terraform**, configure the VPS and install **Coolify** using **Ansible**, and automate the entire workflow via **Gitea Actions** on every push to the `main` or `master` branches.
---
## 🏗️ Architecture & Pipeline Flow
The deployment workflow is fully automated and consists of the following steps:
```mermaid
graph TD
A[Push to main/master] --> B[Gitea Actions Runner Starts]
B --> C[Terraform Initializes & Applies]
C -->|Creates SSH Key & Instance| D[OVH Public Cloud VPS]
C -->|Generates| E[ansible/inventory.ini]
B --> F[Ansible Environment Setup]
F -->|Reads inventory.ini| G[Ansible Playbook Run]
G -->|Configures Firewall & Installs| H[Coolify on VPS]
H --> I[Ready to Use on Port 8000/80/443]
```
1. **Gitea Actions** is triggered on a `push` to the default branch.
2. **Terraform** provisions an OVH Public Cloud instance (acting as our VPS), registers the deployment public SSH key on the instance, and outputs the public IP address.
3. **Terraform** dynamically creates the Ansible `inventory.ini` using the provisioned VPS public IP.
4. **Ansible** waits for SSH to become ready, updates system packages, configures the `ufw` firewall (opening ports 22, 80, 443, 8000, 6001), and runs the official non-interactive Coolify installation script safely and idempotently.
---
## 📁 Repository Structure
```text
├── .gitea/
│ └── workflows/
│ └── deploy.yml # Gitea Actions CI/CD workflow configuration
├── ansible/
│ ├── ansible.cfg # Ansible master configuration
│ ├── inventory.tpl # Template file for dynamic inventory generation
│ └── playbook.yml # Playbook to configure VPS and install Coolify
├── terraform/
│ ├── main.tf # Terraform core resources (OVH instance, SSH key)
│ ├── outputs.tf # Outputs the VPS IP address and Instance ID
│ ├── providers.tf # Configures the OVH Terraform Provider
│ └── variables.tf # Input variables with sensible defaults
├── .gitignore # Prevents secrets/state files from being committed
└── README.md # This documentation file
```
---
## ⚙️ Prerequisites
To run this pipeline, you will need:
1. **An OVH Cloud Account** with an active **Public Cloud Project**.
2. **OVH API Credentials** with permissions to manage Public Cloud resources.
3. **A Gitea Instance** with Gitea Actions enabled and a runner registered.
### 🔑 Step 1: Generate OVH API Credentials
Go to [OVH API Keys Creation Page](https://eu.api.ovh.com/createToken/) (or the corresponding URL for your OVH region, e.g. CA or US) and create a new set of API keys. Give your token access to `/cloud/*` and `/me/*` paths.
Once created, you will receive:
- `Application Key` (AK)
- `Application Secret` (AS)
- `Consumer Key` (CK)
You also need your **Public Cloud Project ID** (often called `Service Name`). This can be found on your OVH Public Cloud Dashboard (it is a string of letters and numbers like `sb123456-ovh`).
### 🗝️ Step 2: Generate an SSH Keypair
Generate an SSH keypair that Gitea Actions will use to authenticate with the new VPS:
```bash
ssh-keygen -t ed25519 -f id_rsa -N "" -C "gitea-actions-coolify"
```
This generates:
- `id_rsa` (Private key, to be saved in Gitea secrets)
- `id_rsa.pub` (Public key, to be saved in Gitea secrets)
---
## 🔒 Step 3: Configure Gitea Secrets
In your Gitea repository, navigate to **Settings > Actions > Secrets** and add the following secrets:
| Secret Name | Description | Example / Format |
| :--- | :--- | :--- |
| `OVH_APPLICATION_KEY` | Your OVH API Application Key | `xxxxxx` |
| `OVH_APPLICATION_SECRET` | Your OVH API Application Secret | `xxxxxx` |
| `OVH_CONSUMER_KEY` | Your OVH API Consumer Key | `xxxxxx` |
| `OVH_SERVICE_NAME` | Your OVH Public Cloud Project ID | `ab123456-ovh-eu-1` |
| `SSH_PUBLIC_KEY` | Content of the generated `id_rsa.pub` | `ssh-ed25519 AAAAC3...` |
| `SSH_PRIVATE_KEY` | Content of the generated `id_rsa` | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
---
## 💻 Running Terraform Locally
If you want to run Terraform locally to inspect, plan, or deploy from your own machine, you can do so while keeping the state synchronized with your Gitea Actions pipeline.
### Step 1: Install Terraform
Make sure you have the Terraform CLI installed (`>= 1.3.0`).
### Step 2: Configure Local Credentials
Create a file named `terraform.tfvars` inside the `terraform/` directory. *(Note: This file is already configured in `.gitignore` to prevent committing credentials).*
```hcl
ovh_endpoint = "ovh-eu"
ovh_application_key = "your_ovh_application_key"
ovh_application_secret = "your_ovh_application_secret"
ovh_consumer_key = "your_ovh_consumer_key"
ovh_service_name = "your_ovh_public_cloud_project_id"
ssh_public_key = "your_ssh_public_key_content" # or file("~/.ssh/id_rsa.pub")
```
### Step 3: Initialize with Gitea State Backend
To keep your state synchronized with Gitea Actions, run `terraform init` locally and point it to Gitea's built-in Terraform State Registry:
```bash
cd terraform
terraform init \
-backend-config="address=https://<your-gitea-domain>/api/v1/packages/<your-gitea-username>/terraform/state/coolify" \
-backend-config="username=<your-gitea-username>" \
-backend-config="password=<your-gitea-token-or-password>"
```
### Step 4: Plan and Apply
Now you can plan and apply your local changes. They will automatically read from and write to the exact same state file as Gitea Actions:
```bash
# Preview changes
terraform plan
# Apply changes to provision/update the VPS
terraform apply
```
---
## 🛠️ Customization
You can customize the deployment by overriding the default values in `terraform/variables.tf`:
- **OVH Endpoint (`ovh_endpoint`)**: Defaults to `ovh-eu`. Change to `ovh-ca` or `ovh-us` depending on your account region.
- **Region (`region`)**: Defaults to `GRA11` (Gravelines, France). Other popular regions: `SBG5` (Strasbourg), `WAW1` (Warsaw), `DE1` (Frankfurt), `UK1` (London).
- **Instance Size/Flavor (`flavor_name`)**: Defaults to `b2-7` (2 vCPUs, 7 GB RAM). If you want a smaller/starter setup, you can use `s1-2` (1 vCPU, 2 GB RAM) or `d2-4` (2 vCPUs, 4 GB RAM). Coolify operates best with at least 2 vCPUs and 4 GB RAM.
- **Operating System (`image_name`)**: Defaults to `Ubuntu 22.04`. Coolify supports Ubuntu 22.04/24.04 and Debian 11/12 out of the box.
---
## 🚀 Post-Deployment: Accessing Coolify
Once the Gitea Actions pipeline completes successfully:
1. Copy the public IP address of the server from the Terraform output or the Gitea Actions log.
2. Open your browser and navigate to `http://<your-vps-ip>:8000`.
3. Register your administrator account.
4. Set up your wild-card domain or custom domains inside Coolify!
Enjoy your brand-new, self-hosted, enterprise-grade PaaS on OVH VPS!
+1 -2
View File
@@ -1,5 +1,4 @@
[defaults] [defaults]
inventory = inventory.ini inventory = inventory.ini
vault_password_file = .vault_pass
host_key_checking = False host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
+5
View File
@@ -0,0 +1,5 @@
---
ansible_user: "debian"
sysadmin_user: "alessandrovitali"
sysadmin_public_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAm/J+9YG+odym9In9C4iLcrfXlrlPK2TygtI7lBNNpl"
+9
View File
@@ -0,0 +1,9 @@
$ANSIBLE_VAULT;1.1;AES256
65353035363835666333386634353332383639633462326134656533366434373363333636613938
3163306136623962346363373030643330343364333139360a376334666363396337613631303663
34376561376439353863646561616134636230366435653431643633313130356665313163663136
3837396262376433350a646430383332633931383561383939336561616235336232623665316537
37353537303138646630393733626262653362636661613336336565636162343835386265343262
66323466616364306363313638633630343766313065646135336138396639343536326162393562
34303234653937393637643031643066383537363632383663303062303230613135363336326135
63366363353663366137
+3
View File
@@ -0,0 +1,3 @@
coolify_root_username: "alessandrovitali"
coolify_root_user_email: "[email protected]"
coolify_root_user_password: "{{ vault_coolify_admin_password }}"
+2 -2
View File
@@ -1,2 +1,2 @@
[coolify_vps] [coolify]
${vps_ip} ansible_user=ubuntu coolify ansible_host=${public_ip}
+8 -61
View File
@@ -1,62 +1,9 @@
--- ## ansible/setup.yml
- name: Configure VPS and Install Coolify
hosts: coolify_vps
become: true
gather_facts: true
tasks: - name: Setup coolify machine
- name: Wait for VPS to become reachable via SSH hosts: all
ansible.builtin.wait_for_connection: roles:
delay: 5 - role: studio.ansible.base
timeout: 300 - role: studio.ansible.users
- role: studio.ansible.security
- name: Update apt package cache - role: studio.ansible.coolify
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
- name: Install system prerequisites
ansible.builtin.apt:
name:
- curl
- git
- ufw
- jq
state: present
- name: Configure UFW firewall
block:
- name: Allow SSH traffic
ansible.builtin.shell: ufw allow 22/tcp
- name: Allow HTTP traffic
ansible.builtin.shell: ufw allow 80/tcp
- name: Allow HTTPS traffic
ansible.builtin.shell: ufw allow 443/tcp
- name: Allow Coolify Console traffic
ansible.builtin.shell: ufw allow 8000/tcp
- name: Allow Realtime/Websocket traffic
ansible.builtin.shell: ufw allow 6001/tcp
- name: Enable UFW firewall
ansible.builtin.shell: echo "y" | ufw enable
rescue:
- name: Log firewall configuration failure
ansible.builtin.debug:
msg: "Failed to set up firewall, continuing with installation."
- name: Check if Coolify is already installed
ansible.builtin.stat:
path: /data/coolify/source/.env
register: coolify_installed
- name: Install Coolify via official installation script
ansible.builtin.shell: |
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
register: install_output
when: not coolify_installed.stat.exists
async: 600
poll: 10
- name: Print installation output
ansible.builtin.debug:
var: install_output.stdout_lines
when: install_output.changed
+7
View File
@@ -0,0 +1,7 @@
## ansible/requirements.yml
collections:
- name: studio.ansible
source: https://git.studiovita.li/studio/ansible.git ## HTTPS
type: git
version: feat/coolify
+46 -39
View File
@@ -1,54 +1,61 @@
resource "ovh_cloud_project_keypair" "deploy_key" { # Gather facts
data "ovh_cloud_instance_flavors" "flavors" {
service_name = var.ovh_service_name service_name = var.ovh_service_name
name = "coolify_deploy_key" region = var.ovh_region
}
data "ovh_cloud_instance_images" "images" {
service_name = var.ovh_service_name
region = var.ovh_region
}
resource "ovh_cloud_ssh_key" "ansible" {
service_name = var.ovh_service_name
name = "ansible-key"
public_key = var.ssh_public_key public_key = var.ssh_public_key
region = var.region
} }
# 1. Look up the hardware flavor ID dynamically using the flavor name # Provision a d2-2 Public Cloud instance
data "ovh_cloud_project_flavors" "flavor" { resource "ovh_cloud_instance" "coolify" {
service_name = var.ovh_service_name
region = var.region
name_filter = var.flavor_name
}
# 2. Look up the image ID list dynamically for the region
data "ovh_cloud_project_images" "images" {
service_name = var.ovh_service_name
region = var.region
}
resource "ovh_cloud_project_instance" "coolify" {
service_name = var.ovh_service_name service_name = var.ovh_service_name
name = var.instance_name region = var.ovh_region
region = var.region name = var.project_name
billing_period = "hourly" ssh_key_name = ovh_cloud_ssh_key.ansible.name
# Use flavor block with the retrieved ID flavor_id = one([
flavor { for flavor in data.ovh_cloud_instance_flavors.flavors.flavors :
flavor_id = tolist(data.ovh_cloud_project_flavors.flavor.flavors)[0].id flavor.id if flavor.name == var.ovh_flavor_name
} ])
# Use boot_from block to filter and select the correct image ID image_id = one([
boot_from { for image in data.ovh_cloud_instance_images.images.images :
image_id = [for img in data.ovh_cloud_project_images.images.images : img.id if img.name == var.image_name][0] image.id if image.name == var.image_name
} ])
# Use network block to assign a public IP networks = [
network { { auto_assign_public_ip = true },
public = true ]
}
# Use ssh_key block to attach the deploy key
ssh_key {
name = ovh_cloud_project_keypair.deploy_key.name
}
} }
# Generate inventory file for Ansible (using the updated addresses structure) # Fetch provisioned VM's public IPv4
locals {
public_ip = one(flatten([
for net in ovh_cloud_instance.coolify.current_state.networks : [
for addr in net.addresses : addr.ip
if addr.type == "FIXED" && addr.version == 4
]
]))
}
# Write IP to Ansible inventory
resource "local_file" "ansible_inventory" { resource "local_file" "ansible_inventory" {
content = templatefile("${path.module}/../ansible/inventory.tpl", { content = templatefile("${path.module}/../ansible/inventory.tpl", {
vps_ip = [for addr in ovh_cloud_project_instance.coolify.addresses : addr.ip if addr.version == 4][0] public_ip = local.public_ip
}) })
filename = "${path.module}/../ansible/inventory.ini" filename = "${path.module}/../ansible/inventory.ini"
} }
output "public_ip" {
description = "The public IPv4 address of the provisioned OVHcloud VM"
value = local.public_ip
}
-9
View File
@@ -1,9 +0,0 @@
output "vps_ip" {
description = "The public IPv4 address of the Coolify VPS"
value = [for addr in ovh_cloud_project_instance.coolify.addresses : addr.ip if addr.version == 4][0]
}
output "instance_id" {
description = "The ID of the provisioned OVH instance"
value = ovh_cloud_project_instance.coolify.id
}
+6 -3
View File
@@ -1,12 +1,15 @@
terraform { terraform {
required_version = ">= 1.3.0" required_version = ">= 1.0.0"
required_providers { required_providers {
ovh = { ovh = {
source = "ovh/ovh" source = "ovh/ovh"
version = "~> 0.43.0" version = "~> 2.19.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.9.0"
} }
} }
backend "http" {} # Allows local and Gitea Actions to share state securely
} }
provider "ovh" { provider "ovh" {
+16 -20
View File
@@ -1,57 +1,53 @@
variable "project_name" {
type = string
description = "VM name"
}
variable "ovh_endpoint" { variable "ovh_endpoint" {
type = string type = string
description = "OVH API endpoint (e.g., ovh-eu, ovh-us, ovh-ca)" description = "OVH API endpoint"
default = "ovh-eu" default = "ovh-eu"
} }
variable "ovh_application_key" { variable "ovh_application_key" {
type = string type = string
description = "OVH Application Key"
sensitive = true sensitive = true
} }
variable "ovh_application_secret" { variable "ovh_application_secret" {
type = string type = string
description = "OVH Application Secret"
sensitive = true sensitive = true
} }
variable "ovh_consumer_key" { variable "ovh_consumer_key" {
type = string type = string
description = "OVH Consumer Key"
sensitive = true sensitive = true
} }
variable "ovh_service_name" { variable "ovh_service_name" {
type = string type = string
description = "OVH Public Cloud Project ID (Service Name)" description = "OVHcloud Public Cloud Project ID"
} }
variable "region" { variable "ovh_region" {
type = string type = string
description = "OVH Region to deploy the VPS in (e.g., GRA11, SBG5, WAW1)" description = "OVHcloud region code"
default = "GRA11" default = "DE1" # Frankfurt, Germany
} }
variable "instance_name" { variable "ovh_flavor_name" {
type = string type = string
description = "Name of the VPS/Instance" description = "VM flavor name"
default = "coolify-vps" default = "d2-2"
}
variable "flavor_name" {
type = string
description = "The hardware flavor / size of the VPS (Coolify recommends at least 2 vCPUs and 4GB RAM)"
default = "b2-7" # 2 vCPUs, 7GB RAM (excellent for Coolify)
} }
variable "image_name" { variable "image_name" {
type = string type = string
description = "The OS Image name" description = "OS distribution image name to boot the VM from"
default = "Ubuntu 22.04" default = "Debian 13"
} }
variable "ssh_public_key" { variable "ssh_public_key" {
type = string type = string
description = "SSH public key content to be authorized on the VPS" description = "Public SSH key to install on the VM"
} }