Cách deploy ứng dụng Phoenix + PostgreSQL trên Cloud Server (Ubuntu 22.04)

Bài viết chia sẻ cách deploy ứng dụng Phoenix trên cloud server (AWS EC2 hoặc Google cloud compute engine) với OS Ubuntu 22.04.

Cách deploy ứng dụng Phoenix + PostgreSQL trên Cloud Server (Ubuntu 22.04)

Có rất nhiều cách để deploy một ứng dụng Elixir/Phoenix. Và cũng có nhiều dịch vụ PaaS hỗ trợ việc này. Hai dịch vụ tiêu biểu nhất có thể kể đến là Fly.ioGigalixir. Trên trang tài liệu chính thức của Phoenix cũng có hướng dẫn chi tiết việc deploy trên hai dịch vụ này:

Bài viết này sẽ chia sẻ cách deploy ứng dụng Phoenix trên một cloud server mà cụ thể là AWS EC2 với hệ điều hành Ubuntu Jammy 22.04. Tuy nhiên cách deploy không chỉ giới hạn ở AWS EC2, mà có thể áp dụng cho cloud server của các nhà cung cấp nổi tiếng khác như Google Cloud, Microsoft Azure, Digital Ocean hay các nhà cung cấp dịch vụ trong nước như Viettel, FPT hay CMC.

Giả định

  • Tên miền đã đăng ký: elixirvn.com.
  • EC2 instance đã được khởi tạo với Ubuntu 22.04 với sudo user mặc định là ubuntu và file pem đã được lưu về máy tính.
  • Source code được lưu trữ trên Github repo với tên elixirvn.
  • Ứng dụng Phoenix sử dụng cơ sở dữ liệu PostgreSQL.

Các bước thực hiện

1. Thiết lập swap cho EC2 instance

Nếu EC2 instance có cấu hình cao thì có thể bỏ qua bước này. Tuy nhiên đối với EC2 instance cấu hình thấp nên thực hiện, bởi khi compile ứng dụng Phoenix cùng các thư viện mà nó sử dụng, nếu RAM quá thấp sẽ bị treo.

$ sudo fallocate -l 1G /swapfile 
$ sudo chmod 600 /swapfile 
$ sudo mkswap /swapfile 
$ sudo swapon /swapfile 
$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
  • Kiểm tra kết quả bằng câu lệnh
$ top

Nếu ở dòng thứ 5 xuất hiện thông tin về trạng thái của swap là thành công.

2. Cấu hình DNS cho tên miền

Truy cập vào trang quản lý DNS record của tên miền elixirvn.com và thêm 2 record A là elixirvn.comwww.elixirvn.com đều cùng trỏ tới địa chỉ IP của EC2 instance.

3. Thêm deploy key cho Github repo

  • Đăng nhập vào EC2 instance bằng lênh SSH sau (file.pem là file pem đã được lưu về máy tính khi khởi tạo EC2 instance).
$ ssh -i /path/to/file.pem ubuntu@elixirvn.com
Chú ý: nếu truy cập vào EC2 instance bị báo lỗi quyền truy cập của file pem quá mở thì phải thay đổi lại như sau:
$ chmod 400 /path/to/file.pem
  • Sau khi truy cập vào EC2 instance, tạo cặp key SSH mới.
$ ssh-keygen
#=> Key được lưu trong thư mục /home/ubuntu/.ssh
  • Thêm public key vừa tạo ra vào danh sách các deploy key trên Github repo. Link tham khảo.
  • Clone Github repo về EC2 instance. Giả sử repo được clone về thư mục home của user ubuntu.
$ mkdir $HOME/code
$ git clone <Github repo SSH link> $HOME/code/
#=> Github repo elixirvn được clone về /home/ubuntu/code/elixirvn

4. Cài đặt Nodejs, Erlang, Elixir, Phoenix

Để cài đặt Nodejs, Erlang và Elixir trên Ubuntu 22.04, ta sử dụng tool quản lý version mạnh và tiện dụng là asdf. Chi tiết hướng dẫn cài đặt asdf có thể tham khảo tại đây. Bên dưới là tóm tắt cách cài đặt asdf.

  • Cài đặt các tool hỗ trợ.
$ sudo apt install curl git unzip
  • Cài đặt asdf
$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
$ echo ". /home/ubuntu/.asdf/asdf.sh" >> ~/.bashrc
$ echo ". /home/ubuntu/.asdf/completions/asdf.bash" >> ~/.bashrc
$ source ~/.bashrc
  • Cài đặt Nodejs bằng asdf. Giả sử bản muốn cài đặt là 18.20.4.
$ asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
$ asdf install nodejs 18.20.4
$ asdf global nodejs 18.20.4
  • Cài đặt Erlang bằng asdf. Giả sử bản muốn cài đặt là 26.2.5.
    • Cài đặt các thư viện cần thiết cho việc biên dịch erlang.
$ sudo apt install build-essential autoconf m4 libncurses5-dev libssh-dev unixodbc-dev
    • Cài đặt erlang.
$ asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
$ export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac"
$ asdf install erlang 26.2.5
$ asdf global erlang 26.2.5
  • Cài đặt Elixir. Giả sử bản muốn cài đặt là 1.17.2 thì ta sẽ chọn 1.17.2-otp-26 để tương thích với erlang bản 26 đã cài đặt phía trên.
$ asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git
$ asdf install elixir 1.17.2-otp-26
$ asdf global elixir 1.17.2-otp-26
  • Cài đặt Phoenix.
$ mix archive.install hex phx_new

5. Cài đặt và thiết lập PostgreSQL

  • Cài đặt PostgreSQL. Giả sử bản muốn cài đặt là 14.
$ echo "deb [signed-by=/usr/share/keyrings/postgresql-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list 
$ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | sudo tee /usr/share/keyrings/postgresql-archive-keyring.gpg > /dev/null 
$ sudo apt update 
$ sudo apt install postgresql-14
  • Nếu ứng dụng có logic xử lý sắp xếp theo thứ tự chữ cái tiếng Việt thì sẽ cần phải cài đặt locale tiếng Việt.
$ sudo locale-gen vi_VN.utf8
$ sudo update-locale
$ sudo systemctl restart postgresql
  • Thêm user mới (giả sử phoenix) cho postgresql.
$ sudo -i -u postgres
$ psql
create user phoenix with password 'your-input-password';
  • Tạo cơ sở dữ liệu (giả sử phoenix_db). Tiếp tục từ bước trên, vẫn đang trong postgresql shell.
create database "phoenix_db" with owner "phoenix" encoding 'UTF-8' lc_collate = 'vi_VN.utf8' lc_ctype = 'vi_VN.utf8' template = template0;
grant all privileges on database phoenix_db to phoenix;
  • Khi cài đặt PostgreSQL, user postgres được tạo sẵn và có mật khẩu mặc định là postgres. Để tăng tính bảo mật, t nên đổi mật khẩu cho user postgres. Tiếp tục từ bước trên, vẫn đang trong postgresql shell.
\password postgres
#=> Nhập mật khẩu mới cho user postgres
\q
$ exit
  • Cài đặt connection để cho phép PostgreSQL lắng nghe kết nối trên tất cả các IP address trên server, bao gồm cả địa chỉ loopback (127.0.0.1) và bất kỳ public IP address hoặc local IP address nào khác mà server có.
$ sudo vi /etc/postgresql/14/main/postgresql.conf
#=> Tìm dòng listen_addresses và sửa thành listen_addresses = '*'
  • Cài đặt phương thức authentication bằng mật khẩu cho PostgreSQL
$ sudo vi /etc/postgresql/14/main/pg_hba.conf
#=> Thêm vào cuối file 2 dòng bên dưới
host    phoenix_db      phoenix         127.0.0.1/32         md5 # IPv4
host    phoenix_db      phoenix         ::1/128              md5 # IPv6
Chú ý: Với cài đặt bên trên thì PostgreSQL sẽ chỉ cho phép authentication bằng mật khẩu của user phoenix tới cơ sở dữ liệu phoenix_db thông qua localhost. Nếu muốn kết nối từ bên ngoài (ví dụ từ pgAdmin client) bằng mật khẩu ta cần thay 127.0.0.1/32 thành 0.0.0.0/0. Thêm vào đó, trong cài đặt cho Security group của EC2 instance, cần mở thêm cổng 5432 dành cho PostgreSQL trong danh sách inbound rules.
  • Khởi động lại PostgreSQL
$ sudo systemctl restart postgresql
  • Cài đặt Postgres tự động khởi động cùng hệ thống
$ sudo systemctl enable postgresql

6. Cài đặt và thiết lập Nginx

  • Cài đặt Nginx và certbot phục vụ cho việc lấy chứng chỉ SSL từ Let's Encrypt
$ sudo apt install -y nginx certbot python3-certbot-nginx
  • Chuẩn bị server trong nginx để thực hiện ACME challenge nhằm chứng minh quyền sở hữu đối với tên miền elixirvn.com.
$ sudo mkdir -p /var/www/html/.well-known/acme-challenge 
$ sudo chown -R www-data:www-data /var/www/html

$ sudo vi /etc/nginx/sites-available/elixirvn
#=> Thêm vào nội dung sau và lưu lại file
server { 
        listen 80; 
        server_name elixirvn.com www.elixirvn.com; 
 
       location /.well-known/acme-challenge/ { 
                root /var/www/html; 
        } 
}
# Hết nội dung file
  • Enable cài đặt bằng cách tạo symbolic link trong thư mục sites-enabled.
$ sudo ln -s /etc/nginx/sites-available/elixirvn /etc/nginx/sites-enabled/
  • Khởi động lại nginx để cài đặt trên có hiệu lực.
$ sudo systemctl restart nginx
  • Lấy chứng chỉ SSL từ Let's Encrypt
$ sudo certbot --nginx -d elixlirvn.com -d www.elixirvn.com
  • Kiểm tra cài đặt cho certbot tự động làm mới chứng SSL.
$ sudo systemctl status certbot.timer
#=> Mặc định certbot sẽ chạy renew job 2 lần/ngày
  • Cài đặt ngnix cho ứng dụng phoenix
$ sudo vi /etc/nginx/sites-available/elixirvn
=> Thay thế toàn bộ nội dung hiện tại bằng nội dung sau và lưu lại file

upstream phoenix {
        server 127.0.0.1:4000;
}

server {
        listen 80; 
        server_name elixirvn.com www.elixirvn.com; 

        location /.well-known/acme-challenge/ {
                root /var/www/html; 
        }

        location / {
                return 301 https://$host$request_uri; 
        }
}

server {
        allow all; 

        listen 443 ssl; 
        server_name elixirvn.com www.elixirvn.com; 

        ssl_certificate /etc/letsencrypt/live/elixirvn.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/elixirvn.com/privkey.pem;

        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

        client_max_body_size 100M; # Trường hợp cần xử lý upload file kích thước lớn

        location / {
                # Trường hợp cần xử lý upload file kích thước lớn
                # 1.Tăng thời gian chờ kết nối tới upstream (default: 60s)
                proxy_connect_timeout 300s;

                # 2.Tăng thời gian chờ khi upstream đang gửi response (default: 60s)
                proxy_read_timeout 300s;

                # 3. Tăng thời gian chờ khi đang gửi request tới upstream (default: 60s)
                proxy_send_timeout 300s;
                
                proxy_http_version 1.1; 
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
                proxy_set_header Host $http_host; 
                proxy_set_header X-Cluster-Client-Ip $remote_addr; 

                proxy_set_header Upgrade $http_upgrade; 
                proxy_set_header Connection 'upgrade'; 

                proxy_redirect off; 
                proxy_pass http://phoenix; 
        }
}
# Hết nội dung file
  • Xóa cài đặt mặc định để tránh xung đột với cài đặt bên trên.
$ sudo rm /etc/nginx/sites-enabled/default
  • Khởi động lại nginx.
$ sudo systemctl restart nginx
Chú ý nếu không thể khởi động lại nginx bằng systemctl, hãy thử cách sau:
$ sudo pkill -f nginx & wait $!
$ sudo systemctl start nginx
  • Cài đặt nginx khởi động cùng hệ thống
$ sudo systemctl enable nginx

7. Compile và release ứng dụng phoenix

$ cd $HOME/code/elixirvn
$ export MIX_ENV=prod
$ mix deps.get --only prod
$ cd assets && npm i -y && cd ..
$ mix assets.deploy
$ mix compile
$ mix phx.gen.release

Sau khi chạy lệnh phx.gen.release, trong thư mục gốc của ứng dụng sẽ có thêm thư mục rel → overlays → bin. Trong thư mục bin, có các file script migrate để migrate dữ liệu.

#!/bin/sh
set -eu

cd -P -- "$(dirname -- "$0")"
exec ./elixirvn eval Elixirvn.Release.migrate

Ta có thể tham khảo cách trên để thực hiện các công việc khác với câu lệnh theo cú pháp:

exec ./elixirvn eval <ModuleName>.<function_name>

Ví dụ để chạy seed master data, ta có thể tạo hàm seed trong module Elixirvn.Release

def seed do
  start_applications()

  seed_script = Path.join([priv_dir(), "repo", "seeds.exs"])
  if File.exists?(seed_script) do
    Code.eval_file(seed_script)
  end
end

defp start_applications do
  Application.load(@app)

  # Ensure all required applications are started
  Enum.each([:logger, :postgrex, :ecto, :ecto_sql], fn app ->
    Application.ensure_all_started(app)
  end)

  # Ensure the application's Ecto repos are started
  Enum.each(repos(), & &1.start_link(pool_size: 1))
end

defp priv_dir() do
  Application.app_dir(@app, "priv")
end

Sau đó tạo một script seed để chạy hàm seed

#!/bin/sh
set -eu

cd -P -- "$(dirname -- "$0")"
exec ./giatocso eval Elixirvn.Release.seed
  • Cuối cùng là tạo bản release.
$ mkdir $HOME/elixirvn
$ mix release --path $HOME/elixirvn

8. Tạo file .env

  • Tạo secret key
$ mix phx.gen.secret
  • Tạo file .env ở thư mục home của user /home/ubuntu
export PHX_HOST=localhost 
export FLY_APP_NAME=elixirvn 
export SECRET_KEY_BASE=<secret key đã tạo ở câu lệnh trước>
export DATABASE_NAME=phoenix_db
export DATABASE_USER_NAME=phoenix
export DATABASE_PASSWORD=your-input-password 
export DATABASE_URL=ecto://${DATABASE_USER_NAME}:${DATABASE_PASSWORD}@${PHX_HOST}/${DATABASE_NAME}
#=> Thêm tiếp các biến môi trường khác cần thiết cho ứng dụng Phoenix nếu có

9. Chạy migrate cho cơ sở dữ liệu

$ source $HOME/.env && exec $HOME/elixirvn/bin/migrate

Chạy seed cho database nếu cần

10. Chạy thử ứng dụng phoenix

  • Chạy ứng dụng
$ source $HOME/.env && exec $HOME/elixirvn/bin/server
  • Truy cập vào địa chỉ https://elixirvn.com để kiểm tra kết quả.

11. Cài đặt ứng dụng dưới dạng service bằng systemd

  • Tạo file .env_service ở thư mục home của user. Chú ý khác với file .env, trong file .env_service không có export ở trước.
PHX_HOST=localhost 
FLY_APP_NAME=elixirvn 
SECRET_KEY_BASE=<secret key đã tạo ở câu lệnh trước>
DATABASE_URL=ecto://phoenix:your-input-password@localhost/phoenix_db
#=> Thêm tiếp các biến môi trường khác cần thiết cho ứng dụng Phoenix nếu có
$ vi /etc/systemd/system/elixirvn.service
#=> Nội dung file
[Unit] 
Description=ElixirVN
After=network.target postgresql.service nginx.service

[Service] 
User=ubuntu 
Group=ubuntu 
WorkingDirectory=/home/ubuntu/elixirvn
EnvironmentFile=/home/ubuntu/.env_service
ExecStart=/home/ubuntu/elixirvn/bin/server 
ExecStop=/home/ubuntu/elixirvn/bin/elixirvn stop 
Restart=on-failure 

[Install] 
WantedBy=multi-user.target
#=> Kết thúc nội dung file
  • Thêm file service vừa tạo vào bộ nhớ của systemd
$ sudo systemctl daemon-reload
  • Một số câu lệnh thao tác với service đã đăng ký:
# Khởi động
$ sudo systemctl start elixirvn.service

# Dừng
$ sudo systemctl stop elixirvn.service

# Khởi động lại
$ sudo systemctl restart elixirvn.service

# Đăng ký khởi động cùng hệ thống
$ sudo systemctl enable elixirvn.service

# Bỏ đăng ký khởi động cùng hệ thống
$ sudo systemctl disable elixirvn.service

12. Tạo script build ứng dụng

$ vi $HOME/auto_build.sh
#=> Nội dung file
#!/bin/bash
set -e

# Update to latest code
rm -rf $HOME/code/elixirvn
git clone <Github repo SSH link> $HOME/code/
cd $HOME/code/elixirvn
git fetch
git reset --hard origin/main # or develop branch

# Compile and release app
rm -rf $HOME/elixirvn/*
export MIX_ENV=prod
mix deps.get --only prod
mix compile
cd assets && npm i -y && cd ..
mix assets.deploy
mix release --path $HOME/elixirvn
cd
rm -rf $HOME/code/elixirvn

# Run migrations
source $HOME/.env && exec $HOME/elixirvn/bin/migrate
#=> Hết nội dung file
  • Thêm quyền thực thi cho script
$ chmod +x $HOME/auto_build.sh
  • Các bước thực hiện
    • Trước khi chay script nên dừng ứng dụng đang chạy
$ sudo systemctl stop elixirvn.service
    • Build ứng dụng
$ exec $HOME/auto_build.sh
    • Sau khi build xong, khởi động lại ứng dụng
$ sudo systemctl start elixirvn.service
  • Có thể tạo thêm script để thực hiện cả 3 bước trên nếu muốn, nhưng chú ý không nên thêm 2 câu lệnh start/stop ứng dụng vào ngay trong script auto_build.sh vì script nào thì việc đấy.