feat: add coolify

This commit is contained in:
Yuris Cakranegara
2025-06-09 20:54:10 +10:00
parent 27ce999c97
commit b48f81ed9d
8 changed files with 621 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
}
}
}
# Generate Caddyfile content
locals {
# Generate the Caddyfile header
caddyfile_header = "# Global options\n{\n admin :2019\n}\n\n"
# Generate site configurations - using separate HTTP and HTTPS blocks but with different hostnames
site_blocks = flatten([
for site in var.sites : [
# HTTPS configuration
"# Site configuration for ${site.domain} (HTTPS)",
"${site.domain}:${var.https_port} {",
" # TLS configuration",
" tls internal",
"",
" # Route configurations",
join("\n\n ", [
for route in site.routes : <<-ROUTE
# Route: ${route.path}
handle ${route.path} {
reverse_proxy ${route.target_host}:${route.target_port} {
${route.websocket ? "# WebSocket protocol handling\n header_up Connection \"Upgrade\"\n header_up Upgrade \"websocket\"" : ""}
}
}
ROUTE
]),
"}",
"",
# HTTP configuration with redirect to HTTPS
"# HTTP redirect for ${site.domain}",
":${var.http_port} {",
" redir https://${site.domain}:${var.https_port}{uri} permanent",
"}"
]
])
# Combine everything into the final Caddyfile
caddyfile_content = "${local.caddyfile_header}${join("\n\n", local.site_blocks)}"
# Define volumes for Caddy
volumes = [
{
host_path = "${var.volume_path}/Caddyfile"
container_path = "/etc/caddy/Caddyfile"
read_only = false
},
{
host_path = "${var.volume_path}/data"
container_path = "/data"
read_only = false
},
{
host_path = "${var.volume_path}/config"
container_path = "/config"
read_only = false
}
]
# Create data directories if they don't exist
data_directories = [
var.volume_path,
"${var.volume_path}/data",
"${var.volume_path}/config"
]
# Environment variables - convert to the format docker_container resource expects
env_vars = var.networks != [] ? ["CADDY_INGRESS_NETWORKS=${join(",", var.networks)}"] : []
# Health check
healthcheck = {
test = ["CMD", "wget", "--spider", "--quiet", "http://localhost:80/healthz"]
interval = "30s"
timeout = "3s"
retries = 3
}
}
# Create data directories
resource "null_resource" "create_directories" {
count = length(local.data_directories)
provisioner "local-exec" {
command = "mkdir -p ${local.data_directories[count.index]}"
}
}
# Create Caddyfile
resource "local_file" "caddyfile" {
content = local.caddyfile_content
filename = "${var.volume_path}/Caddyfile"
depends_on = [null_resource.create_directories]
}
# Pull the image
resource "docker_image" "caddy" {
name = "${var.image}:${var.tag}"
}
# Create the container
resource "docker_container" "caddy" {
name = var.container_name
image = docker_image.caddy.image_id
restart = "unless-stopped"
# Map ports similar to Nginx Proxy Manager
ports {
internal = 80
external = var.http_port
protocol = "tcp"
}
ports {
internal = 443
external = var.https_port
protocol = "tcp"
}
# Admin interface port
ports {
internal = 2019 # Caddy admin API port
external = var.admin_port
protocol = "tcp"
}
# Set up volumes
dynamic "volumes" {
for_each = local.volumes
content {
host_path = volumes.value.host_path
container_path = volumes.value.container_path
read_only = volumes.value.read_only
}
}
# Set environment variables as a list of strings in KEY=VALUE format
env = local.env_vars
# Set networks
dynamic "networks_advanced" {
for_each = var.networks
content {
name = networks_advanced.value
}
}
# Set health check
healthcheck {
test = local.healthcheck.test
interval = local.healthcheck.interval
timeout = local.healthcheck.timeout
retries = local.healthcheck.retries
}
# Add watchtower label if monitoring is enabled
labels {
label = "com.centurylinklabs.watchtower.enable"
value = var.monitoring ? "true" : "false"
}
# Make sure Caddyfile is created before starting container
depends_on = [local_file.caddyfile]
}

View File

@@ -0,0 +1,19 @@
output "container_name" {
description = "Name of the Caddy proxy container"
value = docker_container.caddy.name
}
output "container_ip" {
description = "IP address of the Caddy container"
value = docker_container.caddy.network_data[0].ip_address
}
output "domains" {
description = "Domains being proxied by Caddy"
value = [for site in var.sites : site.domain]
}
output "ready" {
description = "Boolean indicating if Caddy is ready"
value = docker_container.caddy.id != ""
}

View File

@@ -0,0 +1,66 @@
variable "container_name" {
description = "The name of the Caddy container"
type = string
default = "caddy-proxy"
}
variable "http_port" {
description = "External HTTP port mapping"
type = number
default = 9080
}
variable "https_port" {
description = "External HTTPS port mapping"
type = number
default = 9443
}
variable "admin_port" {
description = "External admin port mapping"
type = number
default = 9081
}
variable "image" {
description = "The image to use for the Caddy container"
type = string
default = "caddy"
}
variable "tag" {
description = "The tag of the Caddy image"
type = string
default = "latest"
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
default = "/mnt/appdata/caddy"
}
variable "monitoring" {
description = "Enable or disable monitoring"
type = bool
default = true
}
variable "networks" {
description = "List of networks to attach to the Caddy container"
type = list(string)
default = []
}
variable "sites" {
description = "List of sites to proxy"
type = list(object({
domain = string # Domain name (e.g. deploy.yuris.dev)
routes = list(object({
path = string # Path to match (e.g. "/app/*" or "/" for root)
target_host = string # Target host (e.g. coolify)
target_port = number # Target port (e.g. 8080)
websocket = bool # Whether this route should be treated as websocket
}))
}))
}

View File

@@ -0,0 +1,29 @@
REGISTRY_URL=ghcr.io
# App Configuration
APP_ID=
APP_NAME=Coolify
APP_KEY=
ROOT_USERNAME=
ROOT_USER_EMAIL=
ROOT_USER_PASSWORD=
PHP_MEMORY_LIMIT=256M
PHP_FPM_PM_CONTROL=dynamic
PHP_FPM_PM_START_SERVERS=1
PHP_FPM_PM_MIN_SPARE_SERVERS=1
PHP_FPM_PM_MAX_SPARE_SERVERS=10
# Database Configuration
DB_DATABASE=coolify
DB_USERNAME=coolify
DB_PASSWORD=GENERATE_RANDOM_STRING
# Redis Configuration
REDIS_PASSWORD=GENERATE_RANDOM_STRING
# Pusher/Soketi Configuration
APP_NAME=Coolify
SOKETI_DEBUG=false
PUSHER_APP_ID=GENERATE_RANDOM_HEXADECIMAL
PUSHER_APP_KEY=GENERATE_RANDOM_HEXADECIMAL
PUSHER_APP_SECRET=GENERATE_RANDOM_HEXADECIMAL

View File

@@ -0,0 +1,37 @@
module "coolify_caddy" {
source = "../../10-services-generic/caddy-proxy"
container_name = "coolify-caddy"
volume_path = "${var.volume_path}/caddy"
networks = concat([module.coolify_network.name], var.networks)
# Use custom ports (not exposing 80/443 directly)
http_port = 7080
https_port = 6443
admin_port = 7081
sites = [
{
domain = "deploy.yuris.dev"
routes = [
{
path = "/app/*" # Main WebSocket endpoint for Pusher/Soketi
target_host = local.soketi_container_name
target_port = local.soketi_port
websocket = true
},
{
path = "/apps/*" # Alternative WebSocket endpoint
target_host = local.soketi_container_name
target_port = local.soketi_port
websocket = true
},
{
path = "/*" # All other requests go to the main app
target_host = local.app_container_name
target_port = local.app_port
websocket = false
}
]
}
]
}

View File

@@ -0,0 +1,260 @@
terraform {
required_providers {
dotenv = {
source = "germanbrew/dotenv"
}
}
}
variable "image_tag" {
description = "The tag for the coolify container image"
type = string
default = "latest"
}
variable "volume_path" {
description = "Base directory for volumes"
type = string
}
variable "networks" {
description = "List of networks to which the coolify container should be attached"
type = list(string)
default = []
}
locals {
env_file = "${path.module}/.env"
# Container names
app_container_name = "coolify"
db_container_name = "coolify-db"
redis_container_name = "coolify-redis"
soketi_container_name = "coolify-realtime"
# Images and tags
app_image = "ghcr.io/coollabsio/coolify"
db_image = "postgres"
redis_image = "redis"
soketi_image = "ghcr.io/coollabsio/coolify-realtime"
app_tag = var.image_tag != "" ? var.image_tag : "latest"
db_tag = "15-alpine"
redis_tag = "7-alpine"
soketi_tag = "1.0.8"
monitoring = true
app_port = 8080
soketi_port = 6001
# Volume mappings
app_volumes = [
{
host_path = "${var.volume_path}/source/.env"
container_path = "/var/www/html/.env"
read_only = true
},
{
host_path = "${var.volume_path}/ssh"
container_path = "/var/www/html/storage/app/ssh"
read_only = false
},
{
host_path = "${var.volume_path}/applications"
container_path = "/var/www/html/storage/app/applications"
read_only = false
},
{
host_path = "${var.volume_path}/databases"
container_path = "/var/www/html/storage/app/databases"
read_only = false
},
{
host_path = "${var.volume_path}/services"
container_path = "/var/www/html/storage/app/services"
read_only = false
},
{
host_path = "${var.volume_path}/backups"
container_path = "/var/www/html/storage/app/backups"
read_only = false
},
{
host_path = "${var.volume_path}/webhooks-during-maintenance"
container_path = "/var/www/html/storage/app/webhooks-during-maintenance"
read_only = false
}
]
db_volumes = [
{
host_path = "${var.volume_path}/db_data"
container_path = "/var/lib/postgresql/data"
read_only = false
}
]
redis_volumes = [
{
host_path = "${var.volume_path}/redis_data"
container_path = "/data"
read_only = false
}
]
soketi_volumes = [
{
host_path = "${var.volume_path}/ssh"
container_path = "/var/www/html/storage/app/ssh"
read_only = false
}
]
app_env_vars = {
APP_ENV = "production"
APP_ID = provider::dotenv::get_by_key("APP_ID", local.env_file)
APP_NAME = provider::dotenv::get_by_key("APP_NAME", local.env_file)
APP_KEY = provider::dotenv::get_by_key("APP_KEY", local.env_file)
ROOT_USERNAME = provider::dotenv::get_by_key("ROOT_USERNAME", local.env_file)
ROOT_USER_EMAIL = provider::dotenv::get_by_key("ROOT_USER_EMAIL", local.env_file)
ROOT_USER_PASSWORD = provider::dotenv::get_by_key("ROOT_USER_PASSWORD", local.env_file)
PHP_MEMORY_LIMIT = provider::dotenv::get_by_key("PHP_MEMORY_LIMIT", local.env_file)
PHP_FPM_PM_CONTROL = provider::dotenv::get_by_key("PHP_FPM_PM_CONTROL", local.env_file)
PHP_FPM_PM_START_SERVERS = provider::dotenv::get_by_key("PHP_FPM_PM_START_SERVERS", local.env_file)
PHP_FPM_PM_MIN_SPARE_SERVERS = provider::dotenv::get_by_key("PHP_FPM_PM_MIN_SPARE_SERVERS", local.env_file)
PHP_FPM_PM_MAX_SPARE_SERVERS = provider::dotenv::get_by_key("PHP_FPM_PM_MAX_SPARE_SERVERS", local.env_file)
DB_DATABASE = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
DB_USERNAME = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
DB_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
REDIS_PASSWORD = provider::dotenv::get_by_key("REDIS_PASSWORD", local.env_file)
}
db_env_vars = {
POSTGRES_USER = provider::dotenv::get_by_key("DB_USERNAME", local.env_file)
POSTGRES_PASSWORD = provider::dotenv::get_by_key("DB_PASSWORD", local.env_file)
POSTGRES_DB = provider::dotenv::get_by_key("DB_DATABASE", local.env_file)
}
redis_env_vars = {
REDIS_PASSWORD = provider::dotenv::get_by_key("REDIS_PASSWORD", local.env_file)
}
soketi_env_vars = {
APP_NAME = provider::dotenv::get_by_key("APP_NAME", local.env_file)
SOKETI_DEBUG = provider::dotenv::get_by_key("SOKETI_DEBUG", local.env_file)
SOKETI_DEFAULT_APP_ID = provider::dotenv::get_by_key("PUSHER_APP_ID", local.env_file)
SOKETI_DEFAULT_APP_KEY = provider::dotenv::get_by_key("PUSHER_APP_KEY", local.env_file)
SOKETI_DEFAULT_APP_SECRET = provider::dotenv::get_by_key("PUSHER_APP_SECRET", local.env_file)
}
healthchecks = {
db = {
test = ["CMD-SHELL", "pg_isready -U coolify -d coolify"]
interval = "5s"
timeout = "2s"
retries = 10
}
redis = {
test = ["CMD", "redis-cli", "ping"]
interval = "5s"
timeout = "2s"
retries = 10
}
soketi = {
test = ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1"]
interval = "5s"
timeout = "2s"
retries = 10
}
app = {
test = ["CMD-SHELL", "curl --fail http://127.0.0.1:8080/api/health || exit 1"]
interval = "5s"
timeout = "2s"
retries = 10
}
}
}
module "coolify_network" {
source = "../../01-networking/docker-network"
name = "coolify"
driver = "bridge"
}
module "postgres" {
source = "../../10-services-generic/docker-service"
container_name = local.db_container_name
image = local.db_image
tag = local.db_tag
volumes = local.db_volumes
puid = 9999
pgid = 9999
env_vars = local.db_env_vars
networks = [module.coolify_network.name]
monitoring = local.monitoring
healthcheck = local.healthchecks.db
}
module "redis" {
source = "../../10-services-generic/docker-service"
container_name = local.redis_container_name
image = local.redis_image
tag = local.redis_tag
volumes = local.redis_volumes
puid = 9999
pgid = 9999
env_vars = local.redis_env_vars
networks = [module.coolify_network.name]
monitoring = local.monitoring
command = ["redis-server", "--save", "20", "1", "--loglevel", "warning", "--requirepass", provider::dotenv::get_by_key("REDIS_PASSWORD", local.env_file)]
healthcheck = local.healthchecks.redis
}
module "soketi" {
source = "../../10-services-generic/docker-service"
container_name = local.soketi_container_name
image = local.soketi_image
tag = local.soketi_tag
volumes = local.soketi_volumes
puid = 9999
pgid = 9999
env_vars = local.soketi_env_vars
networks = [module.coolify_network.name]
monitoring = local.monitoring
healthcheck = local.healthchecks.soketi
}
module "coolify" {
source = "../../10-services-generic/docker-service"
container_name = local.app_container_name
image = local.app_image
tag = local.app_tag
volumes = local.app_volumes
puid = 9999
pgid = 9999
env_vars = local.app_env_vars
networks = concat([module.coolify_network.name], var.networks)
monitoring = local.monitoring
healthcheck = local.healthchecks.app
host_mappings = [
{
host = "host.docker.internal"
ip = "host-gateway"
}
]
depends_on = [module.postgres, module.redis, module.soketi]
}
output "service_definition" {
description = "Service definition with ingress configuration"
value = {
name = local.app_container_name
primary_port = local.app_port
endpoint = "http://${local.app_container_name}:${local.app_port}"
subdomains = ["deploy"]
}
}