Clone
defmodule Mix.Tasks.Deploy.Service do
use Mix.Task
@shortdoc "Update systemd service on server"
@moduledoc """
Update systemd service configuration for the deployed Phoenix application.
## Usage
mix deploy.service # Update service for production
mix deploy.service staging # Update service for staging
This task will:
1. Upload the service file from deploy/
2. Replace template variables with actual values
3. Reload systemd daemon
4. Restart the service
"""
require Logger
@impl Mix.Task
def run(args) do
# Load deployment config
Mix.Task.run("loadconfig", ["config/deploy.exs"])
env = case args do
[] -> :production
[env] -> String.to_atom(env)
_ ->
Logger.error("Too many arguments. Usage: mix deploy.service [environment]")
exit(1)
end
config = get_deploy_config(env)
Logger.info("Updating systemd service for #{env} environment...")
with :ok <- check_requirements(config),
:ok <- upload_service_file(config),
:ok <- reload_and_restart_service(config) do
Logger.info("✅ Systemd service updated successfully!")
else
{:error, reason} ->
Logger.error("❌ Failed to update systemd service: #{reason}")
exit(1)
end
end
defp get_deploy_config(env), do: Deploy.SSH.get_deploy_config(env)
defp check_requirements(config) do
required_keys = [:user, :domain, :deploy_to, :app_port, :app_name]
missing = Enum.filter(required_keys, &(not Map.has_key?(config, &1)))
if Enum.empty?(missing) do
:ok
else
{:error, "Missing required config keys: #{inspect(missing)}"}
end
end
defp upload_service_file(config) do
# Look for service file
service_files = Path.wildcard("deploy/*.service")
service_file = case service_files do
[] -> nil
[file] -> file
files ->
# Prefer one that matches app name
Enum.find(files, hd(files), &String.contains?(&1, config.app_name))
end
if service_file do
# Service name is {app_name}-{env} (e.g., testapp-staging, testapp-production)
service_name = "#{config.app_name}-#{config.env}"
Logger.info("Uploading service file from #{service_file} as #{service_name}...")
# Read template
template = File.read!(service_file)
# Extract host from URL
host = case URI.parse(Map.get(config, :url, "")) do
%{host: nil} -> "localhost"
%{host: h} -> h
end
# Replace variables
content = template
|> String.replace("${DEPLOY_TO}", Map.get(config, :deploy_to, "/var/www/#{config.app_name}"))
|> String.replace("${APP_PORT}", to_string(config.app_port))
|> String.replace("${PHX_HOST}", host)
# Ensure PHX_SERVER=true is set
content = if not String.contains?(content, "PHX_SERVER") do
String.replace(content, "Environment=\"PORT=", "Environment=\"PHX_SERVER=true\"\nEnvironment=\"PORT=")
else
content
end
# Create temp file
temp_file = Path.join(System.tmp_dir!(), "#{service_name}.service")
File.write!(temp_file, content)
# Upload to server
remote_temp = "/tmp/#{service_name}.service"
case scp_upload(config, temp_file, remote_temp) do
{_, 0} ->
# Install service
install_cmd = """
sudo cp #{remote_temp} /etc/systemd/system/#{service_name}.service && \
sudo systemctl daemon-reload
"""
case ssh_exec(config, install_cmd) do
{_, 0} ->
File.rm(temp_file)
:ok
{output, _} ->
File.rm(temp_file)
{:error, "Failed to install service: #{output}"}
end
{output, _} ->
File.rm(temp_file)
{:error, "Failed to upload service file: #{output}"}
end
else
{:error, "No service file found in deploy/"}
end
end
defp reload_and_restart_service(config) do
# Service name is {app_name}-{env} (e.g., testapp-staging, testapp-production)
service_name = "#{config.app_name}-#{config.env}"
Logger.info("Restarting service: #{service_name}")
restart_cmd = """
sudo systemctl daemon-reload && \
sudo systemctl restart #{service_name} && \
sudo systemctl status #{service_name} | head -n 10
"""
case ssh_exec(config, restart_cmd) do
{output, 0} ->
Logger.info(output)
:ok
{output, _} ->
{:error, "Failed to restart service: #{output}"}
end
end
defp ssh_exec(config, command), do: Deploy.SSH.ssh_exec(config, command)
defp scp_upload(config, local_file, remote_file), do: Deploy.SSH.scp_upload(config, local_file, remote_file)
end