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. Portainer Setup
  2. Create Shared Config File
  3. Create Start Script
  4. Create Stop Script
  5. Create Start Service
  6. Create Stop Service
  7. Reload and Enable
  8. Helpful Copy/Paste Snippets

1) Setup/Get Portainer Information

  1.  Create an API key in Portainer
    1. Log into Portainer.
    2. Top-right: click your username → My account.
    3. Go to Access tokens (or API keys, depending on version).
    4. Add access token, give it a name like stack-autostart, create it, and copy the token (you won’t see it again).
    5. You’ll use this as X-API-Key.
  2. Get your endpointId and stack IDs
    1. Find endpointId for local
    2. 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/endpoints

Example 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.)

2) Create Shared Config File

This will allow easy modification of start/stop orders and times after initial setup.

Shared config: /etc/portainer-stacks.conf

sudo nano /etc/portainer-stacks.conf

Make sure your .conf contains the below, tailored to your needs and results above:

  • IMPORTANT: Keep the delay on your first container >0.
    • I use 15 seconds for my system. 
    • Too little delay on the first stack will cause stack start failures as Portainer isn’t fully ready. 
# /etc/portainer-stacks.conf

# === Portainer connection ===
# Use http://127.0.0.1:9000 or your HTTPS URL.
PORTAINER_URL="https://172.17.0.2:9443"

# API key from Portainer (My account -> Access tokens)
API_KEY="CHANGE_ME_PORTAINER_API_KEY"

# Docker endpoint ID (often 1 for local)
ENDPOINT_ID=2

# If you use self-signed HTTPS, set this to "-k" for curl, otherwise leave empty.
CURL_EXTRA_OPTS="-k"

# === Stack order & delays ===
# Format: "STACK_ID:STACK_NAME:DELAY_BEFORE_START_SECONDS"
# - STACK_ID: numeric ID from /api/stacks
# - STACK_NAME: just for logging
# - DELAY_BEFORE_START_SECONDS: how long to sleep BEFORE starting this stack
#
# Example desired behavior on startup:
# 1) dns-server -> start immediately (delay 0)
# 2) npm -> start 10s after dns (delay 10)
# 3) other-stack -> start 20s after npm (delay 20)
#
# On shutdown, they’ll stop in REVERSE order:
# other-stack -> npm -> dns-server

STACKS=(
"5:dns-server:0"
"6:npm:10"
"7:other-stack:20"
)

 

Edit PORTAINER_URL, API_KEY, ENDPOINT_ID, and the STACKS entries to match your setup

Make it readable:

      sudo chmod 600 /etc/portainer-stacks.conf

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