···11+# This file excludes paths from the Docker build context.
22+#
33+# By default, Docker's build context includes all files (and folders) in the
44+# current directory. Even if a file isn't copied into the container it is still sent to
55+# the Docker daemon.
66+#
77+# There are multiple reasons to exclude files from the build context:
88+#
99+# 1. Prevent nested folders from being copied into the container (ex: exclude
1010+# /assets/node_modules when copying /assets)
1111+# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc)
1212+# 3. Avoid sending files containing sensitive information
1313+#
1414+# More information on using .dockerignore is available here:
1515+# https://docs.docker.com/engine/reference/builder/#dockerignore-file
1616+1717+.dockerignore
1818+1919+# Ignore git, but keep git HEAD and refs to access current commit hash if needed:
2020+#
2121+# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
2222+# d0b8727759e1e0e7aa3d41707d12376e373d5ecc
2323+.git
2424+!.git/HEAD
2525+!.git/refs
2626+2727+# Common development/test artifacts
2828+/cover/
2929+/doc/
3030+/test/
3131+/tmp/
3232+.elixir_ls
3333+3434+# Mix artifacts
3535+/_build/
3636+/deps/
3737+*.ez
3838+3939+# Generated on crash by the VM
4040+erl_crash.dump
4141+4242+# Static artifacts - These should be fetched and built inside the Docker image
4343+# https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Release.html#module-docker
4444+/assets/node_modules/
4545+/priv/static/assets/
4646+/priv/static/cache_manifest.json
+101
Dockerfile
···11+# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
22+# instead of Alpine to avoid DNS resolution issues in production.
33+#
44+# https://hub.docker.com/r/hexpm/elixir/tags?name=ubuntu
55+# https://hub.docker.com/_/ubuntu/tags
66+#
77+# This file is based on these images:
88+#
99+# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
1010+# - https://hub.docker.com/_/debian/tags?name=trixie-20260610-slim - for the release image
1111+# - https://pkgs.org/ - resource for finding needed packages
1212+# - Ex: docker.io/hexpm/elixir:1.20.0-erlang-29.0.1-debian-trixie-20260610-slim
1313+#
1414+ARG ELIXIR_VERSION=1.20.0
1515+ARG OTP_VERSION=29.0.1
1616+ARG DEBIAN_VERSION=trixie-20260610-slim
1717+1818+ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
1919+ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}"
2020+2121+FROM ${BUILDER_IMAGE} AS builder
2222+2323+# install build dependencies
2424+RUN apt-get update \
2525+ && apt-get install -y --no-install-recommends build-essential git \
2626+ && rm -rf /var/lib/apt/lists/*
2727+2828+# prepare build dir
2929+WORKDIR /app
3030+3131+# install hex + rebar
3232+RUN mix local.hex --force \
3333+ && mix local.rebar --force
3434+3535+# set build ENV
3636+ENV MIX_ENV="prod"
3737+3838+# install mix dependencies
3939+COPY mix.exs mix.lock ./
4040+RUN mix deps.get --only $MIX_ENV
4141+RUN mkdir config
4242+4343+# copy compile-time config files before we compile dependencies
4444+# to ensure any relevant config change will trigger the dependencies
4545+# to be re-compiled.
4646+COPY config/config.exs config/${MIX_ENV}.exs config/
4747+RUN mix deps.compile
4848+4949+RUN mix assets.setup
5050+5151+COPY priv priv
5252+5353+COPY lib lib
5454+5555+# Compile the release
5656+RUN mix compile
5757+5858+COPY assets assets
5959+6060+# compile assets
6161+RUN mix assets.deploy
6262+6363+# Changes to config/runtime.exs don't require recompiling the code
6464+COPY config/runtime.exs config/
6565+6666+COPY rel rel
6767+RUN mix release
6868+6969+# start a new build stage so that the final image will only contain
7070+# the compiled release and other runtime necessities
7171+FROM ${RUNNER_IMAGE} AS final
7272+7373+RUN apt-get update \
7474+ && apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates curl \
7575+ && rm -rf /var/lib/apt/lists/*
7676+7777+# Set the locale
7878+RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
7979+ && locale-gen
8080+8181+ENV LANG=en_US.UTF-8
8282+ENV LANGUAGE=en_US:en
8383+ENV LC_ALL=en_US.UTF-8
8484+8585+WORKDIR "/app"
8686+RUN chown nobody /app
8787+8888+# set runner ENV
8989+ENV MIX_ENV="prod"
9090+9191+# Only copy the final release from the build stage
9292+COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/annot_at ./
9393+9494+USER nobody
9595+9696+# If using an environment that doesn't automatically reap zombie processes, it is
9797+# advised to add an init process such as tini via `apt-get install`
9898+# above and adding an entrypoint. See https://github.com/krallin/tini for details
9999+# ENTRYPOINT ["/tini", "--"]
100100+101101+CMD ["/app/bin/server"]
+12
README.md
···2626```
27272828Make sure that same host is set in `dev.exs` and the app has been restarted. Click login and use any active atproto handle, and you should be able to OAuth against it.
2929+3030+## Deploying
3131+3232+You need to set up some env vars to run this service in a production environment, `SECRET_KEY_BASE`, `DATABASE_URL`, `HOST`, `CLOAK_KEY`, and `ATPROTO_CLIENT_PRIVATE_JWK`.
3333+3434+- `SECRET_KEY_BASE` - run `mix phx.gen.secret 64` and copy the output
3535+- `DATABASE_URL` - a Postgres database URL like `postgresql://app:pass@db:5432/db_name`
3636+- `HOST` - a domain name like `annot.at`
3737+- `CLOAK_KEY` - run `mix run -e 'IO.puts(Base.encode64(:crypto.strong_rand_bytes(32)))'` and copy the output
3838+- `ATPROTO_CLIENT_PRIVATE_JWK` - run `mix run -e '{_, jwk} = JOSE.JWK.to_map(JOSE.JWK.generate_key({:ec, "P-256"})); IO.puts(Jason.encode!(jwk))'` and copy the output
3939+4040+Note that `ATPROTO_CLIENT_PRIVATE_JWK`, `CLOAK_KEY`, and `SECRET_KEY_BASE` are secrets and should not be shared.
+30
lib/annot_at/release.ex
···11+defmodule AnnotAt.Release do
22+ @moduledoc """
33+ Used for executing DB release tasks when run in production without Mix
44+ installed.
55+ """
66+ @app :annot_at
77+88+ def migrate do
99+ load_app()
1010+1111+ for repo <- repos() do
1212+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
1313+ end
1414+ end
1515+1616+ def rollback(repo, version) do
1717+ load_app()
1818+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
1919+ end
2020+2121+ defp repos do
2222+ Application.fetch_env!(@app, :ecto_repos)
2323+ end
2424+2525+ defp load_app do
2626+ # Many platforms require SSL when connecting to the database
2727+ Application.ensure_all_started(:ssl)
2828+ Application.ensure_loaded(@app)
2929+ end
3030+end