diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..61a7393 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,45 @@ +# This file excludes paths from the Docker build context. +# +# By default, Docker's build context includes all files (and folders) in the +# current directory. Even if a file isn't copied into the container it is still sent to +# the Docker daemon. +# +# There are multiple reasons to exclude files from the build context: +# +# 1. Prevent nested folders from being copied into the container (ex: exclude +# /assets/node_modules when copying /assets) +# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) +# 3. Avoid sending files containing sensitive information +# +# More information on using .dockerignore is available here: +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +.dockerignore + +# Ignore git, but keep git HEAD and refs to access current commit hash if needed: +# +# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat +# d0b8727759e1e0e7aa3d41707d12376e373d5ecc +.git +!.git/HEAD +!.git/refs + +# Common development/test artifacts +/cover/ +/doc/ +/test/ +/tmp/ +.elixir_ls + +# Mix artifacts +/_build/ +/deps/ +*.ez + +# Generated on crash by the VM +erl_crash.dump + +# Static artifacts - These should be fetched and built inside the Docker image +/assets/node_modules/ +/priv/static/assets/ +/priv/static/cache_manifest.json diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..b0c246e --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4df2735 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,97 @@ +# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian +# instead of Alpine to avoid DNS resolution issues in production. +# +# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu +# https://hub.docker.com/_/ubuntu?tab=tags +# +# This file is based on these images: +# +# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20241202-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: hexpm/elixir:1.17.3-erlang-27.1.2-debian-bullseye-20241202-slim +# +ARG ELIXIR_VERSION=1.17.3 +ARG OTP_VERSION=27.1.2 +ARG DEBIAN_VERSION=bullseye-20241202-slim + +ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} as builder + +# install build dependencies +RUN apt-get update -y && apt-get install -y build-essential git \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# prepare build dir +WORKDIR /app + +# install hex + rebar +RUN mix local.hex --force && \ + mix local.rebar --force + +# set build ENV +ENV MIX_ENV="prod" + +# install mix dependencies +COPY mix.exs mix.lock ./ +RUN mix deps.get --only $MIX_ENV +RUN mkdir config + +# copy compile-time config files before we compile dependencies +# to ensure any relevant config change will trigger the dependencies +# to be re-compiled. +COPY config/config.exs config/${MIX_ENV}.exs config/ +RUN mix deps.compile + +COPY priv priv + +COPY lib lib + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# Compile the release +RUN mix compile + +# Changes to config/runtime.exs don't require recompiling the code +COPY config/runtime.exs config/ + +COPY rel rel +RUN mix release + +# start a new build stage so that the final image will only contain +# the compiled release and other runtime necessities +FROM ${RUNNER_IMAGE} + +RUN apt-get update -y && \ + apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \ + && apt-get clean && rm -f /var/lib/apt/lists/*_* + +# Set the locale +RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen + +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +WORKDIR "/app" +RUN chown nobody /app + +# set runner ENV +ENV MIX_ENV="prod" + +# Only copy the final release from the build stage +COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/honyaku ./ + +USER nobody + +# If using an environment that doesn't automatically reap zombie processes, it is +# advised to add an init process such as tini via `apt-get install` +# above and adding an entrypoint. See https://github.com/krallin/tini for details +# ENTRYPOINT ["/tini", "--"] + +CMD ["/app/bin/server"] diff --git a/config/dev.exs b/config/dev.exs index 13b94e4..203e5f8 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -19,7 +19,7 @@ config :honyaku, Honyaku.Repo, config :honyaku, HonyakuWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {127, 0, 0, 1}, port: 8080], check_origin: false, code_reloader: true, debug_errors: true, @@ -65,15 +65,6 @@ config :honyaku, HonyakuWeb.Endpoint, # Enable dev routes for dashboard and mailbox config :honyaku, dev_routes: true -# Gemini API key 开发环境 -config :honyaku, gemini_api_key: System.get_env("GEMINI_API_KEY_DEV") - -# DeepL API key 开发环境 -config :honyaku, deepl_api_key: System.get_env("DEEPL_API_KEY_DEV") - -# Rapid API key 开发环境 -config :honyaku, rapid_api_key: System.get_env("RAPID_API_KEY_DEV") - # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" diff --git a/config/prod.exs b/config/prod.exs index 86a2469..d46dc90 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -7,14 +7,6 @@ import Config # before starting your production server. config :honyaku, HonyakuWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" -# Gemini API key 生产环境 -config :honyaku, gemini_api_key: System.get_env("GEMINI_API_KEY") -# DeepL API key 生产环境 -config :honyaku, deepl_api_key: System.get_env("DEEPL_API_KEY") - -# Rapid API key 生产环境 -config :honyaku, rapid_api_key: System.get_env("RAPID_API_KEY") - # Configures Swoosh API Client config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Honyaku.Finch diff --git a/config/runtime.exs b/config/runtime.exs index 21ceb4d..ae32770 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -49,7 +49,7 @@ if config_env() == :prod do """ host = System.get_env("PHX_HOST") || "example.com" - port = String.to_integer(System.get_env("PORT") || "4000") + port = String.to_integer(System.get_env("PORT") || "8080") config :honyaku, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") @@ -115,3 +115,10 @@ if config_env() == :prod do # # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. end + +# Gemini API key 生产环境 +config :honyaku, gemini_api_key: System.get_env("GEMINI_API_KEY") +# DeepL API key 生产环境 +config :honyaku, deepl_api_key: System.get_env("DEEPL_API_KEY") +# Rapid API key 生产环境 +config :honyaku, rapid_api_key: System.get_env("RAPID_API_KEY") diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..689d169 --- /dev/null +++ b/fly.toml @@ -0,0 +1,35 @@ +# fly.toml app configuration file generated for honyaku-white-water-9847 on 2024-12-18T01:02:28+09:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'honyaku-white-water-9847' +primary_region = 'nrt' +kill_signal = 'SIGTERM' + +[build] + +[deploy] + release_command = '/app/bin/migrate' + +[env] + PHX_HOST = 'honyaku-white-water-9847.fly.dev' + PORT = '8080' + +[http_service] + internal_port = 8080 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + + [http_service.concurrency] + type = 'connections' + hard_limit = 1000 + soft_limit = 1000 + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 diff --git a/lib/honyaku/external/load_balancer.ex b/lib/honyaku/external/load_balancer.ex index 178bfc5..2a07b50 100644 --- a/lib/honyaku/external/load_balancer.ex +++ b/lib/honyaku/external/load_balancer.ex @@ -6,14 +6,14 @@ defmodule Honyaku.External.TranslationBalancer do require Logger alias Honyaku.External.{ - DeepL, Gemini.Flash1_5, Gemini.Flash2, Rapid.FreeGoogleTranslator, - Rapid.AiBitTranslator + Rapid.AiBitTranslator, + Rapid.DeepLTranslator } - @apis [DeepL, Flash1_5, Flash2, FreeGoogleTranslator, AiBitTranslator] + @apis [Flash1_5, Flash2, FreeGoogleTranslator, AiBitTranslator, DeepLTranslator] @doc """ 翻译文本,使用指定的负载均衡算法。 diff --git a/lib/honyaku/external/rapid/deepl_translator.ex b/lib/honyaku/external/rapid/deepl_translator.ex new file mode 100644 index 0000000..a4028d0 --- /dev/null +++ b/lib/honyaku/external/rapid/deepl_translator.ex @@ -0,0 +1,42 @@ +defmodule Honyaku.External.Rapid.DeepLTranslator do + require Logger + + @base_url "https://deepl-translator4.p.rapidapi.com/api/v1" + + def translate(text, target_lang, source_lang) do + case Req.post( + "#{@base_url}/translate", + headers: [ + {"Authorization", + "DeepL-Auth-Key #{Application.fetch_env!(:honyaku, :deepl_api_key)}"} + ], + json: %{ + "text" => [ + text + ], + "to" => target_lang, + "from" => source_lang + } + ) do + {:ok, + %Req.Response{ + status: 200, + body: %{ + "text" => text + } + }} -> + {:ok, text} + + {:ok, %Req.Response{status: 429}} -> + {:error, :quota_exhausted} + + {:ok, reason} -> + Logger.error("翻译失败:#{inspect(reason)}") + {:error, :unknown_error} + + {:error, reason} -> + Logger.error("API调用失败:#{inspect(reason)}") + {:error, reason} + end + end +end diff --git a/lib/honyaku/release.ex b/lib/honyaku/release.ex new file mode 100644 index 0000000..0586787 --- /dev/null +++ b/lib/honyaku/release.ex @@ -0,0 +1,28 @@ +defmodule Honyaku.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :honyaku + + def migrate do + load_app() + + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + load_app() + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/rel/env.sh.eex b/rel/env.sh.eex new file mode 100755 index 0000000..efeb7ff --- /dev/null +++ b/rel/env.sh.eex @@ -0,0 +1,13 @@ +#!/bin/sh + +# configure node for distributed erlang with IPV6 support +export ERL_AFLAGS="-proto_dist inet6_tcp" +export ECTO_IPV6="true" +export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal" +export RELEASE_DISTRIBUTION="name" +export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}" + +# Uncomment to send crash dumps to stderr +# This can be useful for debugging, but may log sensitive information +# export ERL_CRASH_DUMP=/dev/stderr +# export ERL_CRASH_DUMP_BYTES=4096 diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate new file mode 100755 index 0000000..d751d71 --- /dev/null +++ b/rel/overlays/bin/migrate @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +exec ./honyaku eval Honyaku.Release.migrate diff --git a/rel/overlays/bin/migrate.bat b/rel/overlays/bin/migrate.bat new file mode 100755 index 0000000..78af5fc --- /dev/null +++ b/rel/overlays/bin/migrate.bat @@ -0,0 +1 @@ +call "%~dp0\honyaku" eval Honyaku.Release.migrate diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server new file mode 100755 index 0000000..31dfd87 --- /dev/null +++ b/rel/overlays/bin/server @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./honyaku start diff --git a/rel/overlays/bin/server.bat b/rel/overlays/bin/server.bat new file mode 100755 index 0000000..6e40af4 --- /dev/null +++ b/rel/overlays/bin/server.bat @@ -0,0 +1,2 @@ +set PHX_SERVER=true +call "%~dp0\honyaku" start