Healthcheck web improvements + PUID & PGID support added to Docker

This commit is contained in:
Chris Caron 2024-06-29 20:57:38 -04:00
parent 6e57e33b8f
commit 48514c0aad
15 changed files with 328 additions and 99 deletions

View File

@ -45,7 +45,7 @@ FROM base as runtime
# Install requirements and gunicorn
COPY ./requirements.txt /etc/requirements.txt
COPY --from=builder /build/*.whl .
COPY --from=builder /build/*.whl ./
RUN set -eux && \
echo "Installing cryptography" && \
pip3 install *.whl && \
@ -55,6 +55,9 @@ RUN set -eux && \
apt-get update -qq && \
apt-get install -y -qq \
nginx && \
echo "Installing tools" && \
apt-get install -y -qq \
sed && \
echo "Cleaning up" && \
apt-get --yes autoremove --purge && \
apt-get clean --yes && \
@ -73,16 +76,12 @@ WORKDIR /opt/apprise
# Copy over Apprise API
COPY apprise_api/ webapp
#
# # Configuration Permissions (to run nginx as a non-root user)
# Configuration Permissions (to run nginx as a non-root user)
RUN umask 0002 && \
mkdir -p /attach /config /plugin /run/apprise && \
chown www-data:www-data -R /run/apprise /var/lib/nginx /attach /config /plugin
touch /etc/nginx/override.conf
# Handle running as a non-root user (www-data is id/gid 33)
USER www-data
VOLUME /config
VOLUME /attach
VOLUME /plugin
EXPOSE 8000
CMD ["/usr/local/bin/supervisord", "-c", "/opt/apprise/webapp/etc/supervisord.conf"]
CMD ["/opt/apprise/webapp/supervisord-startup"]

View File

@ -56,11 +56,17 @@ docker pull caronc/apprise:latest
# setting APPRISE_STATEFUL_MODE to simple allows you to map your defined {key}
# straight to a file found in the `/config` path. In simple home configurations
# this is sometimes the ideal expectation.
#
# Set your User ID or Group ID if you wish to over-ride the default of 1000
# in the below example, we make sure it runs as the user we created the container as
docker run --name apprise \
-p 8000:8000 \
-v /var/lib/apprise/config:/config \
-v /var/lib/apprise/plugin:/plugin \
-v /var/lib/apprise/attach:/attach \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-v /path/to/local/config:/config \
-v /path/to/local/plugin:/plugin \
-v /path/to/local/attach:/attach \
-e APPRISE_STATEFUL_MODE=simple \
-e APPRISE_WORKER_COUNT=1 \
-d caronc/apprise:latest
@ -72,11 +78,17 @@ A common change one might make is to update the Dockerfile to point to the maste
# Setup your environment the way you like
docker build -t apprise/local:latest -f Dockerfile .
# Set up a directory you wish to store your configuration in:
mkdir -p /etc/apprise
# Launch your instance
docker run --name apprise \
-p 8000:8000 \
-e PUID=$(id -u) \
-e PGID=$(id -g) \
-e APPRISE_STATEFUL_MODE=simple \
-e APPRISE_WORKER_COUNT=1 \
-v /etc/apprise:/config \
-d apprise/local:latest
```
A `docker-compose.yml` file is already set up to grant you an instant production ready simulated environment:
@ -86,40 +98,6 @@ A `docker-compose.yml` file is already set up to grant you an instant production
docker-compose up
```
### Config Directory Permissions
Under the hood, An NginX services is reading/writing your configuration files as the user (and group) `www-data` which generally has the id of `33`. In preparation so that you don't get the error: `An error occured saving configuration.` consider also setting up your local `/var/lib/apprise/config` permissions as:
```bash
# Create a user/group (if one doesn't already exist) owned
# by the user and group id of 33
id 33 &>/dev/null || sudo useradd \
--system --no-create-home --shell /bin/false \
-u 33 -g 33 www-data
# Securely set the directory limiting access to only those who
# are part of the www-data group:
sudo chmod 770 -R /var/lib/apprise/config
sudo chown 33:33 -R /var/lib/apprise/config
# Now optionally add yourself to the group if you wish to be able to view
# contents.
sudo usermod -a -G 33 $(whoami)
# You may need to log out and back in again for the above usermod
# to reflect on you. Alternatively you can just type the following
# and it will work as a temporary solution:
sudo su - $(whoami)
```
Alternatively a dirty solution is to just set the directory with full read/write permissions (which is not ideal in a production environment):
```bash
# Grant full permission to the local directory you're saving your
# Apprise configuration to:
chmod 777 /var/lib/apprise/config
```
## Dockerfile Details
The following architectures are supported: `amd64`, `arm/v7`, and `arm64`. The following tags can be used:
@ -398,6 +376,8 @@ The use of environment variables allow you to provide over-rides to default sett
| Variable | Description |
|--------------------- | ----------- |
| `PUID` | The User ID you wish the Apprise instance under the hood to run as. The default is `1000` if not otherwise specified.
| `PGID` | The Group ID you wish the Apprise instance under the hood to run as. The default is `1000` if not otherwise specified.
| `APPRISE_DEFAULT_THEME` | Can be set to `light` or `dark`; it defaults to `light` if not otherwise provided. The theme can be toggled from within the website as well.
| `APPRISE_DEFAULT_CONFIG_ID` | Defaults to `apprise`. This is the presumed configuration ID you always default to when accessing the configuration manager via the website.
| `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:<br/> - Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. However for the path for the container is `/config`.

View File

@ -37,9 +37,9 @@
</a>
<h1>{% trans "Apprise API" %}</h1>
<ul>
<li>APPRISE v{{APPRISE_VERSION}}</li>
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
</ul>
<li>APPRISE v{{APPRISE_VERSION}}</li>
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
</ul>
</div>
</div>
<!-- Page Layout here -->
@ -50,10 +50,10 @@
<ul class="collection z-depth-1">
<a class="collection-item" href="{% url 'config' DEFAULT_CONFIG_ID %}"><i class="material-icons">settings</i>
{% trans "Configuration Manager" %}</a>
{% if not CONFIG_LOCK %}
{% if not CONFIG_LOCK %}
<a class="collection-item" href="{% url 'config' UNIQUE_CONFIG_ID %}"><i class="material-icons">refresh</i>
{% trans "New Configuration" %}</a>
{% endif %}
{% endif %}
</ul>
{% endif %}
<ul class="collection z-depth-1">
@ -80,6 +80,28 @@
</div>
<div class="col s9">
<div id="health-check" class="section" style="display: none">
<h4><i class="material-icons" style="color: orange">warning</i>&nbsp;{% trans "Apprise Health Check Failed" %}&nbsp;<i class="material-icons" style="color: orange">warning</i></h4>
{% blocktrans %}The following disk access errors have been detected with your Apprise instance{% endblocktrans %}:
<ul>
<li class="can_write_config" style="display: none"><strong>
<i class="material-icons"
style="color: red">cancel</i>&nbsp;{% trans "Configuration Write Failure" %}</strong>
<p>{% blocktrans %}Apprise can not write new configuration information to the directory:{% endblocktrans %} <code>{{CONFIG_DIR}}</code>.</p>
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_CONFIG_LOCK=yes</code> and reload your Apprise instance.{% endblocktrans %}</p>
</li>
<li class="can_write_attach" style="display: none"><strong>
<i class="material-icons"
style="color: red">cancel</i>&nbsp;{% trans "Attachment Temporary Storage Write Failure" %}</strong>
<p>{% blocktrans %}Apprise can not circulate attachments (if provided) along to supported endpoints due to not having write access to the directory:{% endblocktrans %} <code>{{ATTACH_DIR}}</code>.</p>
<p>{% blocktrans %}<em>Note:</em> If this is the expected behavior, you should pre-set the environment variable <code>APPRISE_ATTACH_SIZE=0</code> and reload your Apprise instance.{% endblocktrans %}</p>
</p>
</li>
</ul>
<p>{% blocktrans %}Under most circumstances, the issue(s) identified here are usually related to permission issues. Make sure you set the correct <code>PUID</code> and <code>GUID</code> to reflect the permissions you wish Apprise to utilize when it is reading and writing its files. In addition to this, you may need to make sure the permissions are set correctly on the directories you mapped them too.{% endblocktrans %}</p>
<p>{% blocktrans %}The issue(s) identified here can also be associated with SELinux too. You may wish to rule out SELinux by first temporarily disabling it using the command <code>setenforce 0</code>. You can always re-enstate it with <code>setenforce 1</code>{% endblocktrans %}.</p>
</div>
{% block body %}{% endblock %}
</div>
@ -91,9 +113,37 @@
M.AutoInit();
// highlightjs
hljs.initHighlightingOnLoad();
{% block onload %} {% endblock %}
{% block onload %}{% endblock %}
// healthcheck
health_check()
});
{% block jsfooter %} {% endblock %}
function health_check() {
// perform our health check
document.querySelector('#health-check').style.display = 'none';
document.querySelector('#health-check li.can_write_config').style.display = 'none';
document.querySelector('#health-check li.can_write_attach').style.display = 'none';
let response = fetch('{% url "health" %}', {
method: 'GET',
headers: {
'Accept': 'application/json;charset=utf-8'
},
}).then(function(response) {
if(response.status != 200)
{
response.json().then(function(content) {
if (content['status']['can_write_config'] === false && content['config_lock'] === false) {
document.querySelector('#health-check li.can_write_config').style.display = '';
}
if (content['status']['can_write_attach'] === false && content['attach_lock'] === false) {
document.querySelector('#health-check li.can_write_attach').style.display = '';
}
document.querySelector('#health-check').style.display = '';
})
}
});
}
</script>
</body>

View File

@ -188,7 +188,7 @@
{% endif %}
{% endblock %}
{% block jsfooter %}
{{ block.super }}
{% if STATEFUL_MODE != 'disabled' %}
function update_count() {
const p_count = document.querySelectorAll('#url-list li.card-panel.selected').length;
@ -695,8 +695,8 @@ function notify_init() {
{% endblock %}
{% block onload %}
{% if STATEFUL_MODE != 'disabled' %}
{{ block.super }}
{% if STATEFUL_MODE != 'disabled' %}
document.querySelector('label [for="id_tag"]')
{
// create a new div with the class 'chips' assigned to it

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% load i18n %}
{% block body %}
<h4>{% trans "The Apprise API" %}</h4>
<p>

View File

@ -64,6 +64,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': False,
'status': {
'can_write_config': True,
'can_write_attach': True,
@ -87,6 +88,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content)
assert content == {
'config_lock': True,
'attach_lock': False,
'status': {
'can_write_config': False,
'can_write_attach': True,
@ -109,6 +111,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': False,
'status': {
'can_write_config': False,
'can_write_attach': True,
@ -131,6 +134,7 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': True,
'status': {
'can_write_config': True,
'can_write_attach': False,
@ -153,9 +157,10 @@ class HealthCheckTests(SimpleTestCase):
content = loads(response.content)
assert content == {
'config_lock': False,
'attach_lock': False,
'status': {
'can_write_config': True,
'can_write_attach': False,
'can_write_attach': True,
'details': ['OK']
}
}

View File

@ -181,6 +181,7 @@ class NotifyTests(SimpleTestCase):
# Reset our mock object
mock_notify.reset_mock()
# A setting of zero means unlimited attachments are allowed
with override_settings(APPRISE_MAX_ATTACHMENTS=0):
# Preare our form data
@ -196,6 +197,67 @@ class NotifyTests(SimpleTestCase):
form = NotifyForm(form_data, attach_data)
assert form.is_valid()
# Send our notification
response = self.client.post(
'/notify/{}'.format(key), form.cleaned_data)
# We're good!
assert response.status_code == 200
assert mock_notify.call_count == 1
# Reset our mock object
mock_notify.reset_mock()
# Only allow 1 attachment, but we'll attempt to send more...
with override_settings(APPRISE_MAX_ATTACHMENTS=1):
# Preare our form data
form_data = {
'body': 'test notifiction',
}
# At a minimum, just a body is required
form = NotifyForm(form_data)
assert form.is_valid()
# Required to prevent None from being passed into self.client.post()
del form.cleaned_data['attachment']
data = {
**form.cleaned_data,
'file1': SimpleUploadedFile(
"attach1.txt", b"content here", content_type="text/plain"),
'file2': SimpleUploadedFile(
"attach2.txt", b"more content here", content_type="text/plain"),
}
# Send our notification
response = self.client.post(
'/notify/{}'.format(key), data, format='multipart')
# Too many attachments
assert response.status_code == 400
assert mock_notify.call_count == 0
# Reset our mock object
mock_notify.reset_mock()
# A setting of zero means unlimited attachments are allowed
with override_settings(APPRISE_ATTACH_SIZE=0):
# Preare our form data
form_data = {
'body': 'test notifiction',
}
attach_data = {
'attachment': SimpleUploadedFile(
"attach.txt", b"content here", content_type="text/plain")
}
# At a minimum, just a body is required
form = NotifyForm(form_data, attach_data)
assert form.is_valid()
# Send our notification
response = self.client.post(
'/notify/{}'.format(key), form.cleaned_data)

View File

@ -207,6 +207,22 @@ class HTTPAttachment(A_MGR['http']):
pass
def touch(fname, mode=0o666, dir_fd=None, **kwargs):
"""
Acts like a Linux touch and updates a file with a current timestamp
"""
flags = os.O_CREAT | os.O_APPEND
try:
with os.fdopen(os.open(fname, flags=flags, mode=mode, dir_fd=dir_fd)) as f:
os.utime(f.fileno() if os.utime in os.supports_fd else fname,
dir_fd=None if os.supports_fd else dir_fd, **kwargs)
except OSError:
return False
return True
def parse_attachments(attachment_payload, files_request):
"""
Takes the payload provided in a `/notify` call and extracts the
@ -230,15 +246,12 @@ def parse_attachments(attachment_payload, files_request):
count += 1
if settings.APPRISE_ATTACH_SIZE <= 0:
raise ValueError("The attachment size is restricted to 0MB")
raise ValueError("Attachment support has been disabled")
if settings.APPRISE_MAX_ATTACHMENTS <= 0 or \
(settings.APPRISE_MAX_ATTACHMENTS > 0 and
count > settings.APPRISE_MAX_ATTACHMENTS):
if settings.APPRISE_MAX_ATTACHMENTS > 0 and count > settings.APPRISE_MAX_ATTACHMENTS:
raise ValueError(
"There is a maximum of %d attachments" %
settings.APPRISE_MAX_ATTACHMENTS
if settings.APPRISE_MAX_ATTACHMENTS > 0 else 0)
settings.APPRISE_MAX_ATTACHMENTS)
if isinstance(attachment_payload, (tuple, list, set)):
for no, entry in enumerate(attachment_payload, start=1):
@ -772,67 +785,70 @@ def healthcheck(lazy=True):
if not (settings.APPRISE_STATEFUL_MODE == AppriseStoreMode.DISABLED or settings.APPRISE_CONFIG_LOCK):
# Update our Configuration Check Block
path = os.path.join(ConfigCache.root, '.tmp_healthcheck')
path = os.path.join(ConfigCache.root, '.tmp_hc')
if lazy:
try:
modify_date = datetime.fromtimestamp(os.path.getmtime(path))
delta = (datetime.now() - modify_date).total_seconds()
if delta <= 7200.00: # 2hrs
if delta <= 30.00: # 30s
response['can_write_config'] = True
except FileNotFoundError:
# No worries... continue with below testing
pass
if not response['can_write_config']:
try:
os.makedirs(path, exist_ok=True)
# Write a small file
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
# Test writing 1 block
fp.write(b'.')
# Read it back
fp.seek(0)
fp.read(1) == b'.'
# Toggle our status
response['can_write_config'] = True
except OSError:
# Permission Issue or something else likely
# We can take an early exit
response['details'].append('CONFIG_PERMISSION_ISSUE')
if settings.APPRISE_MAX_ATTACHMENTS > 0 and settings.APPRISE_ATTACH_SIZE > 0:
if not (response['can_write_config'] or 'CONFIG_PERMISSION_ISSUE' in response['details']):
try:
os.makedirs(ConfigCache.root, exist_ok=True)
if touch(path):
# Toggle our status
response['can_write_config'] = True
else:
# We can take an early exit as there is already a permission issue detected
response['details'].append('CONFIG_PERMISSION_ISSUE')
except OSError:
# We can take an early exit as there is already a permission issue detected
response['details'].append('CONFIG_PERMISSION_ISSUE')
if settings.APPRISE_ATTACH_SIZE > 0:
# Test our ability to access write attachments
# Update our Configuration Check Block
path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_healthcheck')
path = os.path.join(settings.APPRISE_ATTACH_DIR, '.tmp_hc')
if lazy:
try:
modify_date = datetime.fromtimestamp(os.path.getmtime(path))
delta = (datetime.now() - modify_date).total_seconds()
if delta <= 7200.00: # 2hrs
if delta <= 30.00: # 30s
response['can_write_attach'] = True
except FileNotFoundError:
# No worries... continue with below testing
pass
if not response['can_write_attach']:
except OSError:
# We can take an early exit as there is already a permission issue detected
response['details'].append('ATTACH_PERMISSION_ISSUE')
if not (response['can_write_attach'] or 'ATTACH_PERMISSION_ISSUE' in response['details']):
# No lazy mode set or content require a refresh
try:
os.makedirs(path, exist_ok=True)
# Write a small file
with tempfile.TemporaryFile(mode='w+b', dir=path) as fp:
# Test writing 1 block
fp.write(b'.')
# Read it back
fp.seek(0)
fp.read(1) == b'.'
os.makedirs(settings.APPRISE_ATTACH_DIR, exist_ok=True)
if touch(path):
# Toggle our status
response['can_write_attach'] = True
else:
# We can take an early exit as there is already a permission issue detected
response['details'].append('ATTACH_PERMISSION_ISSUE')
except OSError:
# We can take an early exit
response['details'].append('ATTACH_PERMISSION_ISSUE')

View File

@ -161,8 +161,8 @@ class HealthCheckView(View):
and ACCEPT_ALL.match(request.headers.get('accept', '')) else \
MIME_IS_JSON.match(request.headers.get('accept', '')) is not None
# Run our healthcheck
response = healthcheck()
# Run our healthcheck; allow ?force which will cause the check to run each time
response = healthcheck(lazy='force' not in request.GET)
# Prepare our response
status = ResponseCode.okay if 'OK' in response['details'] else ResponseCode.expectation_failed
@ -172,6 +172,7 @@ class HealthCheckView(View):
return HttpResponse(response, status=status, content_type='text/plain') \
if not json_response else JsonResponse({
'config_lock': settings.APPRISE_CONFIG_LOCK,
'attach_lock': settings.APPRISE_ATTACH_SIZE <= 0,
'status': response,
}, encoder=JSONEncoder, safe=False, status=status)

View File

@ -29,4 +29,8 @@ def base_url(request):
"""
Returns our defined BASE_URL object
"""
return {'BASE_URL': settings.BASE_URL}
return {
'BASE_URL': settings.BASE_URL,
'CONFIG_DIR': settings.APPRISE_CONFIG_DIR,
'ATTACH_DIR': settings.APPRISE_ATTACH_DIR,
}

View File

@ -17,10 +17,10 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# Upload Restriction
##
client_max_body_size 500M;
##
# Upload Restriction
##
client_max_body_size 500M;
##
# Logging Settings
@ -43,6 +43,10 @@ http {
listen 8000;
listen [::]:8000;
# Allow users to map to this file and provide their own custom
# overrides such as
include /etc/nginx/override.conf;
# Main Website
location / {
include /etc/nginx/uwsgi_params;

View File

@ -4,9 +4,11 @@ pidfile=/run/apprise/supervisord.pid
logfile=/dev/null
logfile_maxbytes=0
user=www-data
group=www-data
[program:nginx]
command=/usr/sbin/nginx -c /opt/apprise/webapp/etc/nginx.conf
command=/usr/sbin/nginx -c /opt/apprise/webapp/etc/nginx.conf -p /opt/apprise
directory=/opt/apprise
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr

View File

@ -238,3 +238,33 @@ code.config-id {
.chip.selected {
font-weight: 600;
}
#health-check {
background-color: #f883791f;
border-radius: 25px;
padding: 2em;
margin-bottom: 2em;
}
#health-check h4 {
font-size: 30px;
}
#health-check h4 .material-icons {
margin-top: -0.2em;
}
#health-check li .material-icons {
font-size: 30px;
margin-top: -0.2em;
}
#health-check ul {
list-style-type: disc;
padding-left: 2em;
}
#health-check ul strong {
font-weight: 600;
font-size: 1.2rem;
display: block;
}

View File

@ -273,7 +273,6 @@ h5 {
em {
color: #5e81ac !important;
text-shadow: 6px 3px #2e3440
}
.card-panel {

78
apprise_api/supervisord-startup Executable file
View File

@ -0,0 +1,78 @@
#!/bin/bash
# Copyright (C) 2024 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
if [ $(id -u) -ne 0 ]; then
echo "You must be root to run this script."
echo "Caution: This should only be ran in a dockerized instance!"
exit 1
fi
# Default values
PUID=${PUID:=1000}
PGID=${PGID:=1000}
# lookup our identifier
GROUP=$(getent group $PGID 2>/dev/null | cut -d: -f1)
[ -z "$GROUP" ] && groupadd --force -g $PGID apprise &>/dev/null && \
GROUP=apprise
USER=$(id -un $PUID 2>/dev/null)
[ $? -ne 0 ] && useradd -M -N \
-o -u $PUID -G $GROUP -c "Apprise API User" -d /opt/apprise apprise && \
USER=apprise
if [ -z "$USER" ]; then
echo "The specified User ID (PUID) of $PUID is invalid; Aborting operation."
exit 1
elif [ -z "$GROUP" ]; then
echo "The specified Group ID (PGID) of $PGID is invalid; Aborting operation."
exit 1
fi
# Ensure our group has been correctly assigned
usermod -a -G $GROUP $USER &>/dev/null
chmod o+w /dev/stdout /dev/stderr
[ ! -d /attach ] && mkdir -p /attach
chown -R $USER:$GROUP /attach
[ ! -d /config ] && mkdir -p /config
chown $USER:$GROUP /config
[ ! -d /plugin ] && mkdir -p /plugin
[ ! -d /run/apprise ] && mkdir -p /run/apprise
# Some Directories require enforced permissions to play it safe
chown $USER:$GROUP -R /run/apprise /var/lib/nginx /opt/apprise
sed -i -e "s/^\(user[ \t]*=[ \t]*\).*$/\1$USER/g" \
/opt/apprise/webapp/etc/supervisord.conf
sed -i -e "s/^\(group[ \t]*=[ \t]*\).*$/\1$GROUP/g" \
/opt/apprise/webapp/etc/supervisord.conf
# Working directory
cd /opt/apprise
# Launch our SupervisorD
/usr/local/bin/supervisord -c /opt/apprise/webapp/etc/supervisord.conf
# Always return our SupervisorD return code
exit $?