Linux/Docker/Portainer Stack - Delayed Start & Stop
Automatically Start/Stop Docker Stacks with Specified Delay and in Specified Order
Below are the relevant code steps to automatically start stacks in a specific order with adjustable delay via Portainer API and services on Ubuntu host, and stops stacks in reverse start order when Ubuntu host is rebooted/shutdown.
This was Developed for:
- Ubuntu Server 24LTS
- Docker
- Portainer
NOTE: In your compose files for the managed stacks, use restart: “no” and let the script start them.
Table of Contents:
1) Setup/Get Portainer Information
- Create an API key in Portainer
- Log into Portainer.
- Top-right: click your username → My account.
- Go to Access tokens (or API keys, depending on version).
- Add access token, give it a name like stack-autostart, create it, and copy the token (you won’t see it again).
- You’ll use this as
X-API-Key.
- Get your endpointId and stack IDs
- Find endpointId for local
- On a simple one-host setup it’s usually 1 or 2
From your Docker host:
curl -s \
-H "X-API-Key: YOUR_API_KEY_HERE" \
http://PORTAINER_HOST:PORT/api/endpointsExample if Portainer is on the same host and using HTTPS on port 9443 (-k flag for setups with self signed certs):
DPORTAPIKEY="KEY HERE"
curl -s -k \
-H "X-API-Key: $DPORTAPIKEY" \
https://172.17.0.2:9443/api/endpoints
You’ll see JSON objects like:
[
{
"Id": 1,
"Name": "local",
...
}
]
So endpointId = 1.
Find stack IDs:
curl -s -k \
-H "X-API-Key: $DPORTAPIKEY" \
"https://172.17.0.2:9443/api/stacks"
You’ll see JSON objects like:
{
"Id": 5,
"Name": "dns-server",
...
}
{
"Id": 6,
"Name": "npm",
...
}
From that, note:
# dns-server stack → Id = 5
# npm stack → Id = 6
# (Substitute whatever actual names/IDs you see.)
3) Create the “start stacks in order” script
This reads the config and starts stacks in order, with per-stack delays.
Create /usr/local/sbin/start-portainer-stacks.sh
sudo nano /usr/local/sbin/start-portainer-stacks.sh
Make sure your .sh contains the below:
#!/bin/bash
set -euo pipefail
CONFIG_FILE="/etc/portainer-stacks.conf"
if [[ ! -r "$CONFIG_FILE" ]]; then
echo "ERROR: Cannot read $CONFIG_FILE" >&2
exit 1
fi
# shellcheck source=/etc/portainer-stacks.conf
source "$CONFIG_FILE"
wait_for_portainer() {
local max_retries=30 # total wait = max_retries * delay
local delay=2
echo "Waiting for Portainer at ${PORTAINER_URL} to become reachable..."
for ((i=1; i<=max_retries; i++)); do
if curl $CURL_EXTRA_OPTS -s -o /dev/null "${PORTAINER_URL}/api/status"; then
echo "Portainer is reachable (attempt $i)."
return 0
fi
echo "Portainer not reachable yet (attempt $i/$max_retries). Sleeping ${delay}s..."
sleep "$delay"
done
echo "ERROR: Portainer not reachable after $((max_retries * delay)) seconds." >&2
return 1
}
start_stack() {
local stack_id="$1"
local name="$2"
echo "Starting stack: $name (ID: $stack_id)..."
local http_code
local response
response=$(curl $CURL_EXTRA_OPTS -s -w "%{http_code}" \
-X POST "${PORTAINER_URL}/api/stacks/${stack_id}/start?endpointId=${ENDPOINT_ID}" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-o /tmp/portainer-stack-start-body.$$ \
) || true
http_code="$response"
# Accept:
# - 200/204: started OK
# - 409: already running -> treat as success / no-op
if [[ "$http_code" == "200" || "$http_code" == "204" ]]; then
echo "Stack ${name} started (HTTP ${http_code})."
elif [[ "$http_code" == "409" ]]; then
echo "Stack ${name} is already running (HTTP 409), treating as success."
else
echo "ERROR: Failed to start stack ${name} (ID: ${stack_id}). HTTP ${http_code}" >&2
echo "Response body:" >&2
cat /tmp/portainer-stack-start-body.$$ >&2 || true
rm -f /tmp/portainer-stack-start-body.$$ || true
return 1
fi
rm -f /tmp/portainer-stack-start-body.$$ || true
}
# --- main ---
wait_for_portainer || exit 1
for entry in "${STACKS[@]}"; do
IFS=':' read -r STACK_ID STACK_NAME STACK_DELAY <<< "$entry"
if [[ -n "${STACK_DELAY:-}" && "$STACK_DELAY" -gt 0 ]]; then
echo "Waiting ${STACK_DELAY}s before starting ${STACK_NAME}..."
sleep "$STACK_DELAY"
fi
start_stack "$STACK_ID" "$STACK_NAME"
done
echo "All stacks started in configured order."
Save and make executable:
sudo chmod +x /usr/local/sbin/start-portainer-stacks.sh
4) Create the “stop stacks in reverse start order” script
This uses the same config and stops stacks in reverse order
Create Stop script: /usr/local/sbin/stop-portainer-stacks.sh
sudo nano /usr/local/sbin/stop-portainer-stacks.sh
Make sure your .sh contains the below:
#!/bin/bash
set -euo pipefail
CONFIG_FILE="/etc/portainer-stacks.conf"
if [[ ! -r "$CONFIG_FILE" ]]; then
echo "ERROR: Cannot read $CONFIG_FILE" >&2
exit 1
fi
# Load PORTAINER_URL, API_KEY, ENDPOINT_ID, CURL_EXTRA_OPTS, STACKS
# shellcheck source=/etc/portainer-stacks.conf
source "$CONFIG_FILE"
stop_stack() {
local stack_id="$1"
local name="$2"
echo "Stopping stack: $name (ID: $stack_id)..."
if ! curl $CURL_EXTRA_OPTS -s --fail \
-X POST "${PORTAINER_URL}/api/stacks/${stack_id}/stop?endpointId=${ENDPOINT_ID}" \
-H "X-API-Key: ${API_KEY}" \
> /dev/null; then
echo "Warning: failed to stop stack $name" >&2
else
echo "Stack ${name} stop request sent."
fi
}
# Iterate STACKS in reverse order
for (( idx=${#STACKS[@]}-1 ; idx>=0 ; idx-- )); do
entry="${STACKS[$idx]}"
IFS=':' read -r STACK_ID STACK_NAME STACK_DELAY <<< "$entry"
stop_stack "$STACK_ID" "$STACK_NAME"
done
echo "All stacks requested to stop in reverse order."
Save and make executable:
sudo chmod +x /usr/local/sbin/stop-portainer-stacks.sh
5) Create "Start" Service
Hook into systemd, Add a systemd unit to run the script at boot
Create /etc/systemd/system/start-portainer-stacks.service:
sudo nano /etc/systemd/system/start-portainer-stacks.service
Make sure your .sh contains the below:
# /etc/systemd/system/start-portainer-stacks.service
[Unit]
Description=Start Docker stacks in order via Portainer
After=network-online.target docker.service
Wants=network-online.target docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/start-portainer-stacks.sh
[Install]
WantedBy=multi-user.target
Reload systemd and enable it:
sudo systemctl daemon-reload
sudo systemctl enable start-portainer-stacks.service
OPTIONAL – Test it without reboot first:
sudo systemctl start start-portainer-stacks.service
OPTIONAL – If something’s off after test, view logs:
journalctl -u start-portainer-stacks.service -xe
6) Create "Stop" Service
Hook into systemd, Add a systemd unit to run the script at shutdown/reboot
Create /etc/systemd/system/stop-portainer-stacks.service:
sudo nano /etc/systemd/system/stop-portainer-stacks.service
Make sure your .sh contains the below:
# /etc/systemd/system/stop-portainer-stacks.service
[Unit]
Description=Gracefully stop Portainer stacks in reverse order at shutdown
After=docker.service portainer.service
Requires=docker.service portainer.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
ExecStop=/usr/local/sbin/stop-portainer-stacks.sh
TimeoutStopSec=300
[Install]
WantedBy=multi-user.target
Reload systemd and enable it:
sudo systemctl daemon-reload
sudo systemctl enable stop-portainer-stacks.service
OPTIONAL – Test it without reboot first:
sudo /usr/local/sbin/stop-portainer-stacks.sh
OPTIONAL – If something’s off after test, view logs:
sudo journalctl -u stop-portainer-stacks.service -b
7) Reload and Enable
Reload + enable:
sudo systemctl daemon-reload
sudo systemctl enable start-portainer-stacks.service
sudo systemctl enable stop-portainer-stacks.service
8) Helpful Copy/Pastes for Updates
Get Endpoints:
DPORTAPIKEY="YOUR KEY HERE"
curl -s -k \
-H "X-API-Key: $DPORTAPIKEY" \
https://172.17.0.2:9443/api/endpoints
Get Stacks:
DPORTAPIKEY="YOUR KEY HERE"
curl -s -k \
-H "X-API-Key: $DPORTAPIKEY" \
"https://172.17.0.2:9443/api/stacks"
Open Config File:
sudo nano /etc/portainer-stacks.conf