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}
]
endThen 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: trueOptions
| 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.lockSetting 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-appDeploying
mix deploy
This will:
- rsync your source code to the server (excluding configured paths)
- SSH into the server and run the build pipeline:
rm -f mix.lockmix deps.get --only prodMIX_ENV=prod mix tailwind.installMIX_ENV=prod mix compileMIX_ENV=prod mix assets.deployMIX_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/.envThis is per-service and fully isolated. Reload after edits with
sudo systemctl daemon-reload && sudo systemctl restart <service>.Interactive commands should load
.envonly 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
.envfile 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.