62
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RUNTEQ Advent Calendar 2023Advent Calendar 2023

Day 3

DockerでRailsAPIモード/Next.jsの環境構築をして、Fly.ioとVercelへデプロイしてみた

Last updated at Posted at 2023-12-02

はじめに

RUNTEQ Advent Calendar 2023の3日目を担当させていただきます、清水と申します。
現在は、事業会社でコーダーをしながら、プログラミングスクールRUNTEQにて、Web開発の勉強をしています。
今回は、「初めた学んだ技術」というテーマということで、個人開発で使用するためにキャッチアップした技術構成での環境構築とデプロイ方法についてまとめてみようと思います。
具体的には、Dockerを使用して、Rails(APIモード)/ Next.jsの環境構築を行い、Fly.ioとVercelにデプロイを行う方法についてまとめています。
これから、この技術構成で環境構築などを行う方に向けて、少しでも参考になればと思います。
また、もしこの記事通りに環境構築・デプロイを行ってみたがエラーが発生してしまった場合や、解説のミスなどありましたらコメントいただけると幸いです。

使用技術

  • Ruby 3.2.2
  • Rails(APIモード) 7.0.8
  • Next.js 14.0.3
  • Postgresql 15.5
  • Docker 24.0.6
  • Docker Compose v2.23.0
  • Node.js 19.4.0
  • Fly.io
  • Vercel
  • Github Actions

全体の流れ

今回は、Webアプリ開発を想定しているので、Githubでリポジトリを作成するところから解説をしていこうと思います。

  1. メインリポジトリ作成、フロントエンド・バックエンドディレクトリのサブモジュール化
  2. Docker設定
  3. フロントエンド側の環境構築(Next.js)
  4. バックエンド側の環境構築(Rails API)
  5. API作成
  6. Next.jsでAPIリクエスト
  7. Vercelへデプロイ
  8. Fly.ioへデプロイ
  9. Github Actions
  10. CORS設定

ソースコード

1.リポジトリ作成とサブモジュール化

今回は、フロントエンドとバックエンドを別々のリポジトリでサブモジュール化して、メインリポジトリで読み込む方法を行います。

submoduleについて

今回のディレクトリ構成を例にすると、frontディレクトリとbackディレクトリをサブモジュール化することにより、メインリポジトリからリンクはされているが、それぞれ独立したリポジトリとして扱われます。
なので、フロントエンドとバックエンドの開発が分離されて、それぞれのリポジトリで開発を進めることができます。

ディレクトリ構成

├── rails-api-nextjs-verification-app
  ├── front
  └── back
  • 任意のディレクトリで、$ mkdir [任意のディレクトリ名]を実行します。今回は、rails-api-nextjs-verification-appで進めていきます。
$ mkdir rails-api-nextjs-verification-app
  • $ cd rails-api-nextjs-verification-appで移動します。
  • github上で、rails-api-nextjs-verification-app用のリポジトリを作成して、git initをします。
rails-api-nextjs-verification-app $ git init
rails-api-nextjs-verification-app $ git add README.md
rails-api-nextjs-verification-app $ git commit -m "first commit"
rails-api-nextjs-verification-app $ git branch -M main
rails-api-nextjs-verification-app $  git remote add origin git@github.com:[ユーザーid]/[リポジトリ名].git
rails-api-nextjs-verification-app $  git push -u origin main

これで、リポジトリと連携ができたかと思います。

  • frontディレクトとbackディレクトリ用のリポジトリを作成します。リポジトリ作成時に、Add a README file にチェックを入れておき、リポジトリ作成時にコミットがされている状態にしておきます。
  • rails-api-nextjs-verification-appディレクトリ内に、frontbackのサブモジュールを追加します。
rails-api-nextjs-verification-app $ git submodule add [フロントエンドリポジトリのSSH] front
rails-api-nextjs-verification-app $ git submodule add [バックエンドリポジトリのSSH] back
  • ルートディレクトリに.gitmodulesが作成されているかと思います。もし、作成されてなかったら、以下のコマンドで作成してください。
rails-api-nextjs-verification-app $ touch .gitmodules
.gitmodules
[submodule "front"]
	path = front
	url = [フロントエンドリポジトリのSSH]
[submodule "back"]
	path = back
	url = [バックエンドリポジトリのSSH]
  • メインリポジトリ変更のcommitとpushを行います。
rails-api-nextjs-verification-app $ git add .
rails-api-nextjs-verification-app $ git commit -m "Add: submolues"
rails-api-nextjs-verification-app $ git push

以下の画面のようになれば完了です。

Image from Gyazo

2.Docker設定

次に、Dockerの設定としてdocker-compose.ymlの作成とfront backディレクトリにDockerfileを作成していきます。

ディレクト構成

├── rails-api-nextjs-verification-app
    ├── front/
        ├── Dockerfile
    └── back/
        ├── Dockerfile
    ├── docker-compose.yml

docker-compose.yml

docker-compose.yml
version: "3"
services:
  db:
    image: postgres:15.5
    environment:
      POSTGRES_DB: app_development
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
  back:
    build:
      context: ./back
      dockerfile: Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'"
    volumes:
      - ./back:/app
    ports:
      - "3000:3000"
    depends_on:
      - db
    tty: true
    stdin_open: true
    environment:
      - RAILS_ENV=development
  front:
    build:
      context: ./front/
      dockerfile: Dockerfile
    volumes:
      - ./front:/app
    command: yarn dev -p 4000
    ports:
      - "8000:4000"
volumes:
  postgres_data:
docker-compose.ymlの各項目について
  • db
    • postgres:15.5を使用しています。
    • environmentで環境変数を設定しています。
    • portsでポートマッピングを行っています。
    • volumesでデータの永続化を設定しています。
      • 通常、Dockerのコンテナ内のデータはコンテナが停止・削除されると消失します。しかし、データベースのデータは永続的に保存する必要があります。
  • back
    • build → Dockerイメージのビルド方法を定義しています。
      • context → Dockerfileがあるディレクトリパスを指定してます。
      • dockerfile → Dockerfileの名前を指定します。
    • command → コンテナが起動するときに実行されるコマンドを指定します。
      • bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'" → まず、tmp/pids/server.pidを削除して、Railsサーバーを起動しています。
    • volumes → コンテナ内のデータをホストマシンと共有するために使用されます。
    • depends_on → backが他のサービスに依存していることを指定します。
  • front
    • command
      • yarn dev -p 4000 → コンテナ起動時に、ポート4000でフロントの開発サーバーを起動します。

front/Dockerfile

frontディレクトリにDockerfileを作成します。
Dockerfileはイメージの設計図として機能します。必要な依存関係のインストールや、アプリケーションのコードのコピーなど、イメージを構築するために必要な情報を記載しています。

/front/Dockerfile
FROM node:19.4.0
WORKDIR /app

back/Dockerfile

backディレクトリにDockerfileentrypoint.shを作成します。
entrypoint.shは、コンテナが開始された時に実行されるスクリプトになります。

/back/Dockerfile
FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client

WORKDIR /app

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

RUN gem install bundler
RUN bundle install

COPY . /app

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

EXPOSE 3002

CMD ["rails", "server", "-b", "0.0.0.0"]
entrypoint.sh
#!/bin/bash
set -e

rm -f /app/tmp/pids/server.pid

exec "$@"
  • #!/bin/bash → Bashスクリプトであることを宣言しています。
  • set -e → スクリプトが失敗したら、直ちに停止します。
  • rm -f /app/tmp/pids/server.pid → Railsが生成するserver.pidファイルが前回のプロセスで残っているとサーバーが起動しないので、それを防ぎます。

続いて、$ docker-compose buildを通すために、backディレクトリにGemfileGemfile.lockを作成します。
Gemfile.lockは空のままで大丈夫です。

Gemfile
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.2.2"

gem "rails", "~> 7.0.5"

現在のディレクトリ構造

├── rails-api-nextjs-verification-app
    ├── front/
        ├── Dockerfile
        ├── README.md
    └── back/
        ├── Dockerfile
        ├── Gemfile
        ├── Gemfile.lock
        ├── README.md
    ├── docker-compose.yml

ルートディレクトリでdocker-compose buildを実行して、Dockerイメージをビルドします。
このコマンドは、docker-compose.ymlファイルに記載された設定をもとに、Dockerイメージを作成します。

rails-api-nextjs-verification-app $ docker-compose build

これで、docker-compose buildが成功すれば、DockerにImageが作成されていると思います。
docker imagesを実行して、Imageが作成されているか確認してみてください。

3. フロントエンド側の環境構築(Next.js)

次に、フロントエンド側で使用するNext.jsアプリケーションを作成していきます。
まずは、frontディレクトリへ移動してください。

$ cd front

続いて、$ docker-compose run --rm front yarn create next-app .を実行します。

front $ docker-compose run --rm front yarn create next-app .

すると、以下のエラーが発生するかと思います。

[##] 2/2The directory app contains files that could conflict:

  Dockerfile
  README.md

Either try using a new directory name, or remove the files listed above.

error Command failed.
Exit code: 1
Command: /usr/local/bin/create-next-app
Arguments: .
Directory: /app
Output:

info Visit https://yarnpkg.com/en/docs/cli/create for documentation about this command.

このエラーは、create-next-appコマンドが新しいNext.jsアプリケーションをセットアップする際に、実行されたディレクトリが空でないということを表しています。
現在、frontディレクトリでは、DockerfileREADME.meが存在しているため、エラーが発生してしまいました。

なので、一度Dockerfileをルートディレクトリへ移動してから、再度$ docker-compose run --rm front yarn create next-app .を実行します。
README.meはNext.jsアプリ作成時に新規に作成されるので、ここでは削除してしまって大丈夫です。

一時的なディレクトリ構造

├── rails-api-nextjs-verification-app
    ├── front/
    └── back/
        ├── Dockerfile
        ├── Gemfile
        ├── Gemfile.lock
        ├── README.md
    ├── docker-compose.yml
    ├── Dockerfile  // ここに移動

以下のようにNext.jsアプリ作成時の設定を色々聞かれると思いますが、今回はTypeScriptとESLintとTailwindCSSとAppRouterを使用したアプリケーションを作成します。

Image from Gyazo

作成が成功するとfrontディレクトが以下のようになります。

Image from Gyazo

そうしたら、先ほど一時的に移動したDockerfileをfrontディレクトリ直下に戻します。

Dockerfileを戻したら、frontディレクトリで$ docker-compose up frontを実行して、Next.jsアプリケーションを起動します。

docker-compose up front について

docker-compose.ymlファイル内に定義されたfrontサービスに関連する、コンテナなどを作成します。

そして、http://localhost:8000/にアクセスして、以下のようなNext.jsの初期画面が表示されたら成功です。

Image from Gyazo

4. バックエンド側の環境構築(Rails API)

フロントエンド側のアプリケーションが作成できたら、次はバックエンド側のアプリケーションをRails APIモードで作成していきます。

まず、backディレクトリに移動します。

front $ cd ..
$ cd back

backディレクトリで、$ docker-compose run --rm --no-deps back bundle exec rails new . --api --database=postgresqlを実行します。
これは、docker-composeを使用して、Railsアプリケーションを作成するためのコマンドです。

docker-compose run --rm --no-deps back bundle exec rails new . --api --database=postgresql について
  • docker-compose run
    • docker-compose.ymlファイル内のサービスを起動するために使用されます。
  • --rm
    • コマンドの実行が完了した後にコンテナを自動的に削除するようにします。
  • --no-deps
    • backに依存しているデータベースも一緒に起動されないようにします。
  • back
    • docker-compose.ymlファイル内で指定されたサービス名です。
  • bundle exec rails new . --api --database=postgresql
    • 新しいRailsアプリケーションをAPI専用で作成し、データベースにはPostgreSQLを使用します。

コマンドを実行すると、README.mdGemfileが競合を起こしてしまうので、以下の画像のようにYと入力して、上書き保存をします。

Image from Gyazo

Railsアプリケーションの作成が完了すると、backディレクトリ内は以下の画像のようになります。

Image from Gyazo

それでは、Railsアプリケーションを起動する前に設定を行います。

国際化とタイムゾーンの設定

config/application.rbに以下のコードを追加します。

config/application.rb
module App
  class Application < Rails::Application

    config.api_only = true
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
    config.i18n.default_locale = :ja

  end
end

ホスト機能設定

config/environments/development.rbconfig.hosts << "api"を追加します。

config/environments/development.rb
Rails.application.configure do

  config.hosts << "api"
end

データベースのセットアップ

/config/database.ymlに以下のコードを追加します。

/config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: user
  password: password
  host: db

次に、backディレクトリで$ docker-compose run --rm back rails db:createを実行します。
そうすると、以下のエラーが発生します。このエラーは、必要なRubyのgemがインストールされていないことを示します。

Could not find pg-1.5.4, puma-5.6.7, bootsnap-1.17.0, debug-1.8.0, msgpack-1.7.2, irb-1.9.1, reline-0.4.0, rdoc-6.6.0, psych-5.1.1.1, stringio-3.0.9 in locally installed gems
Run `bundle install --gemfile /app/Gemfile` to install missing gems.

なので、$ docker-compose run --rm back bundle installを実行し、$ docker-compose build backを実行します。

そしたら、再度$ docker-compose run --rm back rails db:createを実行します。
データベースのセットアップが完了したはずなので、$ docker-compose upを実行してアプリケーションを起動します。

http://0.0.0.0:3000/にアクセスして、Railsの初期画面が表示されたらRailsアプリケーションの作成は完了です。

Image from Gyazo

ここまでが、DockerでRailsAPIとNext.jsの環境構築が完了になります。
この後は、実際にRailsで簡単なAPIを作成してみて、Next.jsで非同期処理を行いたいと思います。

5. API作成

次に、Railsアプリケーションでscaffoldを使用して、簡単なAPIを作成してみたいと思います。
また、Next.js側で作成したAPIと簡単なやり取りができるところまで実装してみます。

scaffold追加

backディレクトリで、docker-compose run --rm back bundle exec rails g scaffold post title:stringを実行します。

back $ docker-compose run --rm back bundle exec rails g scaffold post title:string

次に、docker-compose run --rm back bundle exec rails db:migrateを実行します。

back $ docker-compose run --rm back bundle exec rails db:migrate

次に、seeds.rbでテストデータを作成します。

db/seeds.rb
Post.create!(
  [
    { title: '野球のルール基礎知識' },
    { title: 'プロ野球選手のトレーニング方法' },
    { title: '野球の歴史とは' },
    { title: 'メジャーリーグと日本プロ野球の違い' },
    { title: '野球用具の選び方' },
    { title: '野球のポジション紹介' },
    { title: '野球の戦術入門' },
    { title: '子供向け野球教室の選び方' },
    { title: '高校野球の魅力' },
    { title: '野球観戦の楽しみ方' },
    { title: '野球のスコアブックのつけ方' },
    { title: '野球の審判の役割' },
    { title: '野球におけるピッチングの技術' },
    { title: 'バッティングの基本' },
    { title: '野球の名言集' },
    { title: '野球のトレーニング用品紹介' },
    { title: '野球選手の食事管理' },
    { title: '野球の怪我の予防と対処法' },
    { title: '野球の上達法' },
    { title: '野球の国際大会について' }
  ]
)

back $ docker-compose run --rm back bundle exec rails db:seedを実行して、テストデータを作成します。
この後に、http://0.0.0.0:3000/postsにアクセスすると、JSON形式で先ほど作成したテストデータが表示されているかと思います。

rack-cors追加

次に、gem "rack-cors"を追加して、CORSを管理する設定を行います。
CORSとは、セキュリティの観点から、ブラウザから異なるオリジン(ドメイン・プロトコル・ポート)からのスクリプトによるリソースの読み込みを制限するものです。
なので、RailsAPIをフロントからAPIリクエストを行った時に、Rails側でCORS設定を行っていないオリジンだった場合、エラーになってしまいます。

Gemfilegem "rack-cors"がコメントアウトされていると思うので、コメントアウトして、config/initializers/cors.rbを以下のように変更します。

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:8000', '127.0.0.1:8000'

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

こうすることで、localhost:8000127.0.0.1:8000からのAPIリクエストを許可することができます。
そしたら、backディレクトリで$ docker-compose run --rm back bundle installを実行します。

back $ docker-compose run --rm back bundle install

そして、再ビルドします。

back $ docker-compose build back

6. Next.jsでAPIリクエスト

次に、Next.js側で「記事のタイトルのみ投稿するフォーム」と「記事一覧を取得」する実装を行ってみたいと思います。
以下がTypeScriptを使用したコードになります。(不要なcssは削除しました)

page.tsx
app/page.tsx
"use client";
import React, { useEffect, useState } from "react";

type Post = {
  id: number;
  title: string;
};

export default function Home() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [newTitle, setNewTitle] = useState("");

  const fetchPosts = async () => {
    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/posts`);
      if (!response.ok) {
        throw new Error("データの取得に失敗しました");
      }
      const data = await response.json();
      setPosts(data);
    } catch (error) {
      console.error(error);
    }
  };

  useEffect(() => {
    fetchPosts();
  }, []);

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/posts`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ title: newTitle }),
      });
      if (!response.ok) {
        throw new Error("投稿に失敗しました");
      }
      setNewTitle("");
      fetchPosts();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24">
      <h2 className="text-3xl mb-4">記事の一覧</h2>
      <form onSubmit={handleSubmit} className="mt-4 mb-4">
        <input
          type="text"
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          placeholder="新しい投稿のタイトル"
          className="mr-2 p-2 border"
        />
        <button type="submit" className="p-2 bg-blue-500 text-white">
          投稿する
        </button>
      </form>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

Image from Gyazo

7. Vercelへデプロイ

先ほど作成した、frontディレクトリのNext.jsアプリケーションをVercelへデプロイします。
すごく簡単にデプロイを行うことができます。

  1. https://vercel.com/dashboard にアクセスします。まだ、Vercelへ登録していない方は、登録をお願いします。
  2. 「Dashboard」の「Add New」の「Project」をクリックします。
    Image from Gyazo
  3. 「Import Git Repository」で、frontディレクトリと紐づいているリポジトリを選択(Import)します。
    Image from Gyazo
  4. 「Configure Project」の「Deploy」をクリックします。
    Image from Gyazo
  5. 「Congratulations!」という画面が表示されたら、デプロイ成功です。

これで、frontディレクトリのリポジトリのmainブランチにpushされたら、自動でデプロイが行われます。

8. Fly.ioへデプロイ

次に、RailsAPIアプリケーションをFly.ioにデプロイします。
backディレクトリでfly launchを実行します。

Fly.ioの無料枠で使用できる内容はこちらです。

Up to 3 shared-cpu-1x 256mb VMs
3GB persistent volume storage (total)
160GB outbound data transfer

詳しくは公式ページをご確認ください。
https://fly.io/docs/about/pricing/

back $ fly launch

fly launchを実行して、? Do you want to tweak these settings before proceedingyと答えると、詳細を設定するページが立ち上がります。
今回は無料枠での運用を想定しているため、以下の設定にします。

  • Memory & CPU
    • VM Sizes → shared-cpu-1x
    • VM Memory → 256MB
  • Database
    • Configuration → Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk

設定が完了したら、以下のファイルが作成されているかと思います。

Image from Gyazo

新規にファイルが作成されていることを確認できたら、fly deployでデプロイを行います。
エラーなく、無事にデプロイが完了したら、ダッシュボード(https://fly.io/dashboard)を確認して、

back $ fly deploy

fly openでデプロイしたアプリケーションを確認することができます。
今回のRailsAPIアプリケーションは、scaffoldを使用しているので、https://[設定で指定した名前.fly.dev]/postsにアクセスすると、JSON形式のページが表示されるかと思います。

9. Github Actions

次に、Fly.ioへのデプロイをGithub Actionsを使用して、backリポジトリのmainブランチにpushされたら自動でデプロイが行われるようにしたいと思います。

公式ドキュメントはこちら
https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/

トークン作成

まず、デプロイの設定に必要なトークンを作成します。
fly.ioのダッシュボードにログインして、AccountAccess Tokensをクリックします。
Image from Gyazo
Create tokenにトークン名を入力して、Createをクリックします。
Image from Gyazo
作成されたトークンは、Githubに登録するので、コピーをして控えておきます。

GitHubにトークン設定

次に、作成したトークンをGitHubに設定します。
back用のリポジトリページを開いて、SettingsSecrets and variablesActionsを選択します。

次に、New repository secretを選択します。

そしたら、NameFLY_API_TOKENと入力し、Secretに先ほど作成したトークンを入力して、Add Secretをクリックします。

ワークフロー作成

トークン設定が完了したら、次はGithub Actionsのワークフローを作成します。

backディレクトリ直下に、.github/workflows/fly.ymlを作成します。

fly.yml
name: Fly Deploy
on:
  push:
    branches:
      - main
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

branchesmainと設定することで、backリポジトリのmainブランチにpushされた時に、GithubActionsにより自動でFly.ioへのデプロイが実行されます。

実際に、backディレクトリに少し修正を加えてみて、mainブランチにpushしてみます。
backディレクトリ用のリモートリポジトリのActionsを確認して、デプロイが成功することを確認してみましょう。
以下の画像のように、緑色のチェックマークになればGithub Actionsによるデプロイは成功です。
Image from Gyazo

以下は、私がGithub Actionsでのデプロイ時に発生したエラーになります。もし、エラーが発生した時に参考になれば幸いです。

デプロイ時に発生したエラー

マシンの上限に達している
Error: failed to update VM 4d891224b70168: You have reached the maximum number of machines for this app.
アプリケーションに割り当てられているマシンの最大数が無料枠で使用できる数に達していると発生するエラーになります。

10. CORS設定

最後に、backディレクトリのconfig/initializers/cors.rboriginsにフロントエンドのURL(先ほどVercelにデプロイした際に生成されたURL)を追加することで、デプロイ環境でのCORSエラーを回避することができます。

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:8000', '127.0.0.1:8000', '[フロントエンドをデプロイしたURL]'

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

参考情報

62
52
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?