No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-23 14:46:41 +03:00
lib - prepare for public release 2026-08-23 13:28:37 +03:00
test - prepare for public release 2026-08-23 13:28:37 +03:00
.formatter.exs - prepare for public release 2026-08-23 13:28:37 +03:00
.gitignore - prepare for public release 2026-08-23 13:28:37 +03:00
CHANGELOG.md - docs added 2026-08-23 14:46:41 +03:00
CONTRIBUTING.md - prepare for public release 2026-08-23 13:28:37 +03:00
LICENSE.md - prepare for public release 2026-08-23 13:28:37 +03:00
mix.exs - docs added 2026-08-23 14:46:41 +03:00
README.md - install updated 2026-08-23 13:37:34 +03:00

VPS deploy

Simple VPS deployment for Phoenix applications via rsync + SSH.

Syncs your source code to a server, builds a release remotely, and restarts the systemd service — all with mix deploy.

See CHANGELOG.md for version history.

Installation

Add vps_deploy to your list of dependencies in mix.exs:

def deps do
  [
    {:vps_deploy, "~> 0.1.7", only: :dev}
  ]
end

Then fetch:

mix deps.get

Setup

Run the interactive setup to generate your configuration:

mix deploy.init

This will ask you for:

  • App name — your application name (e.g. my_app)
  • Service name — systemd service name (e.g. my-app)
  • VPS user — SSH user on the server
  • VPS host — server IP or domain
  • Deploy directory — where the app lives on the server
  • Use database — whether the app uses Ecto/database (default: yes)
  • Excludes — files/directories to skip during rsync

The configuration is written to your config/config.exs.

Manual Configuration

You can also add the configuration manually:

# config/config.exs
config :vps_deploy,
  app_name: "my_app",
  service_name: "my-app",
  vps_user: "deploy",
  vps_host: "1.2.3.4",
  deploy_dir: "/home/deploy/www/my_app",
  use_database: true

Options

Option Required Default Description
app_name yes Application name
service_name no "{app_name}-app" Systemd service name
vps_user no app_name SSH user
vps_host yes Server IP or hostname
deploy_dir no "/home/{vps_user}/www/{app_name}" Remote deploy path
use_database no true Set to false to skip Ecto migrations during deploy
excludes no see defaults below Files/dirs to exclude from rsync
remote_script no Phoenix build script Custom remote build script

Environment Variable Overrides

VPS_USER and VPS_HOST override config values at runtime:

VPS_HOST=staging.example.com mix deploy

Default Excludes

.expert _build deps docs .git .hex .mix .env
erl_crash.dump scripts .DS_Store
priv/static/files priv/static/uploads priv/uploads
AGENTS.md
.deploy.lock

Setting excludes still replaces the default list (existing configs keep working). .env and .deploy.lock are always excluded on top of that, so rsync --delete cannot wipe server secrets or a deploy in progress.

Server Setup (first time)

Before your first deploy, configure passwordless sudo for systemctl commands:

mix deploy.setup

This SSHs into your server and creates a sudoers rule so the deploy user can stop/start/restart the application service without a password prompt. You'll be asked for the sudo password once during setup.

If the deploy user doesn't have sudo access yet, specify a user that does:

mix deploy.setup --ssh-user root

Existing projects

If you have projects already deployed before this update, update the dependency and run setup:

mix deps.update vps_deploy
mix deploy.setup

Or configure sudoers manually on the server:

sudo visudo -f /etc/sudoers.d/YOUR-SERVICE-NAME

Add these lines (replace deploy with your user and my-app with your service name):

deploy ALL=(root) NOPASSWD: /usr/bin/systemctl stop my-app
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl start my-app
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart my-app
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl status my-app
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl status my-app --no-pager
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl is-active my-app
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl is-active --quiet my-app

Deploying

mix deploy

This will:

  1. rsync your source code to the server (excluding configured paths)
  2. SSH into the server and run the build pipeline:
    • rm -f mix.lock
    • mix deps.get --only prod
    • MIX_ENV=prod mix tailwind.install
    • MIX_ENV=prod mix compile
    • MIX_ENV=prod mix assets.deploy
    • MIX_ENV=prod mix release --overwrite
    • Stop the systemd service
    • Run migrations (skipped if use_database: false)
    • Start the systemd service
    • Health check (deploy fails if the service does not become active within about 60 seconds)

Custom Build Script

If your project has a different build pipeline, override remote_script:

config :vps_deploy,
  app_name: "my_app",
  vps_host: "1.2.3.4",
  remote_script: """
  set -e
  cd /home/deploy/www/my_app
  rm -f mix.lock
  mix deps.get --only prod
  MIX_ENV=prod mix compile
  MIX_ENV=prod mix release --overwrite
  sudo systemctl restart my-app
  """

Environment Variables

Each app reads its runtime configuration from a .env file at its deploy directory (e.g. /home/deploy/www/my_app/.env). This file is never synced — it lives only on the server (.env is in the default rsync excludes), so secrets stay server-side.

During deploy, the migration step loads .env for the duration of that single command:

set -a; . /home/deploy/www/my_app/.env; set +a

This is scoped — it does not leak into your shell or other apps.

Running multiple apps on one server

Do not source every app's .env from ~/.bashrc. .bashrc runs once per login shell, so all the files merge into one environment and any shared key (PHX_SERVER, PORT, SECRET_KEY_BASE, DATABASE_URL, …) is overwritten by whichever .env was sourced last — commands then run with the wrong app's config.

Instead:

  • The running services get their env from systemd. Each unit file (/etc/systemd/system/<service>.service) should have, under [Service]:

    EnvironmentFile=/home/deploy/www/my_app/.env
    

    This is per-service and fully isolated. Reload after edits with sudo systemctl daemon-reload && sudo systemctl restart <service>.

  • Interactive commands should load .env only for one command, in a subshell. Add this helper to ~/.bashrc (the function sets nothing until called):

    # usage: with-env /home/deploy/www/my_app mix ecto.migrate
    with-env() {
      local dir="$1"; shift
      ( set -a; . "$dir/.env"; set +a; cd "$dir" && "$@" )
    }
    

    The ( … ) subshell keeps each app's variables from clobbering another's.

Server Prerequisites

  • Elixir and Erlang installed on the server
  • A systemd service configured for your app
  • SSH key-based authentication
  • Passwordless sudo for systemctl commands (run mix deploy.setup)
  • A .env file at the deploy directory with runtime environment variables

License

Created and maintained by Rafael Egli. Copyright (c) 2026 e9li GmbH, Switzerland. Released under the MIT License (stated 2026-08-23): use it freely; it comes as is, without warranty. Rafael Egli and e9li GmbH are not responsible for problems caused by using this software. Tagged releases keep the license file they shipped with.

Contributing

Please open an issue on the GitHub mirror: https://github.com/e9li/vps_deploy/issues.

Pull requests are not accepted. The GitHub repo is for issues and browsing; the canonical source is https://git.e9li.com/e9li/vps_deploy. See CONTRIBUTING.md.