annot.at is a service for syncing static websites, such as blogs, to atproto and standard.site annot.at
elixir atproto standardsite
3

Configure Feed

Select the types of activity you want to include in your feed.

Set up deployment plumbing

Ran `mix phx.gen.release --docker` to get a release and dockerfile and added some docs on how to set up the env vars

Johanna Larsson (Jun 19, 2026, 5:05 PM +0100) e313a9cd 3bb4edb5

+209 -2
+46
.dockerignore
··· 1 + # This file excludes paths from the Docker build context. 2 + # 3 + # By default, Docker's build context includes all files (and folders) in the 4 + # current directory. Even if a file isn't copied into the container it is still sent to 5 + # the Docker daemon. 6 + # 7 + # There are multiple reasons to exclude files from the build context: 8 + # 9 + # 1. Prevent nested folders from being copied into the container (ex: exclude 10 + # /assets/node_modules when copying /assets) 11 + # 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc) 12 + # 3. Avoid sending files containing sensitive information 13 + # 14 + # More information on using .dockerignore is available here: 15 + # https://docs.docker.com/engine/reference/builder/#dockerignore-file 16 + 17 + .dockerignore 18 + 19 + # Ignore git, but keep git HEAD and refs to access current commit hash if needed: 20 + # 21 + # $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat 22 + # d0b8727759e1e0e7aa3d41707d12376e373d5ecc 23 + .git 24 + !.git/HEAD 25 + !.git/refs 26 + 27 + # Common development/test artifacts 28 + /cover/ 29 + /doc/ 30 + /test/ 31 + /tmp/ 32 + .elixir_ls 33 + 34 + # Mix artifacts 35 + /_build/ 36 + /deps/ 37 + *.ez 38 + 39 + # Generated on crash by the VM 40 + erl_crash.dump 41 + 42 + # Static artifacts - These should be fetched and built inside the Docker image 43 + # https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Release.html#module-docker 44 + /assets/node_modules/ 45 + /priv/static/assets/ 46 + /priv/static/cache_manifest.json
+101
Dockerfile
··· 1 + # Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian 2 + # instead of Alpine to avoid DNS resolution issues in production. 3 + # 4 + # https://hub.docker.com/r/hexpm/elixir/tags?name=ubuntu 5 + # https://hub.docker.com/_/ubuntu/tags 6 + # 7 + # This file is based on these images: 8 + # 9 + # - https://hub.docker.com/r/hexpm/elixir/tags - for the build image 10 + # - https://hub.docker.com/_/debian/tags?name=trixie-20260610-slim - for the release image 11 + # - https://pkgs.org/ - resource for finding needed packages 12 + # - Ex: docker.io/hexpm/elixir:1.20.0-erlang-29.0.1-debian-trixie-20260610-slim 13 + # 14 + ARG ELIXIR_VERSION=1.20.0 15 + ARG OTP_VERSION=29.0.1 16 + ARG DEBIAN_VERSION=trixie-20260610-slim 17 + 18 + ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" 19 + ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}" 20 + 21 + FROM ${BUILDER_IMAGE} AS builder 22 + 23 + # install build dependencies 24 + RUN apt-get update \ 25 + && apt-get install -y --no-install-recommends build-essential git \ 26 + && rm -rf /var/lib/apt/lists/* 27 + 28 + # prepare build dir 29 + WORKDIR /app 30 + 31 + # install hex + rebar 32 + RUN mix local.hex --force \ 33 + && mix local.rebar --force 34 + 35 + # set build ENV 36 + ENV MIX_ENV="prod" 37 + 38 + # install mix dependencies 39 + COPY mix.exs mix.lock ./ 40 + RUN mix deps.get --only $MIX_ENV 41 + RUN mkdir config 42 + 43 + # copy compile-time config files before we compile dependencies 44 + # to ensure any relevant config change will trigger the dependencies 45 + # to be re-compiled. 46 + COPY config/config.exs config/${MIX_ENV}.exs config/ 47 + RUN mix deps.compile 48 + 49 + RUN mix assets.setup 50 + 51 + COPY priv priv 52 + 53 + COPY lib lib 54 + 55 + # Compile the release 56 + RUN mix compile 57 + 58 + COPY assets assets 59 + 60 + # compile assets 61 + RUN mix assets.deploy 62 + 63 + # Changes to config/runtime.exs don't require recompiling the code 64 + COPY config/runtime.exs config/ 65 + 66 + COPY rel rel 67 + RUN mix release 68 + 69 + # start a new build stage so that the final image will only contain 70 + # the compiled release and other runtime necessities 71 + FROM ${RUNNER_IMAGE} AS final 72 + 73 + RUN apt-get update \ 74 + && apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates curl \ 75 + && rm -rf /var/lib/apt/lists/* 76 + 77 + # Set the locale 78 + RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \ 79 + && locale-gen 80 + 81 + ENV LANG=en_US.UTF-8 82 + ENV LANGUAGE=en_US:en 83 + ENV LC_ALL=en_US.UTF-8 84 + 85 + WORKDIR "/app" 86 + RUN chown nobody /app 87 + 88 + # set runner ENV 89 + ENV MIX_ENV="prod" 90 + 91 + # Only copy the final release from the build stage 92 + COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/annot_at ./ 93 + 94 + USER nobody 95 + 96 + # If using an environment that doesn't automatically reap zombie processes, it is 97 + # advised to add an init process such as tini via `apt-get install` 98 + # above and adding an entrypoint. See https://github.com/krallin/tini for details 99 + # ENTRYPOINT ["/tini", "--"] 100 + 101 + CMD ["/app/bin/server"]
+12
README.md
··· 26 26 ``` 27 27 28 28 Make 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. 29 + 30 + ## Deploying 31 + 32 + 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`. 33 + 34 + - `SECRET_KEY_BASE` - run `mix phx.gen.secret 64` and copy the output 35 + - `DATABASE_URL` - a Postgres database URL like `postgresql://app:pass@db:5432/db_name` 36 + - `HOST` - a domain name like `annot.at` 37 + - `CLOAK_KEY` - run `mix run -e 'IO.puts(Base.encode64(:crypto.strong_rand_bytes(32)))'` and copy the output 38 + - `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 39 + 40 + 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
··· 1 + defmodule AnnotAt.Release do 2 + @moduledoc """ 3 + Used for executing DB release tasks when run in production without Mix 4 + installed. 5 + """ 6 + @app :annot_at 7 + 8 + def migrate do 9 + load_app() 10 + 11 + for repo <- repos() do 12 + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) 13 + end 14 + end 15 + 16 + def rollback(repo, version) do 17 + load_app() 18 + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) 19 + end 20 + 21 + defp repos do 22 + Application.fetch_env!(@app, :ecto_repos) 23 + end 24 + 25 + defp load_app do 26 + # Many platforms require SSL when connecting to the database 27 + Application.ensure_all_started(:ssl) 28 + Application.ensure_loaded(@app) 29 + end 30 + end
+4 -2
mix.exs
··· 47 47 {:phoenix_html, "~> 4.1"}, 48 48 {:phoenix_live_reload, "~> 1.2", only: :dev}, 49 49 {:phoenix_live_view, "~> 1.1.0"}, 50 - {:lazy_html, ">= 0.1.0", only: :test}, 50 + {:lazy_html, ">= 0.1.0"}, 51 51 {:phoenix_live_dashboard, "~> 0.8.3"}, 52 52 {:esbuild, "~> 0.10", runtime: Mix.env() == :dev}, 53 53 {:tailwind, "~> 0.3", runtime: Mix.env() == :dev}, ··· 70 70 {:credo, "~> 1.7", only: [:dev, :test]}, 71 71 {:mimic, "~> 2.3", only: :test}, 72 72 {:cloak, "~> 1.1"}, 73 - {:cloak_ecto, "~> 1.3"} 73 + {:cloak_ecto, "~> 1.3"}, 74 + {:saxy, "~> 1.6"}, 75 + {:date_time_parser, "~> 1.3"} 74 76 ] 75 77 end 76 78
+3
mix.lock
··· 5 5 "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, 6 6 "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, 7 7 "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, 8 + "date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"}, 8 9 "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, 9 10 "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, 10 11 "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, ··· 23 24 "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, 24 25 "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, 25 26 "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, 27 + "kday": {:hex, :kday, "1.1.0", "64efac85279a12283eaaf3ad6f13001ca2dff943eda8c53288179775a8c057a0", [:mix], [{:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}], "hexpm", "69703055d63b8d5b260479266c78b0b3e66f7aecdd2022906cd9bf09892a266d"}, 26 28 "lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"}, 27 29 "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 28 30 "mimic": {:hex, :mimic, "2.3.0", "88b1d13c285e57df6ea57204317bb56e49e7329668006cdcb80a9aafc73a9616", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "52771f23689398c5d41c7d05e91c2c28e10df273b784f40ca8b02e35e46850d3"}, ··· 41 43 "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, 42 44 "postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"}, 43 45 "req": {:hex, :req, "0.5.18", "48e6431cb4135e8a7815e745177485369a9b4a9924d5fe68ca00eb09ceaed1ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "fa03812c440a9754bf34355e0c5d4f3ed316458db62e3284b7a352ef8dc0b996"}, 46 + "saxy": {:hex, :saxy, "1.6.0", "02cb4e9bd045f25ac0c70fae8164754878327ee393c338a090288210b02317ee", [:mix], [], "hexpm", "ef42eb4ac983ca77d650fbdb68368b26570f6cc5895f0faa04d34a6f384abad3"}, 44 47 "swoosh": {:hex, :swoosh, "1.26.1", "2989f1089e3cf1a938bbd3908c11d7dba8c8a4fe78801a7762d26f01bdbb32b4", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ad197c025102bbcbaffcd67c10ff7b390746c32f534ff5158891b149460118e"}, 45 48 "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, 46 49 "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
+5
rel/overlays/bin/migrate
··· 1 + #!/bin/sh 2 + set -eu 3 + 4 + cd -P -- "$(dirname -- "$0")" 5 + exec ./annot_at eval AnnotAt.Release.migrate
+1
rel/overlays/bin/migrate.bat
··· 1 + call "%~dp0\annot_at" eval AnnotAt.Release.migrate
+5
rel/overlays/bin/server
··· 1 + #!/bin/sh 2 + set -eu 3 + 4 + cd -P -- "$(dirname -- "$0")" 5 + PHX_SERVER=true exec ./annot_at start
+2
rel/overlays/bin/server.bat
··· 1 + set PHX_SERVER=true 2 + call "%~dp0\annot_at" start