Updated for v16. Includes a script to quickly stop running bench. Use: stop-bench
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" {}
# --- Parameters ---
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-16"
mutable = true
option {
name = "Version 16 (Stable) - Python 3.14"
value = "version-16"
}
option {
name = "Develop - Python 3.14"
value = "develop"
}
}
data "coder_parameter" "webserver_port" {
name = "webserver_port"
display_name = "Webserver Port"
description = "The port for the Frappe webserver."
default = "8001"
mutable = true
}
data "coder_parameter" "socketio_port" {
name = "socketio_port"
display_name = "SocketIO Port"
description = "The port for SocketIO."
default = "9001"
mutable = true
}
# --- Infrastructure ---
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"
lifecycle {
ignore_changes = all
}
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
}
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=%",
]
ports {
internal = 3306
external = 3306
protocol = "tcp"
}
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 = "150s"
retries = 5
}
lifecycle {
ignore_changes = all
}
}
resource "docker_container" "redis" {
image = "redis:alpine"
name = "redis-${data.coder_workspace.me.name}"
ports {
internal = 6379
external = 6379
protocol = "tcp"
}
healthcheck {
test = ["CMD", "redis-cli", "ping"]
interval = "150s"
timeout = "5s"
retries = 5
}
lifecycle {
ignore_changes = all
}
}
# --- Coder Agent ---
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
"WEBSERVER_PORT" = data.coder_parameter.webserver_port.value
"SOCKETIO_PORT" = data.coder_parameter.socketio_port.value
"DB_HOST" = "127.0.0.1"
"REDIS_CACHE" = "127.0.0.1:6379"
"REDIS_QUEUE" = "127.0.0.1:6379"
"REDIS_SOCKETIO" = "127.0.0.1:6379"
"SHELL" = "/usr/bin/zsh"
}
startup_script = <<-EOT
set -e
if ! command -v zsh >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y zsh git
fi
if [ ! -d "$HOME/.oh-my-zsh" ]; then
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}"
STOP_SCRIPT="/home/frappe/stop.py"
WEBSERVER_PORT="${data.coder_parameter.webserver_port.value}"
SOCKETIO_PORT="${data.coder_parameter.socketio_port.value}"
cat << 'EOF' > $STOP_SCRIPT
#!/usr/bin/env python
import os, socket, errno, sys
def stop_bench():
webserver_port = int(os.environ.get('WEBSERVER_PORT', '8001'))
socketio_port = int(os.environ.get('SOCKETIO_PORT', '9001'))
os.system("ps aux | grep 'frappe-bench/env/bin/python' | grep -v grep | awk '{print $2}' | xargs -r kill -9")
os.system("ps aux | grep 'apps/frappe/socketio.js' | grep -v grep | awk '{print $2}' | xargs -r kill -9")
os.system("pkill -f '^bench' || true")
for port in [webserver_port, socketio_port]:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind(("127.0.0.1", port))
except socket.error as e:
if e.errno == errno.EADDRINUSE:
os.system(f"fuser {port}/tcp -k >/dev/null 2>&1")
finally:
sock.close()
if __name__ == '__main__':
stop_bench()
print('--- Bench Stopped (IDE Connection Safe) ---')
EOF
chmod +x $STOP_SCRIPT
if ! grep -q "alias stop-bench" ~/.zshrc; then
echo "alias stop-bench='python3 $STOP_SCRIPT'" >> ~/.zshrc
echo "alias start-bench='bench start'" >> ~/.zshrc
echo "cd $PROJECT_DIR" >> ~/.zshrc
fi
BRANCH="${data.coder_parameter.frappe_version.value}"
PYTHON_VERSION="3.14"
[ "$BRANCH" = "develop" ] && PYTHON_VERSION="3.14"
if command -v uv >/dev/null 2>&1; then
uv python install $PYTHON_VERSION
fi
if [ ! -f "$PROJECT_DIR/Procfile" ]; then
bench init --skip-redis-config-generation --frappe-branch $BRANCH --python $PYTHON_VERSION $PROJECT_DIR
cd "$PROJECT_DIR"
bench set-config -g db_host 127.0.0.1
bench set-config -g redis_cache redis://127.0.0.1:6379
bench set-config -g redis_queue redis://127.0.0.1:6379
bench set-config -g redis_socketio redis://127.0.0.1:6379
bench set-config -g webserver_port $WEBSERVER_PORT
bench set-config -g socketio_port $SOCKETIO_PORT
sed -i "s/--port 8000/--port $WEBSERVER_PORT/" Procfile
sed -i "s/--bind 0.0.0.0:8000/--bind 0.0.0.0:$WEBSERVER_PORT/" Procfile
sed -i "s/--port 9000/--port $SOCKETIO_PORT/" Procfile
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
else
cd "$PROJECT_DIR"
# Update global config for localhost (host networking)
bench set-config -g db_host 127.0.0.1
bench set-config -g redis_cache redis://127.0.0.1:6379
bench set-config -g redis_queue redis://127.0.0.1:6379
bench set-config -g redis_socketio redis://127.0.0.1:6379
bench set-config -g webserver_port $WEBSERVER_PORT
bench set-config -g socketio_port $SOCKETIO_PORT
# Update site-specific config
if [ -f "sites/dev.localhost/site_config.json" ]; then
bench --site dev.localhost set-config redis_cache redis://127.0.0.1:6379 2>/dev/null || true
bench --site dev.localhost set-config redis_queue redis://127.0.0.1:6379 2>/dev/null || true
bench --site dev.localhost set-config redis_socketio redis://127.0.0.1:6379 2>/dev/null || true
fi
# Update Procfile to use configured ports if not already updated
if grep -q "bind 0.0.0.0:8000" Procfile 2>/dev/null; then
sed -i "s/--bind 0.0.0.0:8000/--bind 0.0.0.0:$WEBSERVER_PORT/" Procfile
echo "✅ Updated Procfile to use port $WEBSERVER_PORT"
fi
if grep -q "port 9000" Procfile 2>/dev/null; then
sed -i "s/--port 9000/--port $SOCKETIO_PORT/" Procfile
echo "✅ Updated Procfile to use socketio port $SOCKETIO_PORT"
fi
fi
echo "🎉 Ready! Frappe running on port $WEBSERVER_PORT (SocketIO: $SOCKETIO_PORT), Terminal set to Zsh in $PROJECT_DIR"
EOT
}
# --- Apps & Container ---
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 "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"
network_mode = "host"
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
volumes {
container_path = "/home/frappe"
volume_name = docker_volume.home_volume.name
read_only = false
}
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
}
}