Archestack mit Docker Compose hosten
Archestack läuft mit Docker Compose auf einem Server: PostgreSQL, die API, der Builder und die veröffentlichte App, davor Caddy, das jedes Zertifikat selbst besorgt. Ein Skript nimmt das neueste Archestack-Release, und jede Nacht von cron ausgeführt, oder von einer Pipeline mit der Version, die du wählst, hält es den Server auf dem neuesten Stand. Das ist der einfachste Weg, Archestack zu hosten. Wenn ein Server nicht reicht oder ein Upgrade nicht einmal einen kurzen Neustart kosten darf, betreibt Hosting auf AKS stattdessen mehrere API-Replikate.
Was wo läuft
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 - Ein Image, zwei Shells. Der Builder (
web) und die veröffentlichte App (web-app) sind dasselbe Web-Image, in einem anderen Modus gestartet. - Caddy fragt die API vor jedem neuen Zertifikat, sodass nur die Adresse eines Workspaces oder die eigene Domain eines Unternehmens eines bekommt, und zwar wenn jemand sie zum ersten Mal aufruft.
- Die API legt ihre Datenbanken selbst an, auf dem leeren PostgreSQL, und migriert sie beim Start. Es gibt kein Skript, das gegen die Datenbank ausgeführt werden muss.
- Die Daten liegen in drei Docker-Volumes:
archestack_postgres,archestack_filesfür Uploads undarchestack_backupsfür die eigenen Backups von Archestack. Ein viertes,archestack_nuget, hält die NuGet-Pakete, die Skripte verwenden, damit ein Update sie nicht erneut herunterlädt.
Bevor du anfängst
- Ein Linux-Server mit Docker Engine und deren Compose-Plugin. Vier Kerne und 8 GB Arbeitsspeicher sind ein Ausgangspunkt; Kapazität und Dimensionierung enthält die gemessenen Zahlen.
- Eine DNS-Zone, in der du Einträge anlegen kannst, und die Ports 80 und 443 des Servers aus dem Internet erreichbar (Weg A), oder ein Reverse Proxy auf dem Server, der schon einen Hostnamen bedient (Weg B).
- Ein Mailserver, über den Archestack senden darf, für Einladungen und das Zurücksetzen von Passwörtern.
Das Starter-Kit
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. Die .env ausfüllen
setup.sh erzeugt .env aus .env.example, mit einem
Datenbankpasswort, das niemand kennen muss, und nur für dich lesbar:
#!/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" Fülle dann jedes CHANGE-ME darin aus:
# .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_FILEwählt den Zugangsweg. Docker Compose liest den Wert aus.env, sodass jederdocker compose-Befehl im Ordner die richtigen Dateien ausführt. - Passwörter bleiben auf dem Server.
.envist die einzige Datei, die sie enthält; die.gitignoredes Kits hält sie aus einem Repository heraus. - Lass
POSTGRES_PASSWORDunverändert, sobald der Stack gestartet ist: Die Datenbank behält das Passwort, mit dem sie angelegt wurde.
2. Der Stack
Die Datenbank und die API, die sich beide Zugangswege teilen:
# 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: Weg A fügt den Builder, die veröffentlichte App und Caddy hinzu, auf eigenen Hostnamen:
# 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
}
Lass editor und api unter deiner Zone auf den Server zeigen, dazu einen
Wildcard-Eintrag * für die eigenen Adressen der Workspaces. Caddy holt seine Zertifikate
von Let's Encrypt, die Ports 80 und 443 müssen also aus dem Internet erreichbar sein.
Unter einem Pfad eines Hostnamens, den du schon hast
Weg B legt Archestack unter einen Pfad eines Hostnamens, den ein Reverse Proxy auf dem Server schon
bedient, etwa https://apps.acme.com/archestack/. Dafür braucht es keinen neuen
DNS-Eintrag, keine neue Adresse und kein eigenes Zertifikat, und die Leute nutzen weiter die
Adresse, die sie schon kennen. Es gibt kein Caddy: Die API und der Builder lauschen nur auf dem
Server, und dein Proxy schickt die Pfade an sie weiter.
# 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" Füge die Pfade deinem Proxy hinzu, für 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;
} oder für 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
} - Schreibe den Pfad nicht um. Archestack bekommt sein Präfix mitgeteilt und schreibt seine eigenen Adressen damit, es muss das Präfix also sehen.
- Ein Proxy, der selbst ein Container ist, erreicht Archestack über ein gemeinsames
Docker-Netzwerk statt über die Ports des Servers: Verbinde ihn mit dem Netzwerk des Stacks und
verwende
api:8080undweb:80. - Halte Anmelde-Proxys davon fern. Ein Proxy, der den
Authorization-Header ersetzt, lässt jeden API-Aufruf scheitern: Die veröffentlichte App, Data-API-Schlüssel, Deploy-Tokens, VS Code und MCP bringen alle ihren eigenen mit. - Der Hostname wird geteilt. Anmelde-Tokens liegen im Speicher des Browsers für diese Adresse, den jede Anwendung darauf lesen kann, ein Skript, das in eine davon eingeschleust wird, erreicht also Archestack-Sitzungen.
- Auf diesem Weg nicht verfügbar: Workspace-Adressen und die eigenen Domains von Unternehmen, die Caddys Zertifikate bei Bedarf brauchen.
3. Starten
# 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 Öffne sofort den Builder und lege den ersten Admin an. Bis das jemand tut, wird Admin, wer ihn als Erstes erreicht.
Das neueste Release, automatisch
update.sh ermittelt, welches Release laufen soll, schreibt es in .env und
startet den Stack damit. Der erste Lauf startet alles; jeder spätere Lauf wechselt auf ein neueres
Release, wenn es eines gibt:
#!/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 Über cron, jede Nacht:
# crontab -e: every night at 03:00, the newest release
0 3 * * * /opt/archestack/update.sh >> /opt/archestack/update.log 2>&1 - Eine Versionsnummer, nie
latest. Das neueste Release wird in seine Nummer aufgelöst, so wie es die AKS-Pipeline tut, und jeder Container läuft mit dieser Nummer.docker compose pszeigt, welche läuft. -
ARCHESTACK_TRACKhält sich an eine Versionslinie:1.4nimmt die neueste1.4.xund wechselt nie von selbst auf1.5. Setze den Wert auf eine vollständige Version, etwa1.4.23, um auf dieser zu bleiben. - Ein Upgrade ist ein kurzer Neustart. Es gibt nur einen API-Container, die Instanz antwortet also nicht, während der neue startet und die Datenbanken migriert: meist Sekunden, länger, wenn ein Release viele Workspaces migriert. Führe das nächtliche Update zu einer ruhigen Uhrzeit aus.
- Zurückgehen heißt
./update.sh 1.4.22und dannARCHESTACK_TRACKdarauf setzen, sonst rückt der nächste nächtliche Lauf wieder vor. Die Datenbanken bleiben so, wie die neuere Version sie migriert hat, ziehe deshalb das nächste Release vor, das das Problem behebt. - PostgreSQL und Caddy folgen ihren Image-Tags (
postgres:16,caddy:2-alpine), das Update bringt also auch ihre Patch-Releases, nie eine neue Major-Version.
Aus einer Pipeline
Statt cron kann eine Pipeline auf Azure DevOps, GitHub Actions oder GitLab CI den Server
aktualisieren, mit der Version, die du wählst. Sie läuft aus einem Repository, das das Kit als
archestack/ enthält: Sie kopiert das Kit per SSH auf den Server und führt dort
update.sh aus. Was läuft, ist dann die Kopie im Repository, eine Änderung an einer
Compose-Datei wird also geprüft und ausgerollt wie jede andere Änderung. .env verlässt
den Server nie. Jede Umgebung, von Entwicklung bis Produktion, ist ein eigener Server.
Auf jedem Server, einmalig, ein Benutzer, als der sich die Pipeline anmeldet und dem der Ordner des Kits gehört:
# 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)"
Entpacke das Kit nach /opt/archestack und führe die Schritte 1 bis 3 als dieser
Benutzer aus (sudo -iu archestack). Lege dann im CI-System eine Umgebung pro Server an,
zuerst archestack-dev, mit diesen Werten:
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 Alle drei Pipelines führen deploy.sh aus:
#!/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" - Der Server wird geprüft, bevor irgendetwas gesendet wird. Ein Server, dessen
Host-Key nicht der in
SSH_KNOWN_HOSTSist, wird abgelehnt, nie beim ersten Kontakt einfach als vertrauenswürdig übernommen. Die Zeile oben wird aus der eigenen Schlüsseldatei des Servers erzeugt, es liegt also kein Netzwerk zwischen dir und ihm. - Die Gruppe docker ist so gut wie root auf diesem Server, und der private Schlüssel ebenso. Lege einen Schlüssel pro Server an und bewahre ihn nur in den Secrets des CI-Systems auf.
- Entferne die cron-Zeile, sobald eine Pipeline den Server aktualisiert, sonst bringt ihn die nächste Nacht wieder auf das neueste Release. Ein Zeitplan im CI-System kann ihre Stelle einnehmen.
- Wähle eine vollständige Version, etwa
1.4.23, damit Abnahme und Produktion genau das bekommen, was in Test läuft.
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) - Einrichten. Füge
archestack/azure-pipelines.ymlals Pipeline hinzu. Unter Pipelines, Library enthält die Variablengruppearchestack-devdie drei Einstellungen, und der private Schlüssel ist das Secure filearchestack-dev.key. Beim ersten Lauf wirst du gefragt, ob die Pipeline beide verwenden darf. - Genehmigungen. Füge unter Pipelines, Environments
archestack-prdeinen Approvals-Check hinzu, damit die Produktion auf eine Person wartet.
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 }} - Einrichten. Verschiebe die Datei nach
.github/workflows/archestack.yml: GitHub führt Workflows nur aus diesem Ordner aus. Unter Settings, Environments sind die drei Einstellungen Variablen undSSH_PRIVATE_KEYist ein Secret. Starte den Workflow unter Actions, Archestack, Run workflow. - Required reviewers auf
archestack-prdlassen die Produktion auf eine Person warten. In einem privaten Repository brauchen Umgebungen GitHub Pro, Team oder 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 - Einrichten. Binde die Datei aus der eigenen
.gitlab-ci.ymldes Repositorys ein, oder lege eine an, die nur dies enthält:include: - local: archestack/gitlab-ci.yml - Variablen. Beschränke unter Settings, CI/CD, Variables jede auf die Umgebung
archestack-dev, mit dem privaten Schlüssel alsSSH_KEY_FILEvom Typ File, und schütze sie, sodass nur geschützte Branches sie bekommen. Starte die Pipeline unter Build, Pipelines, New pipeline.
Daten und Backups
Archestack sichert jeden Workspace selbst, in das Volume archestack_backups (siehe
Database Backups). Dieses Volume liegt auf demselben Datenträger
wie alles andere, kopiere die Backups deshalb zusätzlich vom Server weg:
# 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 Stolperfallen
- Docker veröffentlicht Ports an der Firewall des Hosts vorbei. Ein Port in einer
Compose-Datei ist offen, auch wenn ufw etwas anderes sagt. Weg B bindet deshalb an
127.0.0.1; lass es so. - Ein
$in einem Wert von.envwird als Variable gelesen. Setze diesen Wert in einfache Anführungszeichen. - Eine neue PostgreSQL-Major-Version (17) ist kein Wechsel des Images: Ihre Dateien
sind anders. Dafür braucht es ein
pg_dumpallaus der alten und eine Wiederherstellung in die neue. -
docker compose down -vlöscht die Volumes und mit ihnen jeden Workspace.docker compose downallein behält sie.
Wie es weitergeht
Hosting auf AKS behandelt mehrere API-Replikate, Upgrades ohne Neustart und eine Pipeline pro Umgebung. Workspaces und Hosting erklärt, wie eine Instanz viele Workspaces bedient, und Deployment-Strategie beschreibt, wie du deine App aus einem Repository in sie deployst.