Skip to content
2.3k

Upload service ​

Settings for the optional upload service, the immich-public-proxy-upload container, which lets visitors send photos and videos back to a share. The setup is in Let visitors send photos back.

The service reads config the same way as IPP: a config.json mounted at /app/config.json, the file named by IPP_CONFIG, or the CONFIG environment variable. Its own options are under ipp.upload.*. IPP never reads those, and the service reads none of IPP's options except the two under Shared options, so one config.json can be mounted into both containers.

Example ​

Accept files up to 2 GB, and send a notification to a Gotify server for each stored file:

json
{
  "ipp": {
    "upload": {
      "maxFileSize": 2048,
      "notifyUrl": "https://gotify.example.com/message?token=YOUR_APP_TOKEN"
    }
  }
}

Environment variables ​

IMMICH_URL, PUBLIC_BASE_URL, IPP_PORT, IPP_CONFIG and CONFIG mean the same as for IPP; see Environment variables. IMMICH_URL is required, and the environment block from the IPP container can be copied across as is.

PUBLIC_BASE_URL ​

Optional

The public URL of IPP, for example https://photos.example.com, the same value as on the IPP container. When set, the upload page has a back button to the share's gallery.

The service has no setting for its own public URL. It serves its pages both under /upload and at the root, so the reverse proxy decides where it lives: a path on the IPP hostname (photos.example.com/upload) or a hostname of its own (upload.example.com). See Route it through your reverse proxy.

Options ​

maxFileSize ​

Type: int · Default: 500

The largest single file accepted, in MB. The page refuses a larger file before sending it; the service checks the declared size on arrival and counts the bytes as they stream. The smallest value is 1.

A reverse proxy or CDN in front of the service can have a lower limit of its own. See Request size limits.

rateLimit ​

Type: int · Default: 60

Files accepted per minute from one visitor address to one share. Over the limit, the service answers with the time to wait, and the page sends the file again after it, up to three tries. A wait of more than a minute is shown as a failure with a retry button instead. 0 turns the limit off. A request the service refuses, for example for its size or type, does not count.

The address is the one that connects to the service. Behind a reverse proxy every visitor has the proxy's address, so the limit covers each share as a whole.

byteBudget ​

Type: int · Default: 10240

MB accepted per share per hour, from all visitors together. Each file's declared size is reserved when its upload starts and released if the upload fails. A file that would exceed the budget is refused until the hour ends. 0 turns the budget off.

An upload that delivers less than 8 KB/s over a minute is dropped and its reservation released, so a stalled connection cannot hold the budget. The page retries a dropped upload.

filenamePrefix ​

Type: string · Default: "ipp_upload_"

Added to the start of every stored filename: IMG_0042.jpg is stored as ipp_upload_IMG_0042.jpg. The prefix is what an Immich workflow can match on; see Tag uploaded photos. Set it to "" to store the visitor's filename unchanged.

notifyUrl ​

Type: string · Default: ""

A URL that receives one JSON POST for each stored file, sent after the visitor has been answered. Empty means no notifications. Each notification is sent once, with no retry. A failure is logged with the URL's host only, so a token in the URL never reaches the log.

title and message are at the top level, so Gotify shows them with no setup. Everything else is under data:

json
{
  "title": "Photo sent to Holiday 2025",
  "message": "ipp_upload_IMG_0042.jpg (3.0 MB) was uploaded to Holiday 2025.",
  "data": {
    "event": "upload",
    "time": "2026-10-05T07:18:00.000Z",
    "share": { "type": "album", "id": "9f1c…", "title": "Holiday 2025" },
    "file": { "name": "ipp_upload_IMG_0042.jpg", "size": 3145728, "type": "image/jpeg" },
    "asset": { "id": "4b2e…", "status": "created" }
  }
}
  • share.type is album for an album share and individual for a share of selected photos. share.id is the album ID or the shared link ID.
  • asset.status is created, or duplicate when the owner already had the same file. Immich adds a duplicate to the share too.
  • The body never contains the share key, the custom URL or a password.

ntfy reads the two fields through its inline templates:

https://ntfy.sh/your-topic?tpl=yes&t={{.title}}&m={{.message}}

notifyHeaders ​

Type: object · Default: {}

Extra headers sent with each notification, for example an Authorization header. Values must be strings.

json
{
  "ipp": {
    "upload": {
      "notifyUrl": "https://hooks.example.com/ipp",
      "notifyHeaders": { "Authorization": "Bearer YOUR_TOKEN" }
    }
  }
}

notifyTimeout ​

Type: int · Default: 10000

How long to wait for the notification URL to answer, in milliseconds.

Shared options ​

The service also reads two IPP options, with the same meaning:

  • allowSlugLinks. When false, the upload page is not served at /s/<slug> either.
  • customInvalidResponse. The status code, redirect, null and false forms all apply. There is no invalidRequestHandler.js to replace in the upload image.

responseHeaders is not read. Every response from the service other than its static assets carries Cache-Control: no-store, and none carries a CORS header.

Refused uploads ​

A request for an unknown or expired share, or for a share whose "Allow public user to upload" option is off, gets the same error response as it would from IPP. Once a share accepts uploads, a refused file gets one of these codes and a short reason, which the page shows to the visitor:

CodeReason
400The file is empty, the filename has no extension, or Immich refused the file, for example because the owner's storage quota is full.
403The share no longer accepts uploads.
411The request has no Content-Length header.
413The file is larger than maxFileSize, sent more bytes than its Content-Length declared, or the share has used its byteBudget for the hour.
415The file is not a photo or a video. SVG files are refused, by media type and by extension.
429The visitor is over rateLimit. Retry-After gives the wait in seconds.
502Immich could not be reached, or failed.

Released under the AGPL-3.0 licence.