Przewodnik - 16 min

Hosting Archestack z Docker Compose

Archestack działa na jednym serwerze z Docker Compose: PostgreSQL, API, builder i opublikowana aplikacja, a przed nimi Caddy, który sam uzyskuje każdy certyfikat. Skrypt pobiera najnowsze wydanie Archestack, a uruchamiany co noc przez cron, albo przez pipeline z wybraną przez Ciebie wersją, utrzymuje serwer w aktualnym stanie. To najprostszy sposób hostowania Archestack. Gdy jeden serwer nie wystarcza albo aktualizacja nie może kosztować nawet krótkiego restartu, Hosting na AKS uruchamia zamiast tego kilka replik API.

Twój system CI

Co gdzie działa

                 this server, ports 80 and 443
                              |
                            Caddy ---- asks the API before every new certificate
          +-------------------+--------------------------+
    editor.<zone>         api.<zone>        <workspace>.<zone>, companies' own domains
          |                   |                          |
         web                 api                      web-app
                              |
                          postgres             volumes: postgres, files, backups, nuget
  • Jeden obraz, dwie powłoki. Builder (web) i opublikowana aplikacja (web-app) to ten sam obraz web, uruchomiony w innym trybie.
  • Caddy pyta API przed każdym nowym certyfikatem, więc adres przestrzeni roboczej albo własna domena firmy dostaje certyfikat, gdy ktoś odwiedzi je po raz pierwszy, a nic innego go nie dostaje.
  • API samo tworzy swoje bazy danych na pustym PostgreSQL i migruje je przy starcie. Nie ma skryptu, który trzeba uruchomić na bazie danych.
  • Dane znajdują się w trzech woluminach Dockera: archestack_postgres, archestack_files na przesłane pliki i archestack_backups na własne kopie zapasowe Archestack. Czwarty, archestack_nuget, przechowuje pakiety NuGet używane przez skrypty, dzięki czemu aktualizacja nie pobiera ich od nowa.

Zanim zaczniesz

  • Serwer linuksowy z Docker Engine i jego wtyczką Compose. Cztery rdzenie i 8 GB pamięci to punkt wyjścia; strona Wydajność i dobór serwerów podaje zmierzone liczby.
  • Strefa DNS, do której możesz dodawać rekordy, z portami 80 i 443 serwera osiągalnymi z internetu (wariant A), albo reverse proxy na serwerze, które już obsługuje nazwę hosta (wariant B).
  • Serwer pocztowy, przez który Archestack może wysyłać wiadomości, na potrzeby zaproszeń i resetowania haseł.

Zestaw startowy

setup.sh               makes .env, with a generated database password
.env.example           what .env holds: fill in every CHANGE-ME
update.sh              the newest release, pulled and started; nightly from cron
resolve-version.sh     newest, 1.4 or 1.4.23, made one version number
compose.yaml           PostgreSQL and the API, for both ways in
compose.caddy.yaml     way A: the builder, the published app and Caddy
Caddyfile              way A: hostnames and certificates
compose.path.yaml      way B: the builder under a path, for your own proxy
proxy/nginx.conf       way B: what to add to an nginx you already run
proxy/Caddyfile        way B: the same for a Caddy you already run
deploy.sh              from a pipeline: copies the kit to the server, runs update.sh there
azure-pipelines.yml    that pipeline on Azure DevOps
github-actions.yml     on GitHub Actions: move it to .github/workflows/archestack.yml
gitlab-ci.yml          on GitLab CI: include it from the repository's .gitlab-ci.yml

1. Uzupełnij .env

setup.sh tworzy .env z .env.example, z hasłem do bazy danych, którego nikt nie musi znać; plik może odczytać tylko Ty:

#!/usr/bin/env bash
# setup.sh: makes .env from .env.example, with a database password nobody has to know.
#   ./setup.sh
set -euo pipefail
cd "$(dirname "$0")"
if [ -e .env ]; then echo ".env already exists; leaving it as it is" >&2; exit 1; fi

# Letters and digits only: a ';' would end the connection string, a '$' would be read as a variable.
# Read in whole pieces: a reader that stops early would kill the writer, and pipefail the script.
password=""
while [ ${#password} -lt 40 ]; do
  password+=$(head -c 64 /dev/urandom | LC_ALL=C tr -dc 'A-Za-z0-9')
done
password=${password:0:40}
umask 077
sed "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$password/" .env.example > .env

echo "Made .env, readable by you only. Fill in every CHANGE-ME in it, then run ./update.sh"

Następnie uzupełnij w nim każde CHANGE-ME:

# .env: this server's settings. ./setup.sh makes it from .env.example; then fill in every CHANGE-ME.
# It holds passwords: keep it out of git (the kit's .gitignore does) and readable by you only.
# A value with a $ in it goes in single quotes, such as SMTP_PASSWORD='pa$$word'.

# Way A (hostnames of its own, Caddy) or way B (a path of a hostname your proxy already serves).
COMPOSE_PATH_SEPARATOR=:
COMPOSE_FILE=compose.yaml:compose.caddy.yaml
#COMPOSE_FILE=compose.yaml:compose.path.yaml

# The release running now, written by ./update.sh. ARCHESTACK_TRACK keeps updates to one line,
# such as 1.4; empty for the newest release.
ARCHESTACK_VERSION=
ARCHESTACK_TRACK=

# Made by ./setup.sh. Never change it here alone: the database keeps the one it was created with.
POSTGRES_PASSWORD=

# Way A: your zone. The builder at editor.<zone>, the API at api.<zone>, each workspace at
# <label>.<zone>. Empty both for way B.
ZONE=CHANGE-ME
ACME_EMAIL=CHANGE-ME

# Way B: the hostname your proxy already serves, the path Archestack goes under, and the ports
# on this server your proxy sends it to.
SHARED_HOST=
BASE_PATH=/archestack
API_PORT=8080
WEB_PORT=8081

# The mail server. Empty all five when the instance sends no mail.
SMTP_HOST=CHANGE-ME
SMTP_PORT=587
SMTP_USERNAME=CHANGE-ME
SMTP_PASSWORD=CHANGE-ME
SMTP_FROM=CHANGE-ME
  • COMPOSE_FILE wybiera wariant dostępu. Docker Compose odczytuje go z .env, więc każde polecenie docker compose w tym folderze uruchamia właściwe pliki.
  • Hasła zostają na serwerze. .env to jedyny plik, który je zawiera; .gitignore zestawu nie pozwala mu trafić do repozytorium.
  • Nie zmieniaj POSTGRES_PASSWORD, gdy stos już wystartował: baza danych zachowuje hasło, z którym została utworzona.

2. Stos

Baza danych i API, wspólne dla obu wariantów dostępu:

# compose.yaml: the database and the API, the part both ways in share. .env picks the rest
# (COMPOSE_FILE), so `docker compose up -d` runs the whole stack. Start it with ./update.sh.
name: archestack

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    # PostgreSQL keeps statistics for every database in shared memory, and every workspace owns
    # two. Docker's default of 64 MB runs out at about a thousand workspaces.
    shm_size: 1g
    environment:
      POSTGRES_USER: archestack
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?run ./setup.sh first}
      POSTGRES_DB: postgres
    volumes:
      - postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U archestack -d postgres"]
      interval: 5s
      retries: 30

  api:
    image: nickpeelman/omnicore-api:${ARCHESTACK_VERSION:?run ./update.sh, which picks the version}
    restart: unless-stopped
    depends_on:
      postgres: { condition: service_healthy }
    environment:
      ASPNETCORE_ENVIRONMENT: Production
      ASPNETCORE_URLS: http://+:8080
      # The API makes these databases itself on the empty server, and migrates them as it starts.
      ConnectionStrings__DefaultConnection: Host=postgres;Database=omnicore-db;Username=archestack;Password=${POSTGRES_PASSWORD};SSL Mode=Disable
      ConnectionStrings__Core: Host=postgres;Database=omnicore-core;Username=archestack;Password=${POSTGRES_PASSWORD};SSL Mode=Disable
      ConnectionStrings__Identity: Host=postgres;Database=omnicore-identity;Username=archestack;Password=${POSTGRES_PASSWORD};SSL Mode=Disable
      ConnectionStrings__Keycloak: ""
      Database__CreateMissing: "true"
      Identity__Provider: Builtin
      Identity__Issuer: omnicore
      Identity__ImportFromKeycloak: "false"
      Identity__AllowCompanySignup: "false"
      Smtp__Host: ${SMTP_HOST:-}
      Smtp__Port: ${SMTP_PORT:-587}
      Smtp__UseSsl: "true"
      Smtp__Username: ${SMTP_USERNAME:-}
      Smtp__Password: ${SMTP_PASSWORD:-}
      Smtp__From: ${SMTP_FROM:-}
      Smtp__FromName: Archestack
      # The proxy in front is on a Docker network; the rate limiter reads the visitor's address from it.
      ForwardedHeaders__TrustedProxies: 127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7
      Backup__Directory: /backups
      Files__Directory: /files
      # The NuGet packages scripts use, on a volume of the API's own, so an update does not
      # download them again.
      Scripting__NuGet__Directory: /nuget
    volumes:
      - files:/files
      - backups:/backups
      - nuget:/nuget

volumes:
  postgres:
  files:
  backups:
  nuget:

Wariant A dodaje builder, opublikowaną aplikację i Caddy, na własnych nazwach hostów:

# compose.caddy.yaml: way A. Hostnames of its own (editor.<zone>, api.<zone>, <workspace>.<zone>),
# behind Caddy, which gets every certificate by itself. Needs ports 80 and 443 and DNS records.
services:
  api:
    environment:
      Identity__AppBaseUrl: https://editor.${ZONE:?fill in ZONE in .env}
      # Turns on an address per workspace, and companies' own domains.
      Tenancy__HostSuffix: ${ZONE}

  # The builder.
  web:
    image: nickpeelman/omnicore-web:${ARCHESTACK_VERSION:?run ./update.sh}
    restart: unless-stopped
    environment:
      VITE_API_URL: https://api.${ZONE}
      VITE_UI_MODE: dashboard
      API_INTERNAL_URL: http://api:8080

  # The published app, at every workspace's address. Same image, other shell.
  web-app:
    image: nickpeelman/omnicore-web:${ARCHESTACK_VERSION:?run ./update.sh}
    restart: unless-stopped
    environment:
      VITE_API_URL: https://api.${ZONE}
      VITE_UI_MODE: kiosk
      VITE_DASHBOARD_URL: https://editor.${ZONE}
      API_INTERNAL_URL: http://api:8080

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      ZONE: ${ZONE}
      ACME_EMAIL: ${ACME_EMAIL:?fill in ACME_EMAIL in .env}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
# Caddyfile: way A. {$ZONE} and {$ACME_EMAIL} come from .env, through compose.caddy.yaml.
{
	email {$ACME_EMAIL}
	# A workspace address or a company's own domain gets a certificate only when the API says
	# the hostname belongs to a live workspace.
	on_demand_tls {
		ask http://api:8080/api/v1/tenants/verify-hostname
	}
}

(common) {
	encode zstd gzip
	header {
		# No includeSubDomains: on a company's own root domain it would force HTTPS on all
		# their other subdomains for a year.
		Strict-Transport-Security "max-age=31536000"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}
}

editor.{$ZONE} {
	import common
	reverse_proxy web:80
}

api.{$ZONE} {
	import common
	reverse_proxy api:8080 {
		# Live updates stream; nothing waits for a buffer to fill.
		flush_interval -1
	}
}

# Every other hostname: a workspace's address or a company's own domain, the published app.
https:// {
	import common
	tls {
		on_demand
	}
	reverse_proxy web-app:80
}

Skieruj editor i api w swojej strefie na serwer, a także rekord wieloznaczny * dla własnych adresów przestrzeni roboczych. Caddy uzyskuje certyfikaty od Let's Encrypt, więc porty 80 i 443 muszą być osiągalne z internetu.

Pod ścieżką nazwy hosta, którą już masz

Wariant B umieszcza Archestack pod ścieżką nazwy hosta, którą reverse proxy na serwerze już obsługuje, na przykład https://apps.acme.com/archestack/. Nie potrzebuje nowego rekordu DNS, nowego adresu ani własnego certyfikatu, a ludzie dalej korzystają z adresu, który już znają. Nie ma tu Caddy: API i builder nasłuchują tylko na serwerze, a Twoje proxy przekazuje do nich ścieżki.

# compose.path.yaml: way B. Under a path of a hostname the reverse proxy you already run serves,
# such as https://apps.acme.com/archestack/. No DNS record, address or certificate of its own.
# Both containers listen on this server only; your proxy sends the paths to them (see the guide).
services:
  api:
    environment:
      # Requests arrive with the prefix on; the API writes its own addresses with it.
      Http__PathBase: ${BASE_PATH:?fill in BASE_PATH in .env}
      Identity__AppBaseUrl: https://${SHARED_HOST:?fill in SHARED_HOST in .env}${BASE_PATH}
    ports:
      - "127.0.0.1:${API_PORT:-8080}:8080"

  # The builder, which also serves the published app under <path>/app.
  web:
    image: nickpeelman/omnicore-web:${ARCHESTACK_VERSION:?run ./update.sh}
    restart: unless-stopped
    environment:
      VITE_API_URL: ""
      BASE_PATH: ${BASE_PATH}
      VITE_UI_MODE: dashboard
      API_INTERNAL_URL: http://api:8080
    ports:
      - "127.0.0.1:${WEB_PORT:-8081}:80"

Dodaj ścieżki do swojego proxy, dla nginx:

# proxy/nginx.conf: way B, for an nginx that already serves the hostname. Paste these into its
# server block for that hostname, and reload nginx. The ports are API_PORT and WEB_PORT in .env.
# No path after the address in proxy_pass: the prefix goes through as it is, and Archestack needs it.

client_max_body_size 1024m;            # package imports and file uploads

location /archestack/api/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 3600s;          # backups and imports
}

location /archestack/hubs/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;            # live updates are WebSockets
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 3600s;
}

location = /archestack {
    return 301 /archestack/;
}

location /archestack/ {
    proxy_pass http://127.0.0.1:8081;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

albo dla Caddy:

# proxy/Caddyfile: way B, for a Caddy that already serves the hostname. Add these lines inside its
# site block for that hostname, and reload Caddy. The ports are API_PORT and WEB_PORT in .env.
# handle keeps the path as it is, which Archestack needs; WebSockets and large uploads just work.
apps.acme.com {
	handle /archestack/api/* {
		reverse_proxy 127.0.0.1:8080
	}
	handle /archestack/hubs/* {
		reverse_proxy 127.0.0.1:8080
	}
	redir /archestack /archestack/
	handle /archestack/* {
		reverse_proxy 127.0.0.1:8081
	}

	# ... what this hostname already serves
}
  • Nie przepisuj ścieżki. Archestack dostaje swój prefiks i tworzy z nim własne adresy, więc musi go widzieć.
  • Proxy, które samo jest kontenerem, łączy się z Archestack przez współdzieloną sieć Dockera zamiast przez porty serwera: podłącz je do sieci stosu i używaj api:8080 i web:80.
  • Nie stawiaj proxy logowania przed Archestack. Proxy, które zastępuje nagłówek Authorization, psuje każde wywołanie API: opublikowana aplikacja, klucze Data API, tokeny wdrożeniowe, VS Code i MCP wysyłają własny.
  • Nazwa hosta jest współdzielona. Tokeny logowania są przechowywane w pamięci przeglądarki dla tego adresu, którą może odczytać każda działająca pod nim aplikacja, więc skrypt wstrzyknięty do którejkolwiek z nich dociera do sesji Archestack.
  • Niedostępne w tym wariancie: adresy przestrzeni roboczych i własne domeny firm, które wymagają certyfikatów Caddy wystawianych na żądanie.

3. Uruchom

# On the server, in the folder the kit was unzipped to, such as /opt/archestack
./setup.sh                 # makes .env; then fill in every CHANGE-ME in it
./update.sh                # pulls the newest release and starts everything
docker compose ps          # every container running

Od razu otwórz builder i utwórz pierwszego admina. Dopóki ktoś tego nie zrobi, adminem zostaje ten, kto pierwszy do niego dotrze.

Najnowsze wydanie, automatycznie

update.sh ustala, które wydanie uruchomić, zapisuje je w .env i uruchamia na nim stos. Pierwsze uruchomienie startuje wszystko; każde kolejne przechodzi na nowsze wydanie, gdy takie jest:

#!/usr/bin/env bash
# update.sh: start Archestack on the newest release, or move it to one. Every night from cron:
#   0 3 * * * /opt/archestack/update.sh >> /opt/archestack/update.log 2>&1
# By hand:
#   ./update.sh            the newest release (within ARCHESTACK_TRACK, when .env sets one)
#   ./update.sh 1.4.22     exactly that one, also to go back
set -euo pipefail
cd "$(dirname "$0")"
[ -f .env ] || { echo "No .env yet: run ./setup.sh first" >&2; exit 1; }
if grep -v '^[[:space:]]*#' .env | grep -q CHANGE-ME; then echo "Fill in every CHANGE-ME in .env first" >&2; exit 1; fi
setting() { grep -E "^$1=" .env | tail -n 1 | cut -d= -f2- | tr -d "\"'"; }

# 1. Which version: a number, worked out once and written to .env, so every container runs it.
current=$(setting ARCHESTACK_VERSION)
track=$(setting ARCHESTACK_TRACK)
ARCHESTACK_VERSION=${1:-${track:-newest}} OUT_DIR=. bash resolve-version.sh > /dev/null
version=$(cat version)
if [ "$version" = "$current" ]; then
  echo "$(date -u +%FT%TZ) already on $version"
else
  echo "$(date -u +%FT%TZ) ${current:-nothing} -> $version"
  sed -i "s/^ARCHESTACK_VERSION=.*/ARCHESTACK_VERSION=$version/" .env
fi

# 2. Pull, then recreate whatever changed. Containers whose image and settings are the same keep running.
docker compose pull --quiet
docker compose up -d --remove-orphans

# 3. Wait for the API: it migrates every workspace before it answers, which takes a while with many.
for _ in $(seq 180); do
  if docker compose exec -T web wget -q -O /dev/null http://api:8080/health 2> /dev/null; then
    echo "$(date -u +%FT%TZ) Archestack $version is up"
    exit 0
  fi
  sleep 5
done
echo "$(date -u +%FT%TZ) the API did not answer within 15 minutes: docker compose logs api" >&2
exit 1

Co noc, z crona:

# crontab -e: every night at 03:00, the newest release
0 3 * * * /opt/archestack/update.sh >> /opt/archestack/update.log 2>&1
  • Numer wersji, nigdy latest. Najnowsze wydanie jest zamieniane na swój numer, tak samo jak robi to pipeline AKS, i każdy kontener działa na tym numerze. docker compose ps pokazuje, który z nich działa.
  • ARCHESTACK_TRACK trzyma się jednej linii: 1.4 bierze najnowszą 1.4.x i nigdy nie przechodzi samoczynnie na 1.5. Ustaw w nim pełną wersję, na przykład 1.4.23, żeby na niej zostać.
  • Aktualizacja to krótki restart. Jest jeden kontener API, więc gdy nowy startuje i migruje bazy danych, instancja nie odpowiada: zwykle przez kilka sekund, dłużej, gdy wydanie migruje wiele przestrzeni roboczych. Uruchamiaj nocną aktualizację o spokojnej porze.
  • Powrót to ./update.sh 1.4.22, a potem ustawienie na tę wersję ARCHESTACK_TRACK, bo inaczej następne nocne uruchomienie znowu przejdzie do przodu. Bazy danych pozostają w stanie, w jakim zmigrowała je nowsza wersja, więc lepiej postawić na kolejne wydanie, które naprawia problem.
  • PostgreSQL i Caddy podążają za tagami swoich obrazów (postgres:16, caddy:2-alpine), więc aktualizacja przynosi też ich wydania poprawkowe, ale nigdy nowej wersji głównej.

Z pipeline'u

Zamiast crona serwer może aktualizować pipeline w Azure DevOps, GitHub Actions albo GitLab CI, z wybraną przez Ciebie wersją. Działa on z repozytorium, które zawiera zestaw jako archestack/: kopiuje zestaw na serwer przez SSH i uruchamia tam update.sh. Działa wtedy kopia z repozytorium, więc zmiana w pliku Compose jest przeglądana i wdrażana jak każda inna zmiana. .env nigdy nie opuszcza serwera. Każde środowisko, od deweloperskiego po produkcyjne, to osobny serwer.

Na każdym serwerze, jednorazowo, użytkownik, jako który loguje się pipeline, będący właścicielem folderu zestawu:

# On the server, as root, once
useradd -m -s /bin/bash -G docker archestack                # the user the pipeline signs in as
install -d -o archestack -g archestack /opt/archestack       # the kit's folder, its own

# A key pair for the pipeline: the public half may sign in as archestack
ssh-keygen -t ed25519 -N '' -C archestack-dev -f /root/archestack-dev
install -d -m 700 -o archestack -g archestack /home/archestack/.ssh
install -m 600 -o archestack -g archestack /root/archestack-dev.pub /home/archestack/.ssh/authorized_keys
cat /root/archestack-dev                                    # the private half: into the CI system, then
rm /root/archestack-dev /root/archestack-dev.pub

# SSH_KNOWN_HOSTS, made from the server's own host key, under the name the pipeline uses
echo "archestack.example.com $(cut -d' ' -f1,2 /etc/ssh/ssh_host_ed25519_key.pub)"

Rozpakuj zestaw do /opt/archestack i wykonaj kroki od 1 do 3 jako ten użytkownik (sudo -iu archestack). Następnie w systemie CI utwórz jedno środowisko na każdy serwer, najpierw archestack-dev, zawierające:

SSH_HOST          archestack.example.com
SSH_USER          archestack
SSH_KNOWN_HOSTS   the line the last command printed
the private key   Azure DevOps: the secure file archestack-<env>.key
                  GitHub Actions: the secret SSH_PRIVATE_KEY
                  GitLab CI: SSH_KEY_FILE, a variable of type File

Wszystkie trzy pipeline'y uruchamiają deploy.sh:

#!/usr/bin/env bash
# archestack/deploy.sh: what the pipelines run (azure-pipelines.yml, github-actions.yml, gitlab-ci.yml).
# Copies this folder to the server, leaving the server's .env as it is, then runs update.sh there.
#   SSH_HOST            the server, such as archestack.example.com
#   SSH_USER            the user it signs in as: in the docker group, owning the folder
#   SSH_KNOWN_HOSTS     the server's host key, as a known_hosts line
#   SSH_PRIVATE_KEY     the private key, or SSH_KEY_FILE the path of a file holding it
#   ARCHESTACK_VERSION  newest, 1.4 for the newest 1.4.x, or 1.4.23; empty for what .env says
#   ARCHESTACK_DIR      the folder on the server, /opt/archestack unless set
set -euo pipefail
cd "$(dirname "$0")"

# An Azure DevOps variable that does not exist arrives as its own name, $(NAME).
missing() { case "${!1:-}" in "" | '$('*) return 0 ;; *) return 1 ;; esac; }
for name in SSH_HOST SSH_USER SSH_KNOWN_HOSTS; do
  if missing "$name"; then echo "Set $name for this environment" >&2; exit 1; fi
done
if missing SSH_PRIVATE_KEY && missing SSH_KEY_FILE; then
  echo "Set SSH_PRIVATE_KEY (or SSH_KEY_FILE) for this environment" >&2
  exit 1
fi
version=${ARCHESTACK_VERSION:-}
if [[ ! $version =~ ^[A-Za-z0-9.]*$ ]]; then echo "Not a version: $version" >&2; exit 1; fi
dir=${ARCHESTACK_DIR:-/opt/archestack}

# The key and the server's host key, in files only this job reads. A server whose key is not the one
# in SSH_KNOWN_HOSTS is refused, never trusted on first sight.
ssh_dir=$(mktemp -d)
trap 'rm -rf "$ssh_dir"' EXIT
if missing SSH_PRIVATE_KEY; then cat "$SSH_KEY_FILE"; else printf '%s\n' "$SSH_PRIVATE_KEY"; fi \
  | tr -d '\r' > "$ssh_dir/key"
chmod 600 "$ssh_dir/key"
printf '%s\n' "$SSH_KNOWN_HOSTS" | tr -d '\r' > "$ssh_dir/known_hosts"
remote=(ssh -i "$ssh_dir/key" -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes
  -o UserKnownHostsFile="$ssh_dir/known_hosts" "$SSH_USER@$SSH_HOST")

# 1. The kit as the repository has it. Never .env, which holds the passwords and stays on the server.
tar -czf - --exclude=.env --exclude=version --exclude=update.log . \
  | "${remote[@]}" "mkdir -p '$dir' && tar -xzf - -C '$dir' && chmod +x '$dir'/*.sh"

# 2. The release, pulled and started. update.sh waits until the API answers.
"${remote[@]}" "'$dir/update.sh' $version"
  • Serwer jest sprawdzany, zanim cokolwiek zostanie wysłane. Serwer, którego klucz hosta nie jest tym z SSH_KNOWN_HOSTS, zostaje odrzucony, nigdy nie jest uznawany za zaufany przy pierwszym połączeniu. Powyższa linia powstaje z własnego pliku klucza serwera, więc między Tobą a nim nie stoi żadna sieć.
  • Grupa docker jest równoznaczna z rootem na tym serwerze, tak samo jak klucz prywatny. Utwórz jeden klucz na każdy serwer i trzymaj go tylko w sekretach systemu CI.
  • Usuń linię crona, gdy serwer aktualizuje pipeline, bo inaczej następnej nocy znowu przejdzie on na najnowsze wydanie. Jego miejsce może zająć harmonogram w systemie CI.
  • Wybierz pełną wersję, na przykład 1.4.23, żeby środowisko akceptacyjne i produkcyjne dostały dokładnie to, co działa na testowym.
Twój system CI

Azure DevOps

# archestack/azure-pipelines.yml: Archestack on its server with Docker Compose, from Azure DevOps.
# Add it as a pipeline, run it by hand, and pick the environment and the Archestack version.
trigger: none

parameters:
  - name: environment
    displayName: Environment
    type: string
    default: dev
    values: [dev, tst, acc, prd]
  - name: version
    displayName: Archestack version (newest, 1.4 for the newest 1.4.x, or 1.4.23)
    type: string
    default: newest

variables:
  # One variable group per environment, named archestack-<environment>, holding:
  #   SSH_HOST          the server, such as archestack.example.com
  #   SSH_USER          the user the pipeline signs in as
  #   SSH_KNOWN_HOSTS   the server's host key, as a known_hosts line
  # and one secure file (Pipelines, Library, Secure files), archestack-<environment>.key: the private key.
  - group: archestack-${{ parameters.environment }}

stages:
  - stage: deploy
    displayName: Deploy ${{ parameters.environment }}
    jobs:
      - deployment: deploy
        displayName: Update the server
        pool:
          vmImage: ubuntu-latest
        # Add an Approvals check to archestack-prd (Pipelines, Environments) to make production wait.
        environment: archestack-${{ parameters.environment }}
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self

                - task: DownloadSecureFile@1
                  name: key
                  displayName: The SSH key
                  inputs:
                    secureFile: archestack-${{ parameters.environment }}.key

                - bash: bash archestack/deploy.sh
                  displayName: Copy the kit and update
                  env:
                    ARCHESTACK_VERSION: ${{ parameters.version }}
                    SSH_HOST: $(SSH_HOST)
                    SSH_USER: $(SSH_USER)
                    SSH_KNOWN_HOSTS: $(SSH_KNOWN_HOSTS)
                    SSH_KEY_FILE: $(key.secureFilePath)
  • Konfiguracja. Dodaj archestack/azure-pipelines.yml jako pipeline. W Pipelines, Library grupa zmiennych archestack-dev zawiera trzy ustawienia, a klucz prywatny to plik archestack-dev.key w Secure files. Pierwsze uruchomienie prosi o zgodę na to, żeby pipeline używał obu.
  • Zatwierdzenia. W Pipelines, Environments dodaj sprawdzenie Approvals do archestack-prd, żeby produkcja czekała na osobę zatwierdzającą.

GitHub Actions

# archestack/github-actions.yml: Archestack on its server with Docker Compose, from GitHub Actions.
# GitHub runs workflows from one folder only: move this file to .github/workflows/archestack.yml.
# Run it under Actions, Archestack, Run workflow, and pick the environment and the version.
name: Archestack

on:
  workflow_dispatch:
    inputs:
      environment:
        description: Environment
        type: choice
        options: [dev, tst, acc, prd]
        default: dev
      version:
        description: Archestack version (newest, 1.4 for the newest 1.4.x, or 1.4.23)
        default: newest

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    # One GitHub environment per server, named archestack-<environment>, holding the variables
    #   SSH_HOST          the server, such as archestack.example.com
    #   SSH_USER          the user the pipeline signs in as
    #   SSH_KNOWN_HOSTS   the server's host key, as a known_hosts line
    # and the secret SSH_PRIVATE_KEY, the private key.
    environment: archestack-${{ inputs.environment }}
    concurrency: archestack-${{ inputs.environment }}    # one update per server at a time
    steps:
      - uses: actions/checkout@v4
      - name: Copy the kit and update
        run: bash archestack/deploy.sh
        env:
          ARCHESTACK_VERSION: ${{ inputs.version }}
          SSH_HOST: ${{ vars.SSH_HOST }}
          SSH_USER: ${{ vars.SSH_USER }}
          SSH_KNOWN_HOSTS: ${{ vars.SSH_KNOWN_HOSTS }}
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
  • Konfiguracja. Przenieś plik do .github/workflows/archestack.yml: GitHub uruchamia workflowy tylko z tego folderu. W Settings, Environments trzy ustawienia są zmiennymi, a SSH_PRIVATE_KEY jest sekretem. Uruchom workflow w Actions, Archestack, Run workflow.
  • Wymagani recenzenci (required reviewers) przy archestack-prd sprawiają, że produkcja czeka na osobę zatwierdzającą. W prywatnym repozytorium środowiska wymagają GitHub Pro, Team albo Enterprise.

GitLab CI

# archestack/gitlab-ci.yml: Archestack on its server with Docker Compose, from GitLab CI. Include it
# from the repository's own .gitlab-ci.yml (a file holding just these two lines when there is none):
#   include:
#     - local: archestack/gitlab-ci.yml
# Run it under Build, Pipelines, New pipeline, and pick the environment and the version.
variables:
  ARCHESTACK_ENVIRONMENT:
    value: dev
    options: [dev, tst, acc, prd]
    description: Environment
  ARCHESTACK_VERSION:
    value: newest
    description: Archestack version (newest, 1.4 for the newest 1.4.x, or 1.4.23)

archestack:
  stage: deploy
  image: alpine:3.22
  rules:
    - if: $CI_PIPELINE_SOURCE == "web"
  # Variables scoped to this environment (Settings, CI/CD, Variables), all protected:
  #   SSH_HOST          the server, such as archestack.example.com
  #   SSH_USER          the user the pipeline signs in as
  #   SSH_KNOWN_HOSTS   the server's host key, as a known_hosts line
  #   SSH_KEY_FILE      type File: the private key
  environment:
    name: archestack-$ARCHESTACK_ENVIRONMENT
  resource_group: archestack-$ARCHESTACK_ENVIRONMENT      # one update per server at a time
  before_script:
    - apk add --no-cache bash openssh-client tar
  script:
    - bash archestack/deploy.sh
  • Konfiguracja. Dołącz plik we własnym .gitlab-ci.yml repozytorium albo utwórz taki, który zawiera tylko to:
    include:
      - local: archestack/gitlab-ci.yml
  • Zmienne. W Settings, CI/CD, Variables ogranicz zakres każdej zmiennej do archestack-dev, z kluczem prywatnym jako SSH_KEY_FILE typu File, i oznacz je jako chronione, żeby dostawały je tylko chronione gałęzie. Uruchom pipeline w Build, Pipelines, New pipeline.

Dane i kopie zapasowe

Archestack sam tworzy kopię zapasową każdej przestrzeni roboczej w woluminie archestack_backups (zobacz Database Backups). Ten wolumin jest na tym samym dysku co cała reszta, więc dodatkowo kopiuj kopie zapasowe poza serwer:

# Every database on the server, as one file, then copied off the server
docker compose exec -T postgres pg_dumpall -U archestack | gzip > archestack-$(date +%F).sql.gz

# Workspace files and Archestack's own backups, from their volumes
docker run --rm -v archestack_files:/files -v archestack_backups:/backups -v "$PWD":/out alpine \
  tar czf /out/archestack-files-$(date +%F).tar.gz /files /backups

Pułapki

  • Docker publikuje porty z pominięciem zapory hosta. Port w pliku Compose jest otwarty, nawet gdy ufw twierdzi inaczej. Z tego powodu wariant B wiąże porty z 127.0.0.1; zostaw to tak.
  • Znak $ w wartości w pliku .env jest odczytywany jako zmienna. Ujmij taką wartość w pojedyncze cudzysłowy.
  • Nowa wersja główna PostgreSQL (17) to nie jest zmiana obrazu: jej pliki się różnią. Wymaga pg_dumpall ze starej wersji i przywrócenia do nowej.
  • docker compose down -v usuwa woluminy, a wraz z nimi każdą przestrzeń roboczą. Samo docker compose down je zachowuje.

Co dalej

Strona Hosting na AKS opisuje kilka replik API, aktualizacje bez restartu i pipeline dla każdego środowiska. Strona Przestrzenie robocze i hosting wyjaśnia, jak jedna instancja obsługuje wiele przestrzeni roboczych, a strona Strategia wdrożeń opisuje wdrażanie Twojej aplikacji do nich z repozytorium.