Deployment
The Apprise API is designed to run as a containerized service.
This page explains how to deploy and operate it, not how to use the API endpoints.
If you are looking to:
- send notifications, see API Usage
- integrate with the CLI or Python library, see Integration
- understand keys, storage, or locking, see Configuration
Quick Start
Section titled “Quick Start”Choose one deployment method below to get Apprise API running quickly.
The self-hosted examples publish port 8000 normally so trusted devices on
your home or internal network can connect without extra proxy configuration.
They use normal mode. Do not expose them directly to the Internet. Use the
Deployment Behind a Proxy tab for a public server with HTTPS,
authentication, strict routing, and a loopback-only container port.
docker run --name apprise \ -p 8000:8000 \ -v ./config:/config \ -v ./attach:/attach \ -e APPRISE_STATEFUL_MODE=simple \ -e APPRISE_WORKER_COUNT=1 \ -e APPRISE_DEFAULT_FORMAT=text \ -d caronc/apprise:latestThe following will set the same container up but leverage health check monitoring:
docker run --name apprise \ -p 8000:8000 \ -v ./config:/config \ -v ./attach:/attach \ -e APPRISE_STATEFUL_MODE=simple \ -e APPRISE_WORKER_COUNT=1 \ -e APPRISE_DEFAULT_FORMAT=text \ --health-cmd='curl -fsS http://127.0.0.1:8000/status >/dev/null || exit 1' \ --health-interval=30s \ --health-timeout=5s \ --health-retries=3 \ --health-start-period=20s \ -d caronc/apprise:latestTo try the built-in administrator login locally, stop the earlier quick-start
container first; both examples use port 8000. This one keeps the
configuration list available to admin:
docker run --name apprise-auth \ -p 127.0.0.1:8000:8000 \ -v ./config:/config \ -v ./attach:/attach \ -e APPRISE_STATEFUL_MODE=simple \ -e APPRISE_AUTH_REQUIRED=yes \ -e APPRISE_USER=admin \ -e APPRISE_PASSWORD=changeme \ -e APPRISE_WEB_AUTH_SECRET="$(openssl rand -hex 32)" \ -e APPRISE_WORKER_COUNT=1 \ -e APPRISE_DEFAULT_FORMAT=text \ -d caronc/apprise:latestOpen http://127.0.0.1:8000/cfg and sign in as admin with password
changeme. The list works because stateful storage is in simple mode; the
configuration list is shown by default.
services: apprise: image: caronc/apprise:latest container_name: apprise ports: - "8000:8000" environment: APPRISE_STATEFUL_MODE: simple APPRISE_WORKER_COUNT: 1 APPRISE_DEFAULT_FORMAT: "text" volumes: - ./config:/config - ./attach:/attachThe following will set the same container up but leverage health check monitoring:
services: apprise: image: caronc/apprise:latest container_name: apprise ports: - "8000:8000" environment: APPRISE_STATEFUL_MODE: simple APPRISE_WORKER_COUNT: 1 APPRISE_DEFAULT_FORMAT: "text" volumes: - ./config:/config - ./attach:/attach healthcheck: test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:8000/status >/dev/null || exit 1", ] interval: 30s timeout: 5s retries: 3 start_period: 20sFor the same local administrator setup with Compose, save the YAML below as
compose.yaml. Put the example password and a lasting browser-login secret in
a protected .env file:
printf 'APPRISE_PASSWORD=changeme\nAPPRISE_WEB_AUTH_SECRET=%s\n' \ "$(openssl rand -hex 32)" > .envchmod 600 .envservices: apprise: image: caronc/apprise:latest ports: - "127.0.0.1:8000:8000" env_file: - .env environment: APPRISE_STATEFUL_MODE: simple APPRISE_AUTH_REQUIRED: "yes" APPRISE_USER: admin APPRISE_WORKER_COUNT: 1 APPRISE_DEFAULT_FORMAT: "text" volumes: - ./config:/config - ./attach:/attachThe env_file entry loads the two secrets from .env beside compose.yaml.
Stop any earlier container using port 8000, then run docker compose up -d.
Open http://127.0.0.1:8000/cfg and sign in as admin with password
changeme. Keep .env when redeploying; changing its browser-login secret
signs out existing browser sessions.
This pattern keeps the container reachable only from the Docker host. A separate Nginx instance provides public HTTPS, logging, and the security policy.
Prepare the Container Files
Section titled “Prepare the Container Files”Create persistent directories, an environment file, and an empty host file for the Nginx server override. Replace the example password before starting the container. The override is populated after Docker reports the running container’s gateway.
mkdir -p apprise/config apprise/attachchmod 700 apprise
web_secret="$(openssl rand -hex 32)"cat > apprise/apprise.env <<EOFSTRICT_MODE=yesAPPRISE_STATEFUL_MODE=simpleAPPRISE_WORKER_COUNT=1APPRISE_AUTH_REQUIRED=yesAPPRISE_WEB_AUTH_SECRET=${web_secret}APPRISE_USER=adminAPPRISE_PASSWORD=replace-with-a-strong-passwordAPPRISE_TRUSTED_ORIGINS=https://apprise.example.comALLOWED_HOSTS=apprise.example.comTZ=Etc/UTCEOFchmod 600 apprise/apprise.envtouch apprise/nginx-server-override.confchmod 644 apprise/nginx-server-override.confThe empty host file must exist before docker run because the command below
bind-mounts it over the container’s empty default. Its contents are added after
the container starts.
STRICT_MODE=yes keeps unknown routes at 404, reports unsupported methods as
405, and rate-limits authentication traffic with 429 and Retry-After: 60.
The container owns these strict controls. The public Nginx server below remains
a small HTTPS relay and records the resulting responses for tools such as
Fail2Ban.
Start the Loopback-Only Container
Section titled “Start the Loopback-Only Container”docker run -d \ --name apprise \ --publish 127.0.0.1:8010:8000 \ --user "$(id -u):$(id -g)" \ --env-file "$PWD/apprise/apprise.env" \ --volume "$PWD/apprise/config:/config" \ --volume "$PWD/apprise/attach:/attach" \ --volume "$PWD/apprise/nginx-server-override.conf:/etc/nginx/server-override.conf:ro" \ --health-cmd='curl -fsS -u "$APPRISE_USER:$APPRISE_PASSWORD" http://127.0.0.1:8000/status >/dev/null || exit 1' \ --health-interval=30s \ --health-timeout=5s \ --health-retries=3 \ --health-start-period=20s \ --restart unless-stopped \ caronc/apprise:latestBinding the published port to 127.0.0.1 prevents direct network access to the
container. Only services running on the Docker host, such as the public Nginx
proxy below, can reach port 8010.
Configure the Trusted Docker Gateway
Section titled “Configure the Trusted Docker Gateway”Now inspect this container once, write the reported gateway into the mounted override, and restart Apprise so Nginx loads it:
docker_gateway="$(docker inspect \ --format '{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}' \ apprise)"
if [ -z "$docker_gateway" ]; then echo "Could not determine the Apprise Docker gateway." exit 1fi
cat > apprise/nginx-server-override.conf <<EOF# Obtained from the running Apprise container with docker inspect.set_real_ip_from ${docker_gateway};
real_ip_header X-Forwarded-For;real_ip_recursive on;EOF
docker restart appriseThis works with Docker’s default bridge and custom networks because the gateway comes from the running container. Repeat this step after changing its Docker network. Do not trust an entire Docker or private network unless every device on it is trusted. Apprise uses the resulting client address for auditing and strict-mode rate limits.
Configure the Public Nginx Proxy
Section titled “Configure the Public Nginx Proxy”The map directive belongs in Nginx’s http context. Files under
/etc/nginx/conf.d/ are normally loaded there. It hides routine successful
health checks while retaining failures and rate-limit responses.
map "$uri:$status" $apprise_access_loggable { default 1; ~^/(?:status(?:/.*)?|metrics|favicon\.ico|robots\.txt)/?:[23] 0;}
server { listen 80; server_name apprise.example.com;
return 301 https://apprise.example.com$request_uri;}
server { listen 443 ssl http2; server_name apprise.example.com;
ssl_certificate /etc/letsencrypt/live/apprise.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/apprise.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/apprise_access.log combined if=$apprise_access_loggable; error_log /var/log/nginx/apprise_error.log warn;
# The public proxy controls the effective upload limit. client_max_body_size 100m;
location / { proxy_pass http://127.0.0.1:8010; proxy_http_version 1.1; proxy_set_header Connection "";
proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Real-IP $remote_addr;
# Overwrite any client-supplied forwarding chain at the public edge. proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Expect $http_expect;
proxy_connect_timeout 5s; proxy_send_timeout 310s; proxy_read_timeout 310s;
# Allow streamed uploads and event responses to pass through directly. proxy_request_buffering off; proxy_buffering off; proxy_cache off;
# Preserve authentication, rate-limit, and application error responses. proxy_intercept_errors off; proxy_redirect off; }}Use your existing certificate automation and edge-security policy in place of the example certificate paths. Enable HSTS only after confirming the domain and any subdomains will remain HTTPS-only.
Verify the Proxy Chain
Section titled “Verify the Proxy Chain”nginx -tsystemctl reload nginx
curl --user admin https://apprise.example.com/statusdocker inspect --format '{{json .State.Health}}' appriseThe outer proxy must overwrite X-Forwarded-For, and the container must trust
only that proxy’s Docker gateway. Together, these settings preserve the public
client address without allowing callers to forge it.
apiVersion: apps/v1kind: Deploymentmetadata: name: apprisespec: replicas: 1 selector: matchLabels: app: apprise template: metadata: labels: app: apprise spec: containers: - name: apprise image: caronc/apprise:latest ports: - containerPort: 8000 env: - name: APPRISE_STATEFUL_MODE value: simple - name: APPRISE_WORKER_COUNT value: 1 - name: APPRISE_DEFAULT_FORMAT value: "text"The following example expands on the above and offers a liveness and readiness probe:
apiVersion: apps/v1kind: Deploymentmetadata: name: apprisespec: replicas: 1 selector: matchLabels: app: apprise template: metadata: labels: app: apprise spec: containers: - name: apprise image: caronc/apprise:latest ports: - containerPort: 8000 env: - name: APPRISE_STATEFUL_MODE value: simple - name: APPRISE_WORKER_COUNT value: 1 - name: APPRISE_DEFAULT_FORMAT value: "text"
# Ready only when /status returns 200 readinessProbe: httpGet: path: /status port: 8000 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3
# Restart the container if /status stops returning 200 livenessProbe: httpGet: path: /status port: 8000 initialDelaySeconds: 20 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3Here is a more complete Kubernetes example. Note that this uses the legacy PGID and PUID environment variables:
apiVersion: v1kind: Namespacemetadata: labels: name: apprise name: apprise---apiVersion: v1kind: ConfigMapmetadata: labels: name: apprise name: apprise-api-override-conf-config namespace: apprisedata: location-override.conf: | auth_basic "Apprise API Restricted Area"; auth_basic_user_file /etc/nginx/.htpasswd;---apiVersion: v1kind: Secretmetadata: labels: name: apprise name: apprise-api-htpasswd-secret namespace: apprisedata: .htpasswd: <base64_encoded> # add output of: htpasswd -c apprise_api.htpasswd <USERNAME> && cat apprise_api.htpasswd | base64---apiVersion: v1kind: PersistentVolumeClaimmetadata: labels: name: apprise name: apprise-data namespace: apprisespec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi---apiVersion: v1kind: Servicemetadata: labels: name: apprise name: apprise namespace: apprisespec: ports: - name: http port: 80 protocol: TCP targetPort: 8000 selector: name: apprise type: ClusterIP---apiVersion: apps/v1kind: Deploymentmetadata: labels: name: apprise name: apprise namespace: apprisespec: replicas: 1 selector: matchLabels: name: apprise strategy: type: Recreate template: metadata: labels: name: apprise spec: containers: - env: - name: APPRISE_STATEFUL_MODE value: simple - name: PGID value: "1000" - name: PUID value: "1000" image: caronc/apprise:latest name: apprise ports: - containerPort: 8000 protocol: TCP resources: limits: cpu: "500m" memory: "512Mi" requests: cpu: "250m" memory: "128Mi" volumeMounts: - mountPath: /config name: apprise-data - mountPath: /plugin name: apprise-data - mountPath: /attach name: apprise-data # the following mountPath can be removed if not wanted/used - mountPath: /etc/nginx/.htpasswd name: apprise-api-htpasswd-secret-volume readOnly: true subPath: .htpasswd # the following mountPath can be removed if not wanted/used - mountPath: /etc/nginx/location-override.conf name: apprise-api-override-conf-config-volume readOnly: true subPath: location-override.conf restartPolicy: Always volumes: - name: apprise-data persistentVolumeClaim: claimName: apprise-data # the following volume can be removed if not wanted/used - name: apprise-api-htpasswd-secret-volume secret: secretName: apprise-api-htpasswd-secret # the following volume can be removed if not wanted/used - name: apprise-api-override-conf-config-volume configMap: name: apprise-api-override-conf-configCredit goes to: @steled for the above. The same setup can be further expanded offering a liveness and readiness probe by just replacing the last section with the following instead:
apiVersion: apps/v1kind: Deploymentmetadata: labels: name: apprise name: apprise namespace: apprisespec: replicas: 1 selector: matchLabels: name: apprise strategy: type: Recreate template: metadata: labels: name: apprise spec: containers: - env: - name: APPRISE_STATEFUL_MODE value: simple - name: PGID value: "1000" - name: PUID value: "1000" image: caronc/apprise:latest name: apprise ports: - containerPort: 8000 protocol: TCP
readinessProbe: httpGet: path: /status port: 8000 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3
livenessProbe: httpGet: path: /status port: 8000 initialDelaySeconds: 20 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3
resources: limits: cpu: "500m" memory: "512Mi" requests: cpu: "250m" memory: "128Mi" volumeMounts: - mountPath: /config name: apprise-data - mountPath: /plugin name: apprise-data - mountPath: /attach name: apprise-data - mountPath: /etc/nginx/.htpasswd name: apprise-api-htpasswd-secret-volume readOnly: true subPath: .htpasswd - mountPath: /etc/nginx/location-override.conf name: apprise-api-override-conf-config-volume readOnly: true subPath: location-override.conf restartPolicy: Always volumes: - name: apprise-data persistentVolumeClaim: claimName: apprise-data - name: apprise-api-htpasswd-secret-volume secret: secretName: apprise-api-htpasswd-secret - name: apprise-api-override-conf-config-volume configMap: name: apprise-api-override-conf-configFor a more security conscious setup, you can run Apprise API as a non-root user with a read only root filesystem and explicit ephemeral volumes.
The following example assumes you already created persistent volume claims for /config, /plugin, and /attach.
apiVersion: apps/v1kind: Deploymentmetadata: labels: app: apprise name: apprise namespace: apprisespec: replicas: 1 selector: matchLabels: app: apprise template: metadata: labels: app: apprise spec: securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 readOnlyRootFilesystem: true containers: - name: apprise image: caronc/apprise:latest imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] env: - name: APPRISE_STATEFUL_MODE value: simple - name: APPRISE_WORKER_COUNT value: "1" - name: APPRISE_DEFAULT_FORMAT value: "text" ports: - containerPort: 8000 name: http volumeMounts: # Persistent data - name: config mountPath: /config - name: plugin mountPath: /plugin - name: attach mountPath: /attach
# Ephemeral runtime and temp files - name: tmp mountPath: /tmp
volumes: - name: config persistentVolumeClaim: claimName: apprise-config - name: plugin persistentVolumeClaim: claimName: apprise-plugin - name: attach persistentVolumeClaim: claimName: apprise-attach
# The deployment mounts /tmp as an in-memory emptyDir, which is where nginx, # gunicorn, and supervisord store pids, sockets, and temporary files. # This is configured as an ephemeral volume, stored in memory. - name: tmp emptyDir: medium: MemoryThe same setup can be further expanded offering a liveness and readiness probe by just replacing the section with the following instead:
apiVersion: apps/v1kind: Deploymentmetadata: labels: app: apprise name: apprise namespace: apprisespec: replicas: 1 selector: matchLabels: app: apprise template: metadata: labels: app: apprise spec: securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 readOnlyRootFilesystem: true containers: - name: apprise image: caronc/apprise:latest imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] env: - name: APPRISE_STATEFUL_MODE value: simple - name: APPRISE_WORKER_COUNT value: "1" - name: APPRISE_DEFAULT_FORMAT value: "text" ports: - containerPort: 8000 name: http
# Health checks: /status must return HTTP 200 readinessProbe: httpGet: path: /status port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3
livenessProbe: httpGet: path: /status port: http initialDelaySeconds: 20 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3
volumeMounts: # Persistent data - name: config mountPath: /config - name: plugin mountPath: /plugin - name: attach mountPath: /attach
# Ephemeral runtime and temp files - name: tmp mountPath: /tmp
volumes: - name: config persistentVolumeClaim: claimName: apprise-config - name: plugin persistentVolumeClaim: claimName: apprise-plugin - name: attach persistentVolumeClaim: claimName: apprise-attach
# The deployment mounts /tmp as an in-memory emptyDir, which is where nginx, # gunicorn, and supervisord store pids, sockets, and temporary files. # This is configured as an ephemeral volume, stored in memory. - name: tmp emptyDir: medium: MemoryUse these instructions to run Apprise API directly on a host, without Docker. Each installation uses a Python virtual environment, the operating system’s service manager, and Nginx. The examples keep Gunicorn on the loopback address and expose Nginx over plain HTTP for a local or trusted network. For an Internet-facing installation, add HTTPS and authentication as described in Deployment Behind a Proxy.
Install the required packages:
sudo apt updatesudo apt install -y python3 python3-pip python3-venv git nginxClone Apprise API and install its Python dependencies:
cd /optsudo git clone https://github.com/caronc/apprise-api.gitsudo python3 -m venv /opt/apprise-api/venvsudo /opt/apprise-api/venv/bin/pip install --upgrade pipsudo /opt/apprise-api/venv/bin/pip install -r /opt/apprise-api/requirements.txtsudo install -d -o www-data -g www-data \ /var/lib/apprise/config /var/lib/apprise/attachCreate the systemd unit at /etc/systemd/system/apprise.service:
[Unit]Description=Apprise API ServiceAfter=network.target
[Service]User=www-dataGroup=www-dataWorkingDirectory=/opt/apprise-apiEnvironment=APPRISE_CONFIG_DIR=/var/lib/apprise/configEnvironment=APPRISE_ATTACH_DIR=/var/lib/apprise/attachEnvironment=APPRISE_STATEFUL_MODE=simpleEnvironment=APPRISE_WORKER_COUNT=1Environment=APPRISE_DEFAULT_FORMAT=textEnvironment=TZ=Etc/UTCExecStart=/opt/apprise-api/venv/bin/gunicorn \ --no-control-socket \ --workers 1 \ --worker-class gevent \ --timeout 300 \ --bind 127.0.0.1:8000 \ --chdir /opt/apprise-api/apprise_api \ core.wsgi:applicationRestart=on-failure
[Install]WantedBy=multi-user.targetEnable and start it:
sudo systemctl daemon-reloadsudo systemctl enable --now appriseAPPRISE_STATEFUL_MODE=simple stores each configuration as a readable file
and lets the configuration manager list your saved keys. One Gunicorn worker
keeps resource use modest for a home or small-team installation.
Create /etc/nginx/sites-available/appriseapi to serve static files and
proxy everything else to Gunicorn:
server { listen 80; server_name _;
client_max_body_size 500M;
location /s/ { alias /opt/apprise-api/apprise_api/static/; }
location / { proxy_pass http://127.0.0.1:8000; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $http_host; proxy_read_timeout 300s; proxy_send_timeout 300s; }}Enable the site and reload NGINX:
sudo ln -s /etc/nginx/sites-available/appriseapi /etc/nginx/sites-enabled/sudo rm -f /etc/nginx/sites-enabled/defaultsudo nginx -tsudo systemctl enable --now nginxVisit http://your-server-ip/ (or http://127.0.0.1/ if you’re testing
locally). Apprise API should load.
Logs and files
- Application files:
/opt/apprise-api - Saved configurations:
/var/lib/apprise/config - Temporary attachments:
/var/lib/apprise/attach - Gunicorn logs:
sudo journalctl -u apprise - NGINX logs:
/var/log/nginx/
Managing the service
sudo systemctl status apprisesudo systemctl restart apprisesudo systemctl stop appriseThe same steps work on Red Hat Enterprise Linux 10, Rocky Linux 10, and Oracle Linux 10.
Install the required packages and start NGINX:
sudo dnf install -y python3 python3-pip git nginxsudo systemctl enable --now nginxIf firewalld is active, allow HTTP traffic:
sudo firewall-cmd --permanent --add-service=httpsudo firewall-cmd --reloadClone Apprise API and install its Python dependencies:
cd /optsudo git clone https://github.com/caronc/apprise-api.gitsudo python3 -m venv /opt/apprise-api/venvsudo /opt/apprise-api/venv/bin/pip install --upgrade pipsudo /opt/apprise-api/venv/bin/pip install -r /opt/apprise-api/requirements.txtsudo install -d -o nginx -g nginx \ /var/lib/apprise/config /var/lib/apprise/attachCreate the systemd unit at /etc/systemd/system/apprise.service:
[Unit]Description=Apprise API ServiceAfter=network.target
[Service]User=nginxGroup=nginxWorkingDirectory=/opt/apprise-apiEnvironment=APPRISE_CONFIG_DIR=/var/lib/apprise/configEnvironment=APPRISE_ATTACH_DIR=/var/lib/apprise/attachEnvironment=APPRISE_STATEFUL_MODE=simpleEnvironment=APPRISE_WORKER_COUNT=1Environment=APPRISE_DEFAULT_FORMAT=textEnvironment=TZ=Etc/UTCExecStart=/opt/apprise-api/venv/bin/gunicorn \ --no-control-socket \ --workers 1 \ --worker-class gevent \ --timeout 300 \ --bind 127.0.0.1:8000 \ --chdir /opt/apprise-api/apprise_api \ core.wsgi:applicationRestart=on-failure
[Install]WantedBy=multi-user.targetEnable and start it:
sudo systemctl daemon-reloadsudo systemctl enable --now appriseAPPRISE_STATEFUL_MODE=simple stores each configuration as a readable file
and lets the configuration manager list your saved keys. One Gunicorn worker
keeps resource use modest for a home or small-team installation.
Copy the static files into Nginx’s document tree:
sudo install -d /usr/share/nginx/html/apprisesudo cp -R /opt/apprise-api/apprise_api/static/. \ /usr/share/nginx/html/apprise/Keeping these files under /usr/share/nginx/html lets the standard SELinux
policy serve them without relabeling the application source tree.
Create /etc/nginx/default.d/apprise.conf:
Rocky’s packaged Nginx configuration loads this file inside its existing default server block, so the server’s IP address works without a domain name.
client_max_body_size 500M;
location /s/ { alias /usr/share/nginx/html/apprise/;}
location / { proxy_pass http://127.0.0.1:8000; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $http_host; proxy_read_timeout 300s; proxy_send_timeout 300s;}Let NGINX reach Gunicorn. SELinux blocks it from connecting to other local ports by default:
sudo setsebool -P httpd_can_network_connect 1Test and reload NGINX:
sudo nginx -tsudo systemctl reload nginxVisit http://your-server-ip/ (or http://127.0.0.1/ if you’re testing
locally). Apprise API should load.
Logs and files
- Application files:
/opt/apprise-api - Saved configurations:
/var/lib/apprise/config - Temporary attachments:
/var/lib/apprise/attach - Gunicorn logs:
sudo journalctl -u apprise - NGINX logs:
/var/log/nginx/
Managing the service
sudo systemctl status apprisesudo systemctl restart apprisesudo systemctl stop appriseFreeBSD has no systemd, so this uses its own rc.d service manager instead.
The commands assume that sudo is installed and your account may use it. If
not, open a root shell with su - and omit sudo from each command.
Install the required packages:
sudo pkg install -y python3 git nginx rust pkgconfpython_version="$(python3 -c \ 'import sys; print(f"{sys.version_info.major}{sys.version_info.minor}")')"sudo pkg install -y "py${python_version}-virtualenv"FreeBSD builds Python without ensurepip, so the version-matched virtualenv
package is required to create an environment that contains Pip. Rust and
pkgconf allow Pip to build dependencies such as cryptography, for which
FreeBSD wheels may not be available.
Clone Apprise API and install its Python dependencies:
cd /usr/localsudo git clone https://github.com/caronc/apprise-api.gitsudo python3 -m virtualenv /usr/local/apprise-api/venvsudo /usr/local/apprise-api/venv/bin/pip install --upgrade pipsudo /usr/local/apprise-api/venv/bin/pip install \ -r /usr/local/apprise-api/requirements.txtsudo install -d -o www -g www \ /var/db/apprise/config /var/db/apprise/attachCreate the startup script at /usr/local/etc/rc.d/apprise:
#!/bin/sh
# PROVIDE: apprise# REQUIRE: LOGIN# KEYWORD: shutdown
. /etc/rc.subr
name=apprisercvar=apprise_enable
load_rc_config $name
: ${apprise_enable:="NO"}: ${apprise_user:="www"}: ${apprise_dir:="/usr/local/apprise-api"}: ${apprise_venv:="${apprise_dir}/venv"}: ${apprise_pidfile:="/var/run/${name}.pid"}: ${apprise_logfile:="/var/log/${name}.log"}
pidfile=${apprise_pidfile}command="/usr/sbin/daemon"command_args="-f -r -P ${pidfile} -o ${apprise_logfile} -u ${apprise_user} -t ${name} /usr/bin/env APPRISE_CONFIG_DIR=/var/db/apprise/config APPRISE_ATTACH_DIR=/var/db/apprise/attach APPRISE_STATEFUL_MODE=simple APPRISE_WORKER_COUNT=1 APPRISE_DEFAULT_FORMAT=text TZ=Etc/UTC ${apprise_venv}/bin/gunicorn --no-control-socket --workers 1 --worker-class gevent --timeout 300 --bind 127.0.0.1:8000 --chdir ${apprise_dir}/apprise_api core.wsgi:application"
run_rc_command "$1"Make it executable, enable it at boot, and start it:
sudo chmod +x /usr/local/etc/rc.d/apprisesudo sysrc apprise_enable=YESsudo service apprise startAPPRISE_STATEFUL_MODE=simple stores each configuration as a readable file
and lets the configuration manager list your saved keys. One Gunicorn worker
keeps resource use modest for a home or small-team installation. The -P
option records the supervising daemon process, allowing service apprise stop to stop the service instead of immediately restarting Gunicorn.
Replace the example server block inside the existing http { } section of
/usr/local/etc/nginx/nginx.conf with this one:
server { listen 80; server_name _;
client_max_body_size 500M;
location /s/ { alias /usr/local/apprise-api/apprise_api/static/; }
location / { proxy_pass http://127.0.0.1:8000; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $http_host; proxy_read_timeout 300s; proxy_send_timeout 300s; }}Enable and start NGINX:
sudo sysrc nginx_enable=YESsudo nginx -tsudo service nginx startVisit http://your-server-ip/ (or http://127.0.0.1/ if you’re testing
locally). Apprise API should load.
Logs and files
- Apprise log:
/var/log/apprise.log - Application files:
/usr/local/apprise-api - Saved configurations:
/var/db/apprise/config - Temporary attachments:
/var/db/apprise/attach - PID file:
/var/run/apprise.pid
Managing the service
sudo service apprise startsudo service apprise stopsudo service apprise restartsudo service nginx restartOnce deployed, the API and optional web interface are available at:
http://localhost:8000/Persistent Storage
Section titled “Persistent Storage”Apprise API supports persistent storage for:
- saved configurations
- cached authentication metadata
- uploaded attachments
The container expects the following writable paths:
| Path | Purpose |
|---|---|
/config | Saved configurations and internal state |
/attach | Uploaded attachments |
/plugin | Optional custom plugins |
/tmp | Runtime files (sockets, buffers, temp data) |
For most deployments, mounting only /config and /attach is sufficient.
Stateful Mode
Section titled “Stateful Mode”Persistent features follow the server mode, configuration lock, and current login:
| State | Configuration List | Move / Delete | Configuration Login | Configuration Content |
|---|---|---|---|---|
| Stateful mode disabled | Unavailable | Unavailable | Unavailable | Unavailable |
| Authentication off, configuration unlocked | Available with APPRISE_STATEFUL_MODE=simple unless APPRISE_ADMIN=no | Allowed | Unavailable | Available |
| Authentication off, configuration locked | Hidden and denied | Denied | Unavailable | Hidden; saved notifications still work |
| Configuration user, unlocked | Denied | May move their ID or clear its content | May update their own password | Available for their ID |
| Configuration user, locked | Denied | Denied | May update their own password | Hidden; saved notifications still work |
| Administrator, unlocked | Available with APPRISE_STATEFUL_MODE=simple unless APPRISE_ADMIN=no | Allowed for any ID | May manage any login | Available |
| Administrator, locked | Available with APPRISE_STATEFUL_MODE=simple unless APPRISE_ADMIN=no | Allowed for any ID | May manage any login | Available |
When configuration content is locked, the Web notification form asks for at least one tag because it cannot display the saved destinations.
An authenticated administrator retains full configuration access while locking is enabled. Configuration users and unauthenticated callers cannot add, retrieve, inspect, list, move, or delete configuration. A user account may still make authenticated stateful and stateless notifications, while a locked account is limited to tagged stateful notifications.
Global mode switches always win. Even an administrator cannot send a stateful or stateless notification when its global mode is disabled. Administrator credentials bypass per-configuration access and the configuration lock only.
Hardened Deployments
Section titled “Hardened Deployments”Apprise API also supports process and filesystem hardening. This protects the container itself, but it does not add HTTPS, authentication, or strict routing. For a public or multi-tenant server, combine these controls with the Deployment Behind a Proxy setup above.
Example hardened container configuration:
services: apprise: image: caronc/apprise:latest container_name: apprise user: "1000:1000" read_only: true
cap_drop: - ALL
security_opt: - no-new-privileges:true
ports: - "8000:8000"
environment: APPRISE_STATEFUL_MODE: simple APPRISE_WORKER_COUNT: 1 APPRISE_DEFAULT_FORMAT: "text"
volumes: - ./config:/config - ./attach:/attach
tmpfs: - /tmpImportant Notes:
/tmpmust remain writable- No files are written under
/var/log - All logs are written to stdout and stderr
Kubernetes Hardening Notes
Section titled “Kubernetes Hardening Notes”If you deploy in Kubernetes, see Quick Start -> Kubernetes -> Hardened (rootless + read-only) for a ready-to-apply example that:
- runs as a non-root user
- drops all Linux capabilities
- disables privilege escalation
- uses a read only root filesystem
- mounts
/tmpas an in-memoryemptyDir
Health Checks
Section titled “Health Checks”Apprise API exposes a health endpoint:
GET /status- Returns HTTP
200when healthy - Returns HTTP
417if a blocking issue is detected
Example:
curl http://localhost:8000/statusThis endpoint is suitable for:
- Docker health checks
- Kubernetes liveness probes
- external monitoring systems
Reverse Proxies and Base Paths
Section titled “Reverse Proxies and Base Paths”If Apprise API is hosted behind a reverse proxy or served under a subpath, set APPRISE_BASE_URL to accommodate this.
For a complete HTTPS proxy example with loopback-only container access, see Quick Start -> Docker + External Nginx above.
Example:
APPRISE_BASE_URL=/appriseThis ensures:
- correct URL generation
- correct web UI routing
- correct OpenAPI links
Authentication and Access Control
Section titled “Authentication and Access Control”Apprise API does not require authentication by default. Set APPRISE_AUTH_REQUIRED=yes to protect its endpoints with HTTP Basic Auth. Only Config IDs explicitly made public can bypass it, and only for tagged notifications.
| Mode | Settings | Result |
|---|---|---|
| Disabled | APPRISE_AUTH_REQUIRED=no | Requests remain open. Administrator settings and saved configuration logins are ignored. |
| Administrator enabled | APPRISE_AUTH_REQUIRED=yes with APPRISE_PASSWORD | The administrator can access every key and manage configuration logins. APPRISE_USER is optional. |
| Administrator disabled | APPRISE_AUTH_REQUIRED=yes without APPRISE_PASSWORD | Existing configuration users can access only their own keys. New logins cannot be created until an administrator password is configured. |
APPRISE_USER without APPRISE_PASSWORD produces a startup warning and leaves the administrator account disabled. See Environment Variables.
APPRISE_BASIC_AUTH_REALM names the server in login prompts. It does not change any credentials. The default is Apprise API; use a different name for each hosted instance.
Use HTTPS/TLS. Basic Auth does not encrypt credentials. HTTPS is required outside localhost or a trusted network.
API clients send Basic Auth with every request. The server briefly remembers successful checks to make repeated requests faster. It does not store the credentials or create an API session.
Browser pages use a signed login so Logout can end the session. JSON and plain-text API calls cannot use this browser login.
Stateful clients can keep using paths such as /notify/{KEY}. Supported endpoints also accept X-Apprise-Config-ID, which keeps the key out of the URL.
The packaged Nginx configurations explicitly expose GET /qr, /qr/{KEY},
and /qr/@ for Apprise Mobile setup. They reject other methods, discard request
bodies, disable proxy buffering, and mark responses no-store. Strict mode also
applies its protected-route rate limit to these requests.
The Apprise CLI’s apprise:// plugin sends this header in its default version 2 mode. Use ?v=1 only with older servers that require /notify/{KEY}.
Browser logins expire after 24 hours without activity. Each authenticated request renews that period. Changing APPRISE_WEB_AUTH_SECRET signs out every browser.
The Config ID field can open another configuration without showing its ID in the address bar. Users must log in when they switch to an ID with different credentials. Administrators can also choose or generate an ID from New Configuration.
Each Config ID uses one access mode:
| Access | Notification use | Configuration use |
|---|---|---|
user | Requires its credentials. Tags are optional. | The user may view, edit, clear, and replace their configuration. |
locked | Requires credentials and a specific tag, never all. | Hides content. The user may change their password and move the Config ID. |
public | Requires only the Config ID and a specific tag. | Hides content. Saved credentials, when present, keep the locked abilities. |
disabled | Rejects configuration-user notifications. | Freezes user access while preserving configuration and credentials. |
Public access applies only to stateful POST /notify/{KEY} calls. Other endpoints still require valid credentials. Attachments remain supported.
For stateless POST /notify, administrators can authenticate directly. A configuration user must provide explicit urls, matching credentials, and X-Apprise-Config-ID; the configuration must use user access. Without urls, the v2 header form remains a stateful send through the saved configuration.
The user and locked accounts may retrieve /status and /details with their credentials and Config ID. A disabled account cannot. Administrators may include a valid X-Apprise-Config-ID for client convenience, but do not need one.
To set access, open a configuration and select its lock icon or Authentication. Choose the access mode, enter credentials when required, then select Save. Administrators can use Randomize for either credential.
The Config ID and credentials are separate values. Share the Config ID and configured credentials with the user. A username is optional; saved passwords are not displayed again.
API clients manage access with POST /auth/{KEY}. The access field accepts
user, locked, public, or disabled. A new record defaults to user, or
to locked while APPRISE_CONFIG_LOCK=yes. Saved user access then behaves
as locked without being rewritten. Saved public access remains public
because it already hides configuration content. A configuration user may
change only their password and must repeat their username. See
API Endpoints.
Per-key locks are ignored while APPRISE_AUTH_REQUIRED is disabled, restoring the original open behavior. Their files remain and take effect again when authentication is enabled.
Logout ends the signed browser login. Credentials cached by the browser cannot silently restore it.
Old locks without configuration are removed after APPRISE_AUTH_PRUNE_SECONDS (30 days by default). When a user clears their configuration, their credentials remain and this grace period restarts so they can save a replacement. Locks with configuration remain. See Pruning.
Browser Origin Checks
Section titled “Browser Origin Checks”Browsers add an Origin header when a page changes data. Apprise API checks this header and rejects changes sent from another website. Requests from the same site are allowed.
Apprise API does not require Django’s CSRF token exchange. Direct calls from curl, mobile apps, and other programs normally omit Origin and continue to work.
For an HTTPS site behind a reverse proxy, set APPRISE_TRUSTED_ORIGINS to the full public address, such as https://apprise.example.com. Separate multiple addresses with commas.
For defense in depth, you can add controls outside the application:
- exposing the container only through a trusted reverse proxy
- adding authentication at the proxy or ingress layer
- restricting access with firewall or network policy rules
Nginx override files may also be mounted into the container for specialized access-control requirements.
A Kubernetes example that uses a Secret (basic auth .htpasswd) and a ConfigMap (nginx location-override.conf) is available under Quick Start -> Kubernetes -> Full example.
Pruning
Section titled “Pruning”The packaged container runs one low-priority cleanup cycle each day. It covers both areas below and has a time limit:
| Area | What it prunes | Retention setting |
|---|---|---|
| Persistent state | Apprise’s notification-state storage (APPRISE_STORAGE_DIR). | APPRISE_STORAGE_PRUNE_DAYS (default 30 days) |
| Authentication | Per-key Basic Auth locks without configuration. | APPRISE_AUTH_PRUNE_SECONDS (default 2592000, or 30 days) |
APPRISE_PRUNE_INTERVAL_SECONDS controls the schedule. Set APPRISE_PRUNE_ENABLED=no to disable scheduled cleanup. APPRISE_PRUNE_TIMEOUT_SECONDS changes the 8-hour time limit. The combined or individual commands may still run manually:
docker exec -it apprise python manage.py prunedocker exec -it apprise python manage.py storeprune --days 30docker exec -it apprise python manage.py authprune --seconds 2592000Logging and Observability
Section titled “Logging and Observability”- All logs are emitted to stdout and stderr
- No log files are written to disk
- Prometheus metrics are available at
/metrics - Successful
/status,/metrics, favicon, and robots probes are omitted from access logs; failures and rate-limit responses remain visible
Development Deployments
Section titled “Development Deployments”For local development, the repository includes Docker Compose overrides that:
- mount the local source tree
- reload UI and template changes without rebuilding
- expose the API on port
8000
This mode is intended for development only and is not recommended for production.
Next Steps
Section titled “Next Steps”Once deployed, you can:
- save configuration keys using the web UI or API
- send notifications using
/notifyor/notify/{KEY} - integrate external systems using HTTP or the Apprise CLI
Questions or Feedback?
Technical Issues
Having trouble with the code? Open an issue on GitHub: