diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5d2adff --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# 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 +# https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Release.html#module-docker +/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..2438747 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,101 @@ +# 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?name=ubuntu +# https://hub.docker.com/_/ubuntu/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/tags?name=trixie-20260112-slim - for the release image +# - https://pkgs.org/ - resource for finding needed packages +# - Ex: docker.io/hexpm/elixir:1.19.4-erlang-27.3.4.6-debian-trixie-20260112-slim +# +ARG ELIXIR_VERSION=1.19.4 +ARG OTP_VERSION=27.3.4.6 +ARG DEBIAN_VERSION=trixie-20260112-slim + +ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" +ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}" + +FROM ${BUILDER_IMAGE} AS builder + +# install build dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential git \ + && rm -rf /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 + +RUN mix assets.setup + +COPY priv priv + +COPY lib lib + +# Compile the release +RUN mix compile + +COPY assets assets + +# compile assets +RUN mix assets.deploy + +# 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} AS final + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates \ + && rm -rf /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/spazio_solazzo ./ + +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/README.md b/README.md index be95284..f4f3714 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Now, to start your Phoenix server: * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser and you're ready to go! +Now you can visit [`localhost:8080`](http://localhost:8080) from your browser and you're ready to go! ## License diff --git a/config/config.exs b/config/config.exs index da0c963..bbcbcee 100644 --- a/config/config.exs +++ b/config/config.exs @@ -54,7 +54,7 @@ config :spazio_solazzo, ecto_repos: [SpazioSolazzo.Repo], generators: [timestamp_type: :utc_datetime], ash_domains: [SpazioSolazzo.Accounts, SpazioSolazzo.BookingSystem], - base_url: "http://localhost:4000", + base_url: "http://localhost:8080", ash_authentication: [return_error_on_invalid_magic_link_token?: true] config :spazio_solazzo, Oban, diff --git a/config/runtime.exs b/config/runtime.exs index 8b3a35b..b2897a5 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -20,8 +20,9 @@ if System.get_env("PHX_SERVER") do config :spazio_solazzo, SpazioSolazzoWeb.Endpoint, server: true end -config :spazio_solazzo, SpazioSolazzoWeb.Endpoint, - http: [port: String.to_integer(System.get_env("PORT", "4000"))] +port = String.to_integer(System.get_env("PORT", "8080")) + +config :spazio_solazzo, SpazioSolazzoWeb.Endpoint, http: [port: port] if config_env() == :prod do database_url = @@ -36,6 +37,8 @@ if config_env() == :prod do config :spazio_solazzo, SpazioSolazzo.Repo, # ssl: true, url: database_url, + ssl: true, + socket_options: [:inet], pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), # For machines with several cores, consider starting multiple pools of `pool_size` # pool_count: 4, @@ -60,12 +63,10 @@ if config_env() == :prod do config :spazio_solazzo, SpazioSolazzoWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ - # Enable IPv6 and bind on all interfaces. - # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. - # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 - # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {0, 0, 0, 0, 0, 0, 0, 0} + ip: {0, 0, 0, 0}, + port: port ], + server: true, secret_key_base: secret_key_base config :spazio_solazzo, SpazioSolazzo.Mailer, diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..4fac178 --- /dev/null +++ b/fly.toml @@ -0,0 +1,36 @@ +# fly.toml app configuration file generated for spazio-solazzo on 2026-01-13T12:30:59+01:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'spazio-solazzo' +primary_region = 'ams' +kill_signal = 'SIGTERM' + +[build] + +[deploy] +release_command = '/app/bin/migrate' +wait_timeout = "10m" + +[env] +PHX_HOST = 'spazio-solazzo.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 = '512mb' +cpus = 1 +memory_mb = 512 diff --git a/lib/spazio_solazzo/release.ex b/lib/spazio_solazzo/release.ex new file mode 100644 index 0000000..94df41a --- /dev/null +++ b/lib/spazio_solazzo/release.ex @@ -0,0 +1,43 @@ +defmodule SpazioSolazzo.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + @app :spazio_solazzo + + 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 + + def seed do + load_app() + {:ok, _} = Application.ensure_all_started(@app) + + for _repo <- repos() do + seed_file = Application.app_dir(@app, "priv/repo/seeds.exs") + + if File.exists?(seed_file) do + Code.eval_file(seed_file) + end + end + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + # Many platforms require SSL when connecting to the database + Application.ensure_all_started(:ssl) + Application.ensure_loaded(@app) + end +end diff --git a/lib/spazio_solazzo_web/live/page_live.ex b/lib/spazio_solazzo_web/live/page_live.ex index 1073d27..60f752f 100644 --- a/lib/spazio_solazzo_web/live/page_live.ex +++ b/lib/spazio_solazzo_web/live/page_live.ex @@ -5,9 +5,9 @@ defmodule SpazioSolazzoWeb.PageLive do alias SpazioSolazzo.BookingSystem def mount(_params, _session, socket) do - {:ok, coworking_space} = BookingSystem.get_space_by_slug("coworking") - {:ok, meeting_space} = BookingSystem.get_space_by_slug("meeting") - {:ok, music_space} = BookingSystem.get_space_by_slug("music") + {:ok, coworking_space} = BookingSystem.get_space_by_slug("coworking", not_found_error?: false) + {:ok, meeting_space} = BookingSystem.get_space_by_slug("meeting", not_found_error?: false) + {:ok, music_space} = BookingSystem.get_space_by_slug("music", not_found_error?: false) {:ok, assign(socket, diff --git a/rel/env.sh.eex b/rel/env.sh.eex new file mode 100755 index 0000000..f896ca0 --- /dev/null +++ b/rel/env.sh.eex @@ -0,0 +1,19 @@ +#!/bin/sh + +if [ -n "$FLY_APP_NAME" ]; then + export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal" + export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}" + # configure node for distributed erlang with IPV6 support + export ERL_AFLAGS="-proto_dist inet6_tcp" + export ECTO_IPV6="true" +fi + +export RELEASE_DISTRIBUTION="name" + +# 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 + +# when not running on fly.io, use a sensible default +export RELEASE_NODE=${RELEASE_NODE:-<%= @release.name %>@$(hostname)} diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate new file mode 100755 index 0000000..c8c1473 --- /dev/null +++ b/rel/overlays/bin/migrate @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +exec ./spazio_solazzo eval SpazioSolazzo.Release.migrate diff --git a/rel/overlays/bin/migrate.bat b/rel/overlays/bin/migrate.bat new file mode 100755 index 0000000..469fa48 --- /dev/null +++ b/rel/overlays/bin/migrate.bat @@ -0,0 +1 @@ +call "%~dp0\spazio_solazzo" eval SpazioSolazzo.Release.migrate diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server new file mode 100755 index 0000000..fbee6cf --- /dev/null +++ b/rel/overlays/bin/server @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./spazio_solazzo start diff --git a/rel/overlays/bin/server.bat b/rel/overlays/bin/server.bat new file mode 100755 index 0000000..c5102ba --- /dev/null +++ b/rel/overlays/bin/server.bat @@ -0,0 +1,2 @@ +set PHX_SERVER=true +call "%~dp0\spazio_solazzo" start