Current section
Files
Jump to
Current section
Files
vps_deploy
README.md
README.md
# 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](CHANGELOG.md) for version history.
## Installation
Add `vps_deploy` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:vps_deploy, "~> 0.1.8", only: :dev}
]
end
```
Then fetch:
```bash
mix deps.get
```
## Setup
Run the interactive setup to generate your configuration:
```bash
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`.
Existing apps that already deploy with 0.1.7 need **no config changes**. New keys below are optional.
## Commands
| Command | What it does |
|---|---|
| `mix deploy.init` | Write `config :vps_deploy` interactively |
| `mix deploy.setup` | Passwordless sudo for systemctl (optional systemd unit) |
| `mix deploy.check` | Preflight SSH/sudo/Elixir/`.env` (no rsync, no build) |
| `mix deploy --dry-run` | Print rsync dest, excludes, and the remote script |
| `mix deploy` | rsync + remote build + restart |
| `mix deploy.status` | `systemctl status` |
| `mix deploy.logs` | last 100 journal lines |
| `mix deploy.restart` | `systemctl restart` and wait until active |
## Manual Configuration
You can also add the configuration manually:
```elixir
# 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 (never rewritten) |
| `delete_mix_lock` | no | `true` | Set `false` to keep `mix.lock` on the server |
| `assets` | no | `true` | `false` skips esbuild/tailwind/`assets.deploy`; `:detect` runs them only if those deps exist locally |
| `install_assets` | no | `true` | Set `false` to skip `esbuild.install` / `tailwind.install` (still runs `assets.deploy` unless `assets: false`) |
| `migrate_module` | no | `{App}.Release` | Module used as `eval "Module.migrate"` |
| `health_path` | no | unset | After systemd is active, `curl` this path on `127.0.0.1:$PORT` (skipped if `PORT` is missing) |
### Environment Variable Overrides
`VPS_USER` and `VPS_HOST` override config values at runtime:
```bash
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:
```bash
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.
It will also ask whether to install a systemd unit (**default: no**, so existing servers are left alone). Force it with:
```bash
mix deploy.setup --unit
mix deploy.setup --unit --enable
mix deploy.setup --ssh-user root
```
`--enable` runs `systemctl enable` only (no start/restart).
### First-time server (systemd unit)
If you prefer to write the unit yourself:
```ini
# /etc/systemd/system/my-app.service
[Unit]
Description=my_app
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/www/my_app
EnvironmentFile=/home/deploy/www/my_app/.env
ExecStart=/home/deploy/www/my_app/_build/prod/rel/my_app/bin/my_app start
ExecStop=/home/deploy/www/my_app/_build/prod/rel/my_app/bin/my_app stop
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable my-app
```
### Existing projects
If you have projects already deployed before this update, update the dependency and run setup:
```bash
mix deps.update vps_deploy
mix deploy.setup
```
Or configure sudoers manually on the server:
```bash
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
```bash
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`:
```elixir
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:
```bash
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]`:
```ini
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):
```bash
# 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](LICENSE.md) (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](CONTRIBUTING.md).