We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Here's a transcript of how you can deploy an Elixir Phoenix web application using mix releases and a GitHub action.
The release will be deployed by a systemd unit on a Linux server.
Create a blank app:
$ mix phx.new hello
* creating hello/lib/hello/application.ex
* creating hello/lib/hello.ex
* creating hello/lib/hello_web/controllers/error_json.ex
* creating hello/lib/hello_web/endpoint.ex
* creating hello/lib/hello_web/router.ex
* creating hello/lib/hello_web/telemetry.ex
* creating hello/lib/hello_web.ex
* creating hello/mix.exs
* creating hello/README.md
* creating hello/.formatter.exs
* creating hello/.gitignore
* creating hello/test/support/conn_case.ex
* creating hello/test/test_helper.exs
* creating hello/test/hello_web/controllers/error_json_test.exs
* creating hello/lib/hello/repo.ex
* creating hello/priv/repo/migrations/.formatter.exs
* creating hello/priv/repo/seeds.exs
* creating hello/test/support/data_case.ex
* creating hello/lib/hello_web/controllers/error_html.ex
* creating hello/test/hello_web/controllers/error_html_test.exs
* creating hello/lib/hello_web/components/core_components.ex
* creating hello/lib/hello_web/controllers/page_controller.ex
* creating hello/lib/hello_web/controllers/page_html.ex
* creating hello/lib/hello_web/controllers/page_html/home.html.heex
* creating hello/test/hello_web/controllers/page_controller_test.exs
* creating hello/lib/hello_web/components/layouts/root.html.heex
* creating hello/lib/hello_web/components/layouts/app.html.heex
* creating hello/lib/hello_web/components/layouts.ex
* creating hello/priv/static/images/logo.svg
* creating hello/lib/hello/mailer.ex
* creating hello/lib/hello_web/gettext.ex
* creating hello/priv/gettext/en/LC_MESSAGES/errors.po
* creating hello/priv/gettext/errors.pot
* creating hello/priv/static/robots.txt
* creating hello/priv/static/favicon.ico
* creating hello/assets/js/app.js
* creating hello/assets/vendor/topbar.js
* creating hello/assets/css/app.css
* creating hello/assets/tailwind.config.js
Fetch and install dependencies? [Yn] y
* running mix deps.get
* running mix assets.setup
* running mix deps.compile
We are almost there! The following steps are missing:
$ cd hello
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
Generate the release files:
$ mix phx.gen.release
* creating rel/overlays/bin/server
* creating rel/overlays/bin/server.bat
* creating rel/overlays/bin/migrate
* creating rel/overlays/bin/migrate.bat
* creating lib/hello/release.ex
Your application is ready to be deployed in a release!
See https://hexdocs.pm/mix/Mix.Tasks.Release.html for more information about Elixir releases.
Here are some useful release commands you can run in any release environment:
# To build a release
mix release
# To start your system with the Phoenix server running
_build/dev/rel/hello/bin/server
# To run migrations
_build/dev/rel/hello/bin/migrate
Once the release is running you can connect to it remotely:
_build/dev/rel/hello/bin/hello remote
To list all commands:
_build/dev/rel/hello/bin/hello
Create the folder structure on the target server:
mkdir -p /var/www/hello.yellowduck.be
Generate a new secret for the deployment:
mix phx.gen.secret
Create the .env
file on the target server:
/var/www/hello.yellowduck.be/.env
PHX_HOST=hello.yellowduck.be
PORT=4001
PHX_SERVER=true
SECRET_KEY_BASE=my-secret-key
MIX_ENV=prod
DATABASE_URL=ecto://user:pass@localhost/hello
Create the systemctl daemon:
/etc/systemd/system/hello-yellowduck-be.service
[Unit]
Description=hello.yellowduck.be
[Service]
User=root
EnvironmentFile=/var/www/hello.yellowduck.be/.env
Environment=LANG=en_US.utf8
WorkingDirectory=/var/www/hello.yellowduck.be/
ExecStart=/var/www/hello.yellowduck.be/_build/prod/rel/hello/bin/hello start
ExecStop=/var/www/hello.yellowduck.be/_build/prod/rel/hello/bin/hello stop
KillMode=process
Restart=on-failure
LimitNOFILE=65535
SyslogIdentifier=hello-yellowduck-be
[Install]
WantedBy=multi-user.target
Load the daemon configuration:
systemctl daemon-reload
Define the following secrets in GitHub:
SSH_KEY
: the SSH key used to connect to the server (see here on how to get this info)SSH_HOST
: the hostname of the server to where you want to deploySSH_USER
: the username you you want to use to connect via SSH to the server
Create the github action (remember to update the env vars at the top of the action with the correct values for your environment):
.github/workflows/deploy.yaml
name: Deploy
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
MIX_ENV: prod
UBUNTU_VERSION: ubuntu-20.04
DEPLOY_PATH: /var/www/hello.yellowduck.be
DEPLOY_APP_NAME: hello
DEPLOY_DAEMON_NAME: hello-yellowduck-be
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
version-file: .tool-versions
version-type: strict
- name: Cache deps
id: cache-deps
uses: actions/cache@v4
env:
cache-name: cache-elixir-deps
with:
path: deps
key: ${{ env.UBUNTU_VERSION }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('**/.tool-versions') }}
restore-keys: |
${{ env.UBUNTU_VERSION }}-mix-${{ env.cache-name }}-
- name: Cache compiled build
id: cache-build
uses: actions/cache@v4
env:
cache-name: cache-compiled-build
with:
path: _build
key: ${{ env.UBUNTU_VERSION }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('**/.tool-versions') }}
restore-keys: |
${{ env.UBUNTU_VERSION }}-mix-${{ env.cache-name }}-
${{ env.UBUNTU_VERSION }}-mix-
- name: Clean to rule out incremental build as a source of flakiness
if: github.run_attempt != '1'
run: |
mix deps.clean --all
mix clean
shell: sh
- name: Install dependencies
run: mix deps.get --only-prod
- name: Compile
run: mix compile
- name: Compile assets
run: mix assets.deploy
- name: Compile release
run: mix release --overwrite
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: 'to be defined on next step'
- name: Add Known Hosts
run: ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy Release with rsync
run: rsync --delete -avz ./_build ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ env.DEPLOY_PATH }}
- name: Restart Application
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
export $(cat ${{ env.DEPLOY_PATH }}/.env | xargs) && ${{ env.DEPLOY_PATH }}/_build/${{ env.MIX_ENV }}/rel/${{ env.DEPLOY_APP_NAME }}/bin/migrate
systemctl daemon-reload
systemctl restart ${{ env.DEPLOY_DAEMON_NAME }}
Configure Caddy to expose the application:
$ vim /etc/caddy/Caddyfile
Then add the following entry:
hello.yellowduck.be {
root * /var/www/hello.yellowduck.be
encode zstd gzip
reverse_proxy localhost:4001
header -Server
log {
output file /var/log/caddy/access_hello_yellowduck.log
}
}
When you now push anything to the main
branch, it will be built as a release, transferred to the target server and it will restart the daemon.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.