Initial commit on Forgejo
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s

Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:40 -04:00
commit af6077b9c3
455 changed files with 78366 additions and 0 deletions

4
deploy/caddy/Caddyfile Normal file
View file

@ -0,0 +1,4 @@
tarakan.lol {
encode zstd gzip
reverse_proxy 127.0.0.1:4000
}

121
deploy/docker/Dockerfile Normal file
View file

@ -0,0 +1,121 @@
# This file is based on these images:
#
# - https://hub.docker.com/r/hexpm/elixir/tags - for the builder image
# E.g.: docker.io/hexpm/elixir:1.20.1-erlang-29.0.2-debian-trixie-20260610-slim
# - https://hub.docker.com/_/debian/tags?name=trixie-20260610-slim - for the runner image
# E.g.: docker.io/debian:trixie-20260610-slim
#
# Find builder and runner images on Docker Hub or on Hex's Build Server (Bob).
# We recommend using Bob's Web UI to find recent tags:
#
# - https://bob.hex.pm/docker
#
# We suggest using the same Debian version for both the builder and runner images.
#
# We suggest Debian/Ubuntu instead of Alpine to avoid production compatibility issues
# (such as DNS resolution failures, and dynamically linked NIFs/precompiled binaries).
#
# For finding packages in Debian, search on https://packages.debian.org/.
ARG ELIXIR_VERSION=1.20.1
ARG OTP_VERSION=29.0.2
ARG DEBIAN_VERSION=trixie-20260610-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
# git: Tarakan shells out to git for pinned-commit snapshots, blobless
# mirrors, and smart-HTTP/SSH hosting — it must be present at runtime.
# openssh-client: Tarakan.GitSSH.Server generates its ed25519 host key with
# ssh-keygen on first boot. Without it the daemon fails to start, and
# because that failure stops a supervised child, the whole release
# refuses to boot rather than merely losing SSH.
# tini: reaps the many short-lived git subprocesses so they don't become
# zombies.
RUN apt-get update \
&& apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates git openssh-client tini \
&& 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
# Persistent storage for hosted repositories, the mirror hot-tier, and the SSH
# host key. These are mounted as volumes in production; creating them owned by
# nobody means a fresh named volume inherits writable ownership. ssh is 0700
# because ssh-keygen refuses to use a key whose directory is group-readable.
RUN mkdir -p /app/storage/mirrors /app/storage/hosted /app/storage/ssh \
&& chmod 700 /app/storage/ssh \
&& chown -R nobody /app/storage
# 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/tarakan ./
USER nobody
# tini reaps zombie git subprocesses.
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/app/bin/server"]

110
deploy/docker/compose.yml Normal file
View file

@ -0,0 +1,110 @@
# Single-node Tarakan deployment: the Phoenix release plus PostgreSQL.
#
# From the repo root (or /opt/tarakan after rsync):
#
# 1. cp .env.example .env && edit it (at minimum SECRET_KEY_BASE, PHX_HOST)
# 2. docker compose --project-directory . -f deploy/docker/compose.yml up -d --build
#
# Production activate path: scripts/deploy/deploy.sh (loads a pre-built image).
# Put a TLS-terminating reverse proxy (Caddy, nginx, Traefik) in front for HTTPS.
services:
db:
image: postgres:16
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment:
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: ${POSTGRES_DB:-tarakan}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 10
app:
image: ${APP_IMAGE:-tarakan-app}
build:
context: ../..
dockerfile: deploy/docker/Dockerfile
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
security_opt:
- no-new-privileges:true
# Erlang sizes its port table from RLIMIT_NOFILE. Some Docker hosts expose
# a billion-file limit, which wastes more than 1 GB before serving traffic.
ulimits:
nofile:
soft: 65536
hard: 65536
depends_on:
db:
condition: service_healthy
environment:
# Required.
DATABASE_URL: ecto://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB:-tarakan}
# Same Docker network as Postgres — no TLS between containers.
DATABASE_SSL: ${DATABASE_SSL:-false}
SECRET_KEY_BASE: ${SECRET_KEY_BASE:?set SECRET_KEY_BASE in .env (mix phx.gen.secret)}
PHX_HOST: ${PHX_HOST:-localhost}
PORT: ${PORT:-4000}
# Keep the default pool small enough for the minimum 2 GB deployment.
POOL_SIZE: ${POOL_SIZE:-5}
# When a reverse proxy sits in front, list its Docker/host CIDRs so client
# IPs and rate limits are correct (e.g. 172.16.0.0/12,127.0.0.1).
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-}
# Persistent repository storage (see volumes below).
MIRROR_DIR: /app/storage/mirrors
HOSTED_DIR: /app/storage/hosted
# Git over SSH. Off unless GIT_SSH_ENABLED is true|1. The host key lives
# on its own volume: it is the server's identity, so regenerating it
# makes every client that has ever connected refuse with
# "REMOTE HOST IDENTIFICATION HAS CHANGED".
GIT_SSH_ENABLED: ${GIT_SSH_ENABLED:-false}
GIT_SSH_PORT: ${GIT_SSH_PORT:-2222}
GIT_SSH_HOST_KEY_DIR: /app/storage/ssh
# Recommended in production: authenticated GitHub API (5k req/hr, bulk sync).
GITHUB_TOKEN: ${GITHUB_TOKEN:-}
# Optional OAuth sign-in.
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-}
# Transactional email through Elektrine's scoped external API.
ELEKTRINE_EMAIL_API_URL: ${ELEKTRINE_EMAIL_API_URL:-https://elektrine.com}
ELEKTRINE_EMAIL_API_KEY: ${ELEKTRINE_EMAIL_API_KEY:?set ELEKTRINE_EMAIL_API_KEY in .env}
# Migrate, then serve. Both boot the release; migrate exits first.
command: ["/bin/sh", "-c", "/app/bin/migrate && /app/bin/server"]
ports:
# The TLS proxy runs on the host. Do not expose Phoenix directly.
- "${BIND_IP:-127.0.0.1}:${PORT:-4000}:${PORT:-4000}"
# SSH speaks its own transport, so unlike HTTP it is published directly
# rather than proxied. Bound to all interfaces by default because a git
# remote has to reach it from outside the host; set GIT_SSH_BIND_IP to
# narrow it. Harmless while GIT_SSH_ENABLED is false - nothing listens.
- "${GIT_SSH_BIND_IP:-0.0.0.0}:${GIT_SSH_PORT:-2222}:${GIT_SSH_PORT:-2222}"
volumes:
# hosted = source of truth for Tarakan-hosted repos. BACK THIS UP.
- hosted:/app/storage/hosted
# mirrors = regenerable cache of remote repos (rebuilt from upstream on a
# miss). Safe to exclude from backups and to wipe.
- mirrors:/app/storage/mirrors
# ssh = the daemon's host key. Small, irreplaceable, BACK THIS UP: losing
# it is indistinguishable from a man-in-the-middle to every existing
# client, and they will refuse to connect until the user clears it.
- ssh:/app/storage/ssh
volumes:
db_data:
hosted:
mirrors:
ssh:

View file

@ -0,0 +1,23 @@
[Unit]
Description=Back up Tarakan database and hosted repositories
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
User=linuxuser
Group=linuxuser
WorkingDirectory=/opt/tarakan
ExecStart=/opt/tarakan/scripts/deploy/backup.sh
# The backup process only needs its application directory and Docker.
NoNewPrivileges=true
PrivateTmp=true
# Debian's Docker CLI discovers its Compose plugin through a home-visible path.
# Keep home directories immutable while allowing plugin discovery.
ProtectHome=read-only
ProtectSystem=full
ReadWritePaths=/opt/tarakan/backups
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,11 @@
[Unit]
Description=Run the Tarakan backup daily
[Timer]
OnCalendar=*-*-* 03:17:00 UTC
RandomizedDelaySec=15m
Persistent=true
Unit=tarakan-backup.service
[Install]
WantedBy=timers.target