# 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).
