Skip to content

Commit

Permalink
Add immich via OCI containers to plonkie
Browse files Browse the repository at this point in the history
  • Loading branch information
anoadragon453 committed Dec 17, 2023
1 parent 31da4ef commit de37bcd
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 0 deletions.
9 changes: 9 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
];

in {
# TODO: Refactor this to devShell.${sys}.default somehow.
devShell = lib.withDefaultSystems (sys: let
pkgs = allPkgs."${sys}";
in import ./shell.nix { inherit pkgs; });
Expand Down Expand Up @@ -435,6 +436,14 @@
sys.server = {
acme.email = "[email protected]";

immich = {
enable = true;
domain = "i.amorgan.xyz";
port = 8006;
storagePath = "/mnt/storagebox/media/immich";
logLevel = "log";
};

mealie = {
enable = true;
domain = "r.amorgan.xyz";
Expand Down
1 change: 1 addition & 0 deletions modules/server/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
imports = [
./acme.nix
./immich.nix
./mealie.nix
./navidrome.nix
./nginx.nix
Expand Down
283 changes: 283 additions & 0 deletions modules/server/immich.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
# Immich - Self-hosted photos and videos.
#
# Immich is not available as a NixOS module (yet). So deploy it as an OCI
# container instead. See https://github.com/NixOS/nixpkgs/pull/244803 for
# progress on packaging it natively for NixOS.
#
# This file adapted from Diogo Correia's dotfiles:
# https://github.com/diogotcorreia/dotfiles/blob/7676201683a3785ef17eff9f4ad3295375c670bd/hosts/hera/immich.nix
#
{config, lib, pkgs, ...}:

let
cfg = config.sys.server.immich;
in {
options.sys.server.immich = {
enable = lib.mkEnableOption "Immich";

domain = lib.mkOption {
type = lib.types.str;
description = "The domain to host Immich on";
};

port = lib.mkOption {
type = lib.types.int;
description = "The port to listen on";
};

storagePath = lib.mkOption {
type = lib.types.path;
description = "The filepath at which persistent Immich files should be stored";
};

logLevel = lib.mkOption {
type = lib.types.enum [ "verbose" "debug" "log" "warn" "error" ];
default = "log";
description = "The log level to run Immich at";
};
};

config = lib.mkIf cfg.enable
(let
images = {
serverAndMicroservices = {
imageName = "ghcr.io/immich-app/immich-server";
imageDigest =
"sha256:293673607cdc62be83d4982db491544959070982bdd7bab3181f8bbed485e619"; # v1.91.0
sha256 = "sha256-lbgZ82TJzNnsjx2VHaN/KEOUK55wKeTjA0bkubnaUt8=";
};
machineLearning = {
imageName = "ghcr.io/immich-app/immich-machine-learning";
imageDigest =
"sha256:4d5614c2f372acdc779a398f9c27e10ae35f3777fd34dd3f63cf88012644438f"; # v1.91.0
sha256 = "sha256-df00J92Uils7uaoqMkxt97ALgWIjXwY+fxku0OvIiHY=";
};
};
dbUsername = user;

redisName = "immich";

user = "immich";
group = user;
uid = 15015;
gid = 15015;

immichWebUrl = "http://immich_web:3000";
immichServerUrl = "http://immich_server:3001";
immichMachineLearningUrl = "http://immich_machine_learning:3003";

# Full environment variable docs: https://immich.app/docs/install/environment-variables
environment = {
DB_URL = "socket://${dbUsername}:@/run/postgresql?db=${dbUsername}";

REDIS_SOCKET = config.services.redis.servers.${redisName}.unixSocket;

UPLOAD_LOCATION = cfg.storagePath;

IMMICH_WEB_URL = immichWebUrl;
IMMICH_SERVER_URL = immichServerUrl;
IMMICH_MACHINE_LEARNING_URL = immichMachineLearningUrl;

LOG_LEVEL = cfg.logLevel;
};

# A function to wrap a docker image as a locally built container.
# TODO: I think this is only useful for ensuring image digests are followed?
wrapImage = { name, imageName, imageDigest, sha256, entrypoint }:
pkgs.dockerTools.buildImage ({
name = name;
tag = "release";
fromImage = pkgs.dockerTools.pullImage {
imageName = imageName;
imageDigest = imageDigest;
sha256 = sha256;
};
created = "now";
config = if builtins.length entrypoint == 0 then
null
else {
Cmd = entrypoint;
WorkingDir = "/usr/src/app";
};
});

# A function to build a container volume mount string that maps to the
# same place in the container as on the host.
mkMount = dir: "${dir}:${dir}";
in {
# Create a system user that Immich can run under, allowing for peer
# authentication to the postgres database.
users.users.${user} = {
inherit group uid;
isSystemUser = true;
};
users.groups.${group} = { inherit gid; };

# Create a postgres database for Immich, and install the pgvecto-rs plugin
# which we build separately as a custom package (see pkgs/default.nix).
services.postgresql = {
ensureUsers = [{
name = dbUsername;
ensureDBOwnership = true;
# Make the "immich" user a superuser such that it can create
# postgres extensions.
ensureClauses.superuser = true;
}];
ensureDatabases = [ dbUsername ];

extraPlugins = [
pkgs.pgvecto-rs
];
settings = { shared_preload_libraries = "vectors.so"; };
};

# Create a redis server instance specifically for Immich.
services.redis.servers.${redisName} = {
inherit user;
enable = true;
};

# Ensure that the directory where photos will be stored exists.
# TODO: This appear to fail with the following error:
# fchownat() of /mnt/storagebox/media/immich failed: Permission denied
#
# systemd.tmpfiles.rules = [ "d ${cfg.storagePath} 0750 ${user} ${group}" ];

# Start the OCI containers necessary to run an Immich server.
# The containers are connected via a bridge network called "immich-bridge".
virtualisation.oci-containers.containers = {
immich_server = {
imageFile = wrapImage {
inherit (images.serverAndMicroservices) imageName imageDigest sha256;
name = "immich_server";
entrypoint = [ "/bin/sh" "start-server.sh" ];
};
image = "immich_server:release";
extraOptions =
[ "--network=immich-bridge" "--user=${toString uid}:${toString gid}" ];

volumes = [
"${cfg.storagePath}:/usr/src/app/upload"
(mkMount "/run/postgresql")
(mkMount "/run/redis-${redisName}")
];

environment = environment // {
PUID = toString uid;
PGID = toString gid;
};

ports = [ "${toString cfg.port}:3001" ];

autoStart = true;
};

immich_microservices = {
imageFile = wrapImage {
inherit (images.serverAndMicroservices) imageName imageDigest sha256;
name = "immich_microservices";
entrypoint = [ "/bin/sh" "start-microservices.sh" ];
};
image = "immich_microservices:release";
extraOptions =
[ "--network=immich-bridge" "--user=${toString uid}:${toString gid}" ];

volumes = [
"${cfg.storagePath}:/usr/src/app/upload"
(mkMount "/run/postgresql")
(mkMount "/run/redis-${redisName}")
];

environment = environment // {
PUID = toString uid;
PGID = toString gid;
REVERSE_GEOCODING_DUMP_DIRECTORY = "/tmp/reverse-geocoding-dump";
};

autoStart = true;
};

immich_machine_learning = {
imageFile = pkgs.dockerTools.pullImage images.machineLearning;
image = "ghcr.io/immich-app/immich-machine-learning";
extraOptions = [ "--network=immich-bridge" ];

environment = environment;

volumes = [ "immich-model-cache:/cache" ];

autoStart = true;
};
};

systemd.services = {
init-immich-network = {
description = "Create the network bridge for immich.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
# We hardcode the docker command here (instead of using ${pkgs.docker}/bin/docker)
# in order to allow for compatibility with podman's docker wrapper.
script = ''
# Put a true at the end to prevent getting non-zero return code, which would
# otherwise cause the service to fail.
check=$(/run/current-system/sw/bin/docker network ls | grep "immich-bridge" || true)
if [ -z "$check" ];
then /run/current-system/sw/bin/docker network create immich-bridge
else echo "immich-bridge docker network already exists"
fi
'';
};

# TODO: Regardless of creating the extensions manually, Immich still
# complains that it can't create the extensions. So... I've just given
# it superuser for now above.
#
# enable-immich-postgresql-extensions = {
# description = "Activate required Postgres extensions for Immich";
# after = [ "network.target" ];
# wantedBy = [ "multi-user.target" ];
# serviceConfig = {
# # Run this as the postgres user so that we have super-user powers.
# User = "postgres";
# Group = "postgres";

# Type = "oneshot";
# };
# script = ''
# # Create extensions if they don't already exist.
# # psql will exit with an error code if the extension already exists, so append "|| true"
# # to have the operation always succeed.
# ${config.services.postgresql.package}/bin/psql ${dbUsername} -c "CREATE EXTENSION cube" || true
# ${config.services.postgresql.package}/bin/psql ${dbUsername} -c "CREATE EXTENSION earthdistance" || true
# '';
# };
};

# Configure the reverse proxy to route to this service.
services.nginx = {
enable = true;

virtualHosts.${cfg.domain} = {
http2 = true;

# Fetch and configure a TLS cert using the ACME protocol.
enableACME = true;

# Redirect all unencrypted traffic to HTTPS.
forceSSL = true;

locations."/" = {
# Proxy all traffic straight through.
proxyPass = "http://127.0.0.1:${toString cfg.port}";
};

# Allow uploading media files up to 10 gigabytes in size.
extraConfig = ''
client_max_body_size 10G;
'';
};
};
});
}
60 changes: 60 additions & 0 deletions pkgs/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,64 @@ self: super: {
maintainers = [ super.lib.maintainers.sikmir ] ++ super.lib.teams.podman.members;
};
};

# packages/pgvecto-rs.nix
#
# Author: Diogo Correia <[email protected]>
# URL: https://github.com/diogotcorreia/dotfiles
#
# A PostgreSQL extension needed for Immich.
# This builds from the pre-compiled binary instead of from source.
pgvecto-rs = let
# The major version of PostgreSQL that the system is running.
#
# TODO: We're still running PostgreSQL 14 as our machine's state.version
# is 23.11, which implies 14.
#
# We would need to upgrade postgres manually and then set the postgres version with
# services.postgres.package. Probably in its own module?
#
# If I was clever, I could set this major version based on the rest of my
# config itself, like Diogo does.
major = "14";

# A map from PostgreSQL major version to corresponding pgvecto-rs hash.
#
# The pgvecto-rs binary we download depends both on the PostgreSQL major
# version and the version of pgvecto-rs.
versionHashes = {
"14" = "sha256-8YRC1Cd9i0BGUJwLmUoPVshdD4nN66VV3p48ziy3ZbA=";
"15" = "sha256-IVx/LgRnGyvBRYvrrJatd7yboWEoSYSJogLaH5N/wPA=";
};
in super.stdenv.mkDerivation rec {
pname = "pgvecto-rs";
version = "0.1.11";

buildInputs = [ super.dpkg ];

src = super.fetchurl {
url =
"https://github.com/tensorchord/pgvecto.rs/releases/download/v${version}/vectors-pg${major}-v${version}-x86_64-unknown-linux-gnu.deb";
hash = versionHashes."${major}";
};

dontUnpack = true;
dontBuild = true;
dontStrip = true;

installPhase = ''
mkdir -p $out
dpkg -x $src $out
install -D -t $out/lib $out/usr/lib/postgresql/${major}/lib/*.so
install -D -t $out/share/postgresql/extension $out/usr/share/postgresql/${major}/extension/*.sql
install -D -t $out/share/postgresql/extension $out/usr/share/postgresql/${major}/extension/*.control
rm -rf $out/usr
'';

meta = {
description =
"pgvecto.rs extension for PostgreSQL: Scalable Vector database plugin for Postgres, written in Rust, specifically designed for LLM";
homepage = "https://github.com/tensorchord/pgvecto.rs";
};
};
}

0 comments on commit de37bcd

Please sign in to comment.