job "immich" { datacenters = ["dc1"] region = "global" group "immich" { network { mode = "bridge" } volume "data" { source = "immich-data" type = "csi" access_mode = "multi-node-multi-writer" attachment_mode = "file-system" } service { name = "immich" port = 3001 connect { sidecar_service { proxy { upstreams { destination_name = "immich-ml" local_bind_port = 3003 # Work arround, see https://github.com/hashicorp/nomad/issues/18538 destination_type = "service" config { protocol = "http" } } upstreams { destination_name = "postgres" local_bind_port = 5432 # Work arround, see https://github.com/hashicorp/nomad/issues/18538 destination_type = "service" } } } sidecar_task { config { args = [ "-c", "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json", "-l", "${meta.connect.log_level}", "--concurrency", "${meta.connect.proxy_concurrency}", "--disable-hot-restart" ] } resources { cpu = 50 memory = 64 } } } check { type = "http" path = "/api/server-info/ping" expose = true interval = "30s" timeout = "15s" check_restart { limit = 10 grace = "300s" } } tags = [ "traefik.enable=true", "traefik.http.routers.immich.entrypoints=https", "traefik.http.routers.immich.rule=Host(`immich.example.org`)", "traefik.http.middlewares.csp-immich.headers.contentsecuritypolicy=connect-src 'self' https://maputnik.github.io https://*.cofractal.com https://fonts.openmaptiles.org;default-src 'self';font-src 'self' data:;img-src 'self' data: blob:;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';", "traefik.http.routers.immich.middlewares=security-headers@file,forward-proto@file,inflight-std@file,hsts@file,compression@file,csp-immich", "traefik.enable=true", "traefik.http.routers.immich-share.entrypoints=https", "traefik.http.routers.immich-share.rule=Host(`immich.example.org`) && PathRegexp(`^/(share/|_app/immutable/|custom\\.css|api/(asset|server-info)/.*)`)", "traefik.http.middlewares.csp-immich-share.headers.contentsecuritypolicy=connect-src 'self' https://maputnik.github.io https://*.cofractal.com https://fonts.openmaptiles.org;default-src 'self';font-src 'self' data:;img-src 'self' data: blob:;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';", "traefik.http.routers.immich-share.middlewares=security-headers@file,forward-proto@file,inflight-std@file,hsts@file,compression@file,csp-immich-share", ] } # wait for required services tp be ready before starting the main task task "wait-for" { driver = "docker" user = 1053 config { image = "danielberteaud/wait-for:24.3-1" readonly_rootfs = true pids_limit = 20 } lifecycle { hook = "prestart" } env { SERVICE_0 = "master.postgres.service.consul" SERVICE_1 = "immich-ml.service.consul" } resources { cpu = 10 memory = 10 memory_max = 30 } } # The main immich API server task "server" { driver = "docker" leader = true # Run as an unprivileged user user = 3001 config { image = "ghcr.io/immich-app/immich-server:v1.100.0" readonly_rootfs = true command = "start.sh" args = ["immich"] pids_limit = 100 } vault { policies = ["immich"] env = false disable_file = true change_mode = "noop" } env { REDIS_HOSTNAME = "127.0.0.1" IMMICH_MEDIA_LOCATION = "/data" } # Use a template block instead of env {} so we can fetch values from vault template { data = <<_EOT LANG=fr_FR.utf8 NODE_OPTIONS=--max-old-space-size={{ env "NOMAD_MEMORY_LIMIT" }} TZ=Europe/Paris _EOT destination = "secrets/.env" perms = 400 env = true } template { data = <<_EOT DB_URL=postgres://{{ with secret "database/creds/immich" }}{{ .Data.username }}{{ end }}:{{ with secret "database/creds/immich" }}{{ .Data.password }}{{ end }}@127.0.0.1:5432/immich _EOT destination = "secrets/.db.env" perms = 400 env = true } volume_mount { volume = "data" destination = "/data" } resources { cpu = 300 memory = 320 memory_max = 512 } } # microservices is tha task worker, doing all the processing async task "microservices" { driver = "docker" # Run as an unpriviliged user user = 3001 config { image = "ghcr.io/immich-app/immich-server:v1.100.0" readonly_rootfs = true command = "start.sh" args = ["microservices"] pids_limit = 100 } vault { policies = ["immich"] env = false disable_file = true change_mode = "noop" } env { REDIS_HOSTNAME = "127.0.0.1" IMMICH_MEDIA_LOCATION = "/data" } # Use a template block instead of env {} so we can fetch values from vault template { data = <<_EOT LANG=fr_FR.utf8 NODE_OPTIONS=--max-old-space-size={{ env "NOMAD_MEMORY_LIMIT" }} TZ=Europe/Paris _EOT destination = "secrets/.env" perms = 400 env = true } template { data = <<_EOT DB_URL=postgres://{{ with secret "database/creds/immich" }}{{ .Data.username }}{{ end }}:{{ with secret "database/creds/immich" }}{{ .Data.password }}{{ end }}@127.0.0.1:5432/immich _EOT destination = "secrets/.db.env" perms = 400 env = true } volume_mount { volume = "data" destination = "/data" } resources { cpu = 500 memory = 768 memory_max = 1200 } } # Local redis instance task "redis" { driver = "docker" user = 6379 lifecycle { hook = "prestart" sidecar = true } config { image = "redis:alpine" readonly_rootfs = true force_pull = true args = ["/local/redis.conf"] } template { data = <<_EOT bind 127.0.0.1 maxmemory {{ env "NOMAD_MEMORY_LIMIT" | parseInt | subtract 5 }}mb databases 1 save "" appendonly no _EOT destination = "local/redis.conf" } resources { cpu = 20 memory = 64 } } } # Used for face recognition, tags etc. group "machine-learning" { network { mode = "bridge" } volume "ml" { source = "immich-ml" type = "csi" access_mode = "single-node-writer" attachment_mode = "file-system" } service { name = "immich-ml" port = 3003 connect { sidecar_service { } sidecar_task { config { args = [ "-c", "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json", "-l", "${meta.connect.log_level}", "--concurrency", "${meta.connect.proxy_concurrency}", "--disable-hot-restart" ] } resources { cpu = 50 memory = 64 } } } } task "machine-learning" { driver = "docker" user = 3001 config { image = "ghcr.io/immich-app/immich-machine-learning:v1.100.0" readonly_rootfs = true pids_limit = 200 } env { TMPDIR = "/local" MPLCONFIGDIR = "/local" MACHINE_LEARNING_HOST = "127.0.0.1" } # Use a template block instead of env {} so we can fetch values from vault template { data = <<_EOT LANG=fr_FR.utf8 TZ=Europe/Paris _EOT destination = "secrets/.env" perms = 400 env = true } volume_mount { volume = "ml" destination = "/cache" } resources { cpu = 1024 memory = 1024 memory_max = 1536 } } } }