Deploying a Phoenix App Using Mix Release and a Git Hub Action Yellow Duck.be
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.