Skip to content

Commit 6278071

Browse files
committed
nixos/immich-public-proxy: init module
1 parent 6a64387 commit 6278071

File tree

6 files changed

+211
-0
lines changed

6 files changed

+211
-0
lines changed

nixos/doc/manual/release-notes/rl-2505.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777

7878
- [Actual Budget](https://actualbudget.org/), a local-first personal finance app. Available as [services.actual](#opt-services.actual.enable).
7979

80+
- [immich-public-proxy](https://github.com/alangrainger/immich-public-proxy), a proxy for sharing Immich albums without exposing the Immich API. Available as [services.immich-public-proxy](#opt-services.immich-public-proxy.enable).
81+
8082
- [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable).
8183

8284
- [nvidia-gpu](https://github.com/utkuozdemir/nvidia_gpu_exporter), a Prometheus exporter that scrapes `nvidia-smi` for GPU metrics. Available as [services.prometheus.exporters.nvidia-gpu](#opt-services.prometheus.exporters.nvidia-gpu.enable).

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,7 @@
14841484
./services/web-apps/icingaweb2/module-monitoring.nix
14851485
./services/web-apps/ifm.nix
14861486
./services/web-apps/immich.nix
1487+
./services/web-apps/immich-public-proxy.nix
14871488
./services/web-apps/invidious.nix
14881489
./services/web-apps/invoiceplane.nix
14891490
./services/web-apps/isso.nix
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
let
8+
cfg = config.services.immich-public-proxy;
9+
format = pkgs.formats.json { };
10+
inherit (lib)
11+
types
12+
mkIf
13+
mkOption
14+
mkEnableOption
15+
;
16+
in
17+
{
18+
options.services.immich-public-proxy = {
19+
enable = mkEnableOption "Immich Public Proxy";
20+
package = lib.mkPackageOption pkgs "immich-public-proxy" { };
21+
22+
immichUrl = mkOption {
23+
type = types.str;
24+
description = "URL of the Immich instance";
25+
};
26+
27+
port = mkOption {
28+
type = types.port;
29+
default = 3000;
30+
description = "The port that IPP will listen on.";
31+
};
32+
openFirewall = mkOption {
33+
type = types.bool;
34+
default = false;
35+
description = "Whether to open the IPP port in the firewall";
36+
};
37+
38+
settings = mkOption {
39+
type = types.submodule {
40+
freeformType = format.type;
41+
};
42+
default = { };
43+
description = ''
44+
Configuration for IPP. See <https://github.com/alangrainger/immich-public-proxy/blob/main/README.md#additional-configuration> for options and defaults.
45+
'';
46+
};
47+
};
48+
49+
config = mkIf cfg.enable {
50+
systemd.services.immich-public-proxy = {
51+
description = "Immich public proxy for sharing albums publicly without exposing your Immich instance";
52+
after = [ "network.target" ];
53+
wantedBy = [ "multi-user.target" ];
54+
environment = {
55+
IMMICH_URL = cfg.immichUrl;
56+
IPP_PORT = builtins.toString cfg.port;
57+
IPP_CONFIG = "${format.generate "config.json" cfg.settings}";
58+
};
59+
serviceConfig = {
60+
ExecStart = lib.getExe cfg.package;
61+
SyslogIdentifier = "ipp";
62+
User = "ipp";
63+
Group = "ipp";
64+
DynamicUser = true;
65+
Type = "simple";
66+
Restart = "on-failure";
67+
RestartSec = 3;
68+
69+
# Hardening
70+
CapabilityBoundingSet = "";
71+
NoNewPrivileges = true;
72+
PrivateUsers = true;
73+
PrivateTmp = true;
74+
PrivateDevices = true;
75+
PrivateMounts = true;
76+
ProtectClock = true;
77+
ProtectControlGroups = true;
78+
ProtectHome = true;
79+
ProtectHostname = true;
80+
ProtectKernelLogs = true;
81+
ProtectKernelModules = true;
82+
ProtectKernelTunables = true;
83+
RestrictAddressFamilies = [
84+
"AF_INET"
85+
"AF_INET6"
86+
"AF_UNIX"
87+
];
88+
RestrictNamespaces = true;
89+
RestrictRealtime = true;
90+
RestrictSUIDSGID = true;
91+
};
92+
};
93+
94+
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
95+
96+
meta.maintainers = with lib.maintainers; [ jaculabilis ];
97+
};
98+
}

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ in {
467467
ifm = handleTest ./ifm.nix {};
468468
iftop = handleTest ./iftop.nix {};
469469
immich = handleTest ./web-apps/immich.nix {};
470+
immich-public-proxy = handleTest ./web-apps/immich-public-proxy.nix {};
470471
incron = handleTest ./incron.nix {};
471472
incus = pkgs.recurseIntoAttrs (handleTest ./incus { lts = false; inherit system pkgs; });
472473
incus-lts = pkgs.recurseIntoAttrs (handleTest ./incus { inherit system pkgs; });
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import ../make-test-python.nix (
2+
{ pkgs, lib, ... }:
3+
{
4+
name = "immich-public-proxy";
5+
6+
nodes.machine =
7+
{ pkgs, ... }@args:
8+
{
9+
environment.systemPackages = [
10+
pkgs.imagemagick
11+
pkgs.immich-cli
12+
];
13+
services.immich = {
14+
enable = true;
15+
port = 2283;
16+
# disable a lot of features that aren't needed for this test
17+
machine-learning.enable = false;
18+
settings = {
19+
backup.database.enabled = false;
20+
machineLearning.enabled = false;
21+
map.enabled = false;
22+
reverseGeocoding.enabled = false;
23+
metadata.faces.import = false;
24+
newVersionCheck.enabled = false;
25+
notifications.smtp.enabled = false;
26+
};
27+
};
28+
services.immich-public-proxy = {
29+
enable = true;
30+
immichUrl = "http://localhost:2283";
31+
port = 8002;
32+
settings.ipp.responseHeaders."X-NixOS" = "Rules";
33+
};
34+
};
35+
36+
testScript = ''
37+
import json
38+
39+
machine.wait_for_unit("immich-server.service")
40+
machine.wait_for_unit("immich-public-proxy.service")
41+
machine.wait_for_open_port(2283)
42+
machine.wait_for_open_port(8002)
43+
44+
# The proxy should be up
45+
machine.succeed("curl -sf http://localhost:8002")
46+
47+
# Verify the static assets are served
48+
machine.succeed("curl -sf http://localhost:8002/robots.txt")
49+
machine.succeed("curl -sf http://localhost:8002/share/static/style.css")
50+
51+
# Check that the response header in the settings is sent
52+
res = machine.succeed("""
53+
curl -sD - http://localhost:8002 -o /dev/null
54+
""")
55+
assert "x-nixos: rules" in res.lower(), res
56+
57+
# Log in to Immich and create an access key
58+
machine.succeed("""
59+
curl -sf --json '{ "email": "[email protected]", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up
60+
""")
61+
res = machine.succeed("""
62+
curl -sf --json '{ "email": "[email protected]", "password": "admin" }' http://localhost:2283/api/auth/login
63+
""")
64+
token = json.loads(res)['accessToken']
65+
res = machine.succeed("""
66+
curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys
67+
""" % token)
68+
key = json.loads(res)['secret']
69+
machine.succeed(f"immich login http://localhost:2283/api {key}")
70+
res = machine.succeed("immich server-info")
71+
print(res)
72+
73+
# Upload some blank images to a new album
74+
# If there's only one image, the proxy serves the image directly
75+
machine.succeed("magick -size 800x600 canvas:white /tmp/white.png")
76+
machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/white.png")
77+
machine.succeed("magick -size 800x600 canvas:black /tmp/black.png")
78+
machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/black.png")
79+
res = machine.succeed("immich server-info")
80+
print(res)
81+
82+
# Get the new album id
83+
res = machine.succeed("""
84+
curl -sf -H 'Cookie: immich_access_token=%s' http://localhost:2283/api/albums
85+
""" % token)
86+
album_id = json.loads(res)[0]['id']
87+
88+
# Create a shared link
89+
res = machine.succeed("""
90+
curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "albumId": "%s", "type": "ALBUM" }' http://localhost:2283/api/shared-links
91+
""" % (token, album_id))
92+
share_key = json.loads(res)['key']
93+
94+
# Access the share
95+
machine.succeed("""
96+
curl -sf http://localhost:2283/share/%s
97+
""" % share_key)
98+
99+
# Access the share through the proxy
100+
machine.succeed("""
101+
curl -sf http://localhost:8002/share/%s
102+
""" % share_key)
103+
'';
104+
}
105+
)

pkgs/by-name/im/immich-public-proxy/package.nix

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
buildNpmPackage,
44
fetchFromGitHub,
55
nix-update-script,
6+
nixosTests,
67
nodejs,
78
}:
89
buildNpmPackage rec {
@@ -32,6 +33,9 @@ buildNpmPackage rec {
3233
'';
3334

3435
passthru = {
36+
tests = {
37+
inherit (nixosTests) immich-public-proxy;
38+
};
3539
updateScript = nix-update-script { };
3640
};
3741

0 commit comments

Comments
 (0)