Here is a terraform template to quickly spin-up frappe development workspaces on Coder.
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
provider "docker" {
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "project_name" {
name = "project_name"
display_name = "Project Name"
description = "The name of the Frappe Bench folder."
default = "frappe-bench"
mutable = true
}
data "coder_parameter" "frappe_version" {
name = "frappe_version"
display_name = "Frappe Version"
description = "Select the Frappe branch to install."
default = "version-15"
mutable = true
option {
name = "Version 15 (Stable) - Python 3.11"
value = "version-15"
}
option {
name = "Develop (v16 Alpha) - Python 3.14"
value = "develop"
}
}
resource "docker_network" "frappe_network" {
name = "frappe-${data.coder_workspace.me.id}"
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
lifecycle {
ignore_changes = all
}
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
}
resource "docker_volume" "mariadb_data" {
name = "coder-${data.coder_workspace.me.id}-mariadb"
}
resource "docker_container" "mariadb" {
image = "mariadb:10.6"
name = "mariadb-${data.coder_workspace.me.name}"
command = [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
"--skip-character-set-client-handshake"
]
env = [
"MYSQL_ROOT_PASSWORD=123",
"MYSQL_ROOT_HOST=%"
]
networks_advanced {
name = docker_network.frappe_network.name
aliases = ["mariadb"]
}
volumes {
container_path = "/var/lib/mysql"
volume_name = docker_volume.mariadb_data.name
}
healthcheck {
test = ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p123"]
interval = "10s"
timeout = "5s"
retries = 5
}
}
resource "docker_container" "redis" {
image = "redis:alpine"
name = "redis-${data.coder_workspace.me.name}"
networks_advanced {
name = docker_network.frappe_network.name
aliases = ["redis-cache", "redis-queue", "redis-socketio"]
}
healthcheck {
test = ["CMD", "redis-cli", "ping"]
interval = "10s"
timeout = "5s"
retries = 5
}
}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
"FRAPPE_BRANCH" = data.coder_parameter.frappe_version.value
"DB_HOST" = "mariadb"
"REDIS_CACHE" = "redis-cache:6379"
"REDIS_QUEUE" = "redis-queue:6379"
"REDIS_SOCKETIO" = "redis-socketio:6379"
"SHELL" = "/usr/bin/zsh"
}
startup_script = <<-EOT
set -e
if ! command -v zsh >/dev/null 2>&1; then
echo "🎨 Installing Zsh..."
sudo apt-get update && sudo apt-get install -y zsh git
fi
if [ ! -d "$HOME/.oh-my-zsh" ]; then
echo "🖌️ Installing Oh My Zsh..."
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
sed -i 's/ZSH_THEME="robbyrussell"/ZSH_THEME="bira"/' ~/.zshrc
sed -i 's/plugins=(git)/plugins=(git python sudo)/' ~/.zshrc
sudo chsh -s /usr/bin/zsh $(whoami)
fi
PROJECT_DIR="/home/frappe/${data.coder_parameter.project_name.value}"
if ! grep -q "cd $PROJECT_DIR" ~/.zshrc; then
echo "" >> ~/.zshrc
echo "# Default to Bench Directory" >> ~/.zshrc
echo "cd $PROJECT_DIR" >> ~/.zshrc
fi
BRANCH="${data.coder_parameter.frappe_version.value}"
PYTHON_VERSION="3.11"
if [ "$BRANCH" = "develop" ]; then
echo "🚧 Branch is 'develop', switching to Python 3.14..."
PYTHON_VERSION="3.14"
fi
if command -v uv >/dev/null 2>&1; then
echo "🐍 Installing Python $PYTHON_VERSION via uv..."
uv python install $PYTHON_VERSION
fi
if [ ! -f "$PROJECT_DIR/Procfile" ]; then
echo "🚀 Initializing New Bench..."
bench init --skip-redis-config-generation --frappe-branch $BRANCH --python $PYTHON_VERSION $PROJECT_DIR
cd "$PROJECT_DIR"
bench set-config -g db_host mariadb
bench set-config -g redis_cache redis://redis-cache:6379
bench set-config -g redis_queue redis://redis-queue:6379
bench set-config -g redis_socketio redis://redis-socketio:6379
bench new-site dev.localhost --mariadb-root-password "123" --admin-password "admin" --mariadb-user-host-login-scope='%' --db-root-username root
bench --site dev.localhost set-config developer_mode 1
bench --site dev.localhost set-config ignore_csrf 1
bench --site dev.localhost set-config allow_cors '*'
bench use dev.localhost
fi
echo "🎉 Terminal set to Zsh in $PROJECT_DIR"
EOT
}
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
folder = "/home/frappe/${data.coder_parameter.project_name.value}"
order = 1
}
resource "coder_app" "frappe" {
agent_id = coder_agent.main.id
slug = "frappe"
display_name = "Frappe Desk"
url = "http://localhost:8000"
icon = "https://frappe.io/files/frappe-framework-logo.png"
subdomain = true
share = "owner"
healthcheck {
url = "http://localhost:8000"
interval = 5
threshold = 10
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "frappe/bench:latest"
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
user = "1000"
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = [
"CODER_AGENT_TOKEN=${coder_agent.main.token}"
]
networks_advanced {
name = docker_network.frappe_network.name
}
volumes {
container_path = "/home/frappe"
volume_name = docker_volume.home_volume.name
read_only = false
}
host {
host = "host.docker.internal"
ip = "host-gateway"
}
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
labels {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}