Hosting Elixir libraries on your own in your own server infrastructure or in your local dev environment.
As a company or out of any other scenario you might want/need the ability to host your own Elixir / Erlang libraries within your internal network. For example when company knowledge legally should not be exposed publicly on Hex.pm.
This project was inspired by the MiniRepo Github project by Wojtek Mach. I personally was missing a Web UI and the library documentations and started this project to add those things.
- Hosting your private Elixir libraries (also Erlang libraries)
- Publishing packages and their documentation
- Mirroring public Hex.pm libraries
- Functionial HEX API to provide your private libraries for your apps
- Various storage adapter for local filesystem or S3
- Web UI listing all available libraries/version
- Web UI also renders the documentation for your Elixir libraries
- Simple phoenix app which is simple to run or deploy
- More storage adapters (AWS.KMS, Swift, ...)
- Mirroring public Hex libraries incl. their dependency trees
- Admin UI to configure several things like the mirroring
- Your suggestions
LocalHex can be deployed as a normal Phoenix app (mix phx.server), as an OTP release (mix release), or via Docker.
In production (MIX_ENV=prod) the application is configured at runtime from environment variables (see config/runtime.exs).
This keeps secrets out of git and works well for containers / Kubernetes.
A Dockerfile and docker-compose.yml are included.
cp env.example .env
# edit .env and set at least:
# - SECRET_KEY_BASE
# - LOCAL_HEX_AUTH_TOKEN
docker compose up --buildNotes:
- The compose file mounts
./priv/reposand./priv/static/storageinto the container for persistence. - The compose file mounts the test keys from
./test/fixtures/*.pemfor convenience. Do not use those keys in production. - To generate
SECRET_KEY_BASE:openssl rand -base64 48 - CI builds/pushes a container image to GHCR as
ghcr.io/<repo_owner>/local_hex_repo:<tag>when git tags are pushed.
Required:
SECRET_KEY_BASE(orSECRET_KEY_BASE_PATHpointing to a file containing the secret)LOCAL_HEX_AUTH_TOKEN(orLOCAL_HEX_AUTH_TOKEN_PATHpointing to a file containing the token)- Signing keys (provide either PEM or file paths):
LOCAL_HEX_PRIVATE_KEY_PEMorLOCAL_HEX_PRIVATE_KEY_PATHLOCAL_HEX_PUBLIC_KEY_PEMorLOCAL_HEX_PUBLIC_KEY_PATH
Common:
LOCAL_HEX_REPO_NAME(default:local_hex)PORT(default:4000)PHX_HOST(default:localhost)LOG_LEVEL(ex.debug|info|warning|error, default:info)PHX_STATIC_GZIP(trueto serve pre-compressed assets frompriv/staticin prod)LOCAL_HEX_DOCS_CACHE_DIR(recommended in containers): writable directory where documentation tarballs are extracted and served from under/docs/*(ex./var/local_hex/docs)
Storage:
- Local filesystem (default):
LOCAL_HEX_STORE=localLOCAL_HEX_REPO_ROOT(default:/data/repos)
- S3:
LOCAL_HEX_STORE=s3LOCAL_HEX_S3_BUCKET(required)AWS_REGION/AWS_DEFAULT_REGION(default:us-east-1)- Optional IRSA / web identity:
AWS_ROLE_ARN+AWS_WEB_IDENTITY_TOKEN_FILE
Kubernetes note (docs):
Documentation pages are served from extracted tarballs under /docs/*. In Docker/Kubernetes, the release’s priv/static directory is often read-only, so set LOCAL_HEX_DOCS_CACHE_DIR to a writable volume.
Example emptyDir mount:
volumes:
- name: local-hex-docs
emptyDir: {}
containers:
- name: local-hex
env:
- name: LOCAL_HEX_DOCS_CACHE_DIR
value: /var/local_hex/docs
volumeMounts:
- name: local-hex-docs
mountPath: /var/local_hex/docsNote: emptyDir is per-pod. If you run multiple replicas of local_hex_repo and want a shared docs cache across instances, use a PVC (or other shared volume) instead.
To configure the mirror ability add the following repository configuration for the :mirror to your configuration list of repositories.
In production you can also enable the mirror via environment variables:
LOCAL_HEX_MIRROR_ENABLED=trueLOCAL_HEX_MIRROR_UPSTREAM_PUBLIC_KEY_PEM(orLOCAL_HEX_MIRROR_UPSTREAM_PUBLIC_KEY_PATH)- Optional overrides:
LOCAL_HEX_MIRROR_REPO_NAME,LOCAL_HEX_MIRROR_SYNC_INTERVAL_MS,LOCAL_HEX_MIRROR_UPSTREAM_NAME,LOCAL_HEX_MIRROR_UPSTREAM_URL
config :local_hex,
repositories: [
...,
mirror: [
name: "local_hex_dev_mirror", # Any name you like
store: {LocalHex.Storage.Local, root: {:local_hex, "priv/repos/"}},
private_key: File.read!(Path.expand("../test/fixtures/test_private_key.pem", __DIR__)),
public_key: File.read!(Path.expand("../test/fixtures/test_public_key.pem", __DIR__)),
options: %{
sync_interval: 60 * 60 * 1000, # every hour
sync_opts: [max_concurrency: 5, timeout: 20000],
sync_on_demand: true,
sync_only: ~w(jason, phoenix, ...), # Any library you like
# Source: https://hex.pm/docs/public_keys
upstream_name: "hexpm",
upstream_url: "https://repo.hex.pm",
upstream_public_key: """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqREcFDt5vV21JVe2QNB
Edvzk6w36aNFhVGWN5toNJRjRJ6m4hIuG4KaXtDWVLjnvct6MYMfqhC79HAGwyF+
IqR6Q6a5bbFSsImgBJwz1oadoVKD6ZNetAuCIK84cjMrEFRkELtEIPNHblCzUkkM
3rS9+DPlnfG8hBvGi6tvQIuZmXGCxF/73hU0/MyGhbmEjIKRtG6b0sJYKelRLTPW
XgK7s5pESgiwf2YC/2MGDXjAJfpfCd0RpLdvd4eRiXtVlE9qO9bND94E7PgQ/xqZ
J1i2xWFndWa6nfFnRxZmCStCOZWYYPlaxr+FZceFbpMwzTNs4g3d4tLNUcbKAIH4
0wIDAQAB
-----END PUBLIC KEY-----
"""
}
]
]-
name: Name of the repository which also is used in the
hex.configanddepsconfiguration -
store: Currently it's only possible to choose
LocalHex.Storage.(Local | S3)to store packages. In case more is need it is pretty easy to write another adapter. Also see the adapter modules for their configuration. -
private_key: Private key generated via
sshor any other way. This is used to sign packages. Suggestion: It's best to be provided via your infrastructure and not to be included in your codebase. -
public_key: Public key material for you private key. This is used to validate published packages with the private key. Suggestion: It's best to be provided via your infrastructure and not to be included in your codebase.
-
sync_interval: The interval in milliseconds to wait between rechecks if something new has to be mirrored
-
sync_opts: Currently only timeout or concurrency controls for the sync, more documentation and options will follow
-
sync_on_demand: Dependencies when requested but missing will be tried to synced from upstream
-
sync_only: The selection of dependencies to mirror from upstream
-
upstream_name: Default name of Hex.pm, could be changed to some third party package storage
-
upstream_url: Default url of Hex.pm, could be changed to some third party package storage url
-
upstream_public_key: Default public key of Hex.pm, could be changed to some third party package storage public key
Note: The configuration snippets below are for configuring storage via config/*.exs (development/tests or custom deployments).
For MIX_ENV=prod, prefer the environment variables described above.
The LocalHex.Storage.Local adapter writes data directly to the local filesystem.
In the config files (ex. config.exs) you can configure each repository individually by
providing a :store field that contains a tuple with the details.
In the second element in we have keyword list containing a :root field defining the preferred location of your repo
config :local_hex,
auth_token: "secret_production_token",
repositories: [
main: [
name: "local_hex",
store: {LocalHex.Storage.Local, root: {:local_hex, "priv/repos/"}},
private_key: File.read!(Path.expand("../path/to/private_key.pem", __DIR__)),
public_key: File.read!(Path.expand("../path/to/public_key.pem", __DIR__))
]
]The LocalHex.Storage.S3 adapter writes data directly to any S3 compatible storage system (AWS, S3, MinIO, etc.).
In the config files (ex. config.exs) you can configure each repository individually by
providing a :store field that contains a tuple with the details.
In the second element in we have keyword list containing a :bucket and :options field defining the preferred bucket plus additional
options to be used when communicating with the storage (see ExAWS config).
Additionally you need to configure ex_aws as well to be able to connect properly to a server and bucket
of your choice. More details you find here ExAWS
config :ex_aws, :s3,
access_key_id: "123456789",
secret_access_key: "123456789",
scheme: "http://",
host: "localhost",
port: 9000,
region: "local"
storage_config =
{LocalHex.Storage.S3,
bucket: "localhex",
options: [
region: "local"
]}
config :local_hex,
auth_token: "local_token",
repositories: [
main: [
name: "local_hex_dev",
store: storage_config,
...
]
]To publish a library to your local_hex app deployment you need to adapt the mix.exs of your library a bit. You might want to add different things as this just serves as an example.
defmodule ExampleLib.MixProject do
use Mix.Project
def project do
[
app: :example_lib,
version: "0.1.0",
...
package: package(),
hex: hex(),
]
end
defp package do
[
licenses: ["Apache 2.0"],
links: %{}
]
end
defp hex do
[
api_url: "https://local_hex.your_company.com/api",
api_key: "secret_production_token"
]
end
endNow the library can be publish via the following command:
mix hex.publishBe aware that the Hex repo needs to added in every local dev environment and especially also in the CI/CD system of your infrastructure. This is case for both the repository of your own dependencies but also for the the mirror repository in case you have set it up.
It can look like the following but these commands are also accessible in the web frontend of this app as a setup guide. In the example the repository is named local_hex in its configuration.
wget -q https://local_hex.your_company.com/public_key
mix hex.repo add local_hex https://local_hex.your_company.com --public-key public_key
rm -f public_keyUsing your locally hosted libraries in your application is quite simple by specifing the repo field in the deps config.
defp deps do
[
{:phoenix, "~> 1.6.0"},
{:your_library, "~> 1.1.3", repo: :local_hex}
]
endTo start the local runtime:
mix deps.get
mix phx.serverAlternatively, you can run it locally via Docker:
cp env.example .env
docker compose up --buildThis will add the development version of the local_hex repo to your local hex configuration:
wget -q http://localhost:4000/public_key
mix hex.repo add local_hex_dev http://localhost:4000 --public-key public_key
rm -f public_keyIf you want to publish a library, you need to adapt the mix.exs file with some hex config:
defmodule ExampleLib.MixProject do
use Mix.Project
def project do
[
app: :example_lib,
version: "0.1.0",
...
package: package(),
hex: hex(),
]
end
defp package do
[
licenses: ["Apache 2.0"],
links: %{}
]
end
defp hex do
[
api_url: "http://localhost:4000/api",
api_key: "local_token"
]
end
endNow the library can be publish via the following command:
mix hex.publish