Packages

Simple VPS deployment for Phoenix applications via rsync + SSH.

Current section

Files

Jump to
vps_deploy README.md
Raw

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.7", 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`.
## 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 |
### 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.
If the deploy user doesn't have sudo access yet, specify a user that does:
```bash
mix deploy.setup --ssh-user root
```
### 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).