Serverul de facturare internă are nevoie de un endpoint local care să publice starea aplicației, dar instalarea directă a bibliotecilor sale în sistemul gazdă ar complica actualizările și revenirea la versiunea precedentă. Îl vom livra ca Portable Service: un arbore Debian separat, care conține aplicația și unitățile sale systemd, dar folosește kernelul și managerul de servicii ale serverului.
La final va rula status-api.service, ascultând exclusiv pe 127.0.0.1:8088. Imaginea va fi construită cu debootstrap, datele operaționale vor rămâne pe gazdă, serviciul va avea utilizator dinamic, limite de memorie și CPU, iar un timer va verifica endpointul la fiecare cinci minute. Jurnalele vor intra într-un namespace propriu, cu limită de spațiu și retenție, iar scriptul de instalare va restaura automat imaginea precedentă dacă noua versiune nu trece testul HTTP.
Portable Services nu transformă serverul într-un host Docker sau Kubernetes. Nu apare un daemon de containere, nu există orchestrare, registru de imagini ori rețea virtuală proprie. Nici nu trebuie tratat ca o mașină virtuală: procesul folosește kernelul gazdei, iar izolarea depinde de opțiunile aplicate unității systemd.
Fixăm laboratorul la Debian 12 și systemd 252
Tutorialul folosește Debian 12 „bookworm”, systemd 252, Python 3.11 și un server x86-64 cu cel puțin 1 GB RAM, 2 GB liberi în /var și acces la depozitele Debian. Construirea durează, de regulă, 15-30 de minute, în funcție de conexiune și disc.
Rulați verificările înainte de a modifica serverul:
cat /etc/os-release
uname -m
systemd --version
python3 --version
findmnt -no FSTYPE /var
df -h /var
free -h
systemctl is-system-running
Un rezultat compatibil arată astfel; revizia exactă a pachetului systemd și valorile discului pot fi diferite după actualizările de securitate:
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
VERSION_ID="12"
x86_64
systemd 252 (...)
Python 3.11.x
ext4
Filesystem Size Used Avail Use% Mounted on
... ... ... 2G+ ... /var
running
portablectl este livrat de pachetul systemd-container în Debian 12. Numele poate induce în eroare: pachetul include și instrumente pentru containere, dar proiectul nostru folosește numai infrastructura Portable Services.
Instalați într-un singur bloc uneltele de construire și testare:
sudo apt update
sudo apt install --yes debootstrap systemd-container python3-minimal curl
portablectl --version
debootstrap --version
Dacă systemctl is-system-running afișează degraded, examinați mai întâi systemctl --failed. Un serviciu existent defect nu împiedică neapărat laboratorul, dar poate ascunde o problemă produsă de noua instalare.
Separăm imaginea, datele gazdei și retenția jurnalelor
Arborele portabil va fi păstrat în /var/lib/portables/status-api. Fișierul cu starea aplicației rămâne în /srv/status-api/status.json și va fi montat numai pentru citire în serviciu. Această separare permite înlocuirea completă a imaginii fără pierderea datelor.
Jurnalele primesc namespace-ul status-api. Configurația de mai jos limitează namespace-ul la 100 MB și elimină înregistrările mai vechi de 14 zile, în măsura în care sunt disponibile fișiere jurnal arhivate eligibile pentru curățare.
Harta proiectului va fi:
/srv/status-api/
└── status.json date operaționale păstrate pe gazdă
/var/lib/portables/status-api/
├── etc/
│ └── os-release identifică distribuția din imagine
├── srv/status-api/
│ └── status.json destinația montării read-only
└── usr/
├── lib/status-api/
│ ├── app.py serverul HTTP
│ ├── healthcheck.py verificarea periodică
│ └── version versiunea livrată
└── lib/systemd/system/
├── status-api.service
├── status-api-health.service
└── status-api-health.timer
/etc/systemd/journald@status-api.conf.d/
└── retention.conf limita și retenția jurnalelor
/usr/local/sbin/
└── build-status-api-portable construire, instalare și rollback
Pasul 1 — Creăm datele externe și un jurnal cu limită proprie
Fișierul JSON simulează informația transmisă de aplicația internă de facturare. Nu conține parole sau tokenuri. Directorul este deținut de root, iar serviciul primește doar acces de citire prin BindReadOnlyPaths=.
Înaintea unei eventuale modificări, salvăm configurația de retenție existentă.
sudo install -d -m 0755 /srv/status-api
sudo tee /srv/status-api/status.json >/dev/null <<'JSON'
{
"component": "facturare-interna",
"state": "operational"
}
JSON
sudo chown root:root /srv/status-api/status.json
sudo chmod 0644 /srv/status-api/status.json
sudo install -d -m 0755 /etc/systemd/journald@status-api.conf.d
if sudo test -e /etc/systemd/journald@status-api.conf.d/retention.conf; then
sudo cp -a
/etc/systemd/journald@status-api.conf.d/retention.conf
/etc/systemd/journald@status-api.conf.d/retention.conf.bak
fi
sudo tee /etc/systemd/journald@status-api.conf.d/retention.conf >/dev/null <<'INI'
[Journal]
SystemMaxUse=100M
MaxRetentionSec=14day
INI
python3 -m json.tool /srv/status-api/status.json
sudo systemd-analyze cat-config
systemd/journald@status-api.conf
SystemMaxUse= stabilește plafonul aproximativ ocupat de jurnalele persistente ale namespace-ului. MaxRetentionSec= completează limita de spațiu cu una de timp. Namespace-ul este activat ulterior prin LogNamespace=status-api din unități.
Outputul JSON așteptat este:
{
"component": "facturare-interna",
"state": "operational"
}
Punct de control: dacă python3 -m json.tool raportează o eroare, corectați JSON-ul înainte de construire; aplicația ar răspunde cu eroare HTTP când încearcă să-l citească.
Împachetăm aplicația și toate unitățile într-un singur constructor
Pasul 2 — Scriem scriptul care construiește și livrează imaginea
Scriptul creează un arbore Debian minim într-un director temporar din /var/lib/portables, adaugă aplicația și unitățile, validează fișierele, detașează versiunea curentă și instalează imaginea nouă. Dacă atașarea, pornirea sau testul HTTP eșuează, imaginea precedentă este pusă la loc.
Fișierul /usr/local/sbin/build-status-api-portable trebuie scris integral:
sudo tee /usr/local/sbin/build-status-api-portable >/dev/null <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
readonly IMAGE_NAME="status-api"
readonly IMAGE="/var/lib/portables/${IMAGE_NAME}"
readonly PREVIOUS="/var/lib/portables/${IMAGE_NAME}.previous"
readonly DATA_FILE="/srv/status-api/status.json"
readonly MIRROR="https://deb.debian.org/debian"
if [[ ${EUID} -ne 0 ]]; then
printf '%sn' "Eroare: scriptul trebuie rulat ca root." >&2
exit 1
fi
if [[ $# -ne 1 || ! $1 =~ ^[0-9]+.[0-9]+.[0-9]+$ ]]; then
printf '%sn' "Utilizare: $0 VERSIUNE, de exemplu 1.0.0" >&2
exit 2
fi
readonly VERSION="$1"
source /etc/os-release
if [[ ${ID:-} != "debian" || ${VERSION_ID:-} != "12" ]]; then
printf '%sn' "Eroare: constructorul acceptă numai Debian 12." >&2
exit 3
fi
for command_name in debootstrap portablectl systemctl curl python3; do
if ! command -v "${command_name}" >/dev/null 2>&1; then
printf 'Eroare: lipsește comanda %s.n' "${command_name}" >&2
exit 4
fi
done
if [[ ! -r "${DATA_FILE}" ]]; then
printf 'Eroare: %s lipsește sau nu poate fi citit.n' "${DATA_FILE}" >&2
exit 5
fi
python3 -m json.tool "${DATA_FILE}" >/dev/null
install -d -m 0755 /var/lib/portables
STAGING="$(mktemp -d /var/lib/portables/.status-api.XXXXXX)"
readonly STAGING
cleanup() {
if [[ -d "${STAGING}" ]]; then
rm -rf -- "${STAGING}"
fi
}
trap cleanup EXIT
printf 'Construiesc status-api %s în %sn' "${VERSION}" "${STAGING}"
debootstrap
--variant=minbase
--include=python3-minimal,systemd
bookworm
"${STAGING}"
"${MIRROR}"
install -d -m 0755
"${STAGING}/usr/lib/status-api"
"${STAGING}/usr/lib/systemd/system"
"${STAGING}/srv/status-api"
install -m 0644 /dev/null "${STAGING}/srv/status-api/status.json"
printf '%sn' "${VERSION}" >"${STAGING}/usr/lib/status-api/version"
chmod 0644 "${STAGING}/usr/lib/status-api/version"
cat >"${STAGING}/usr/lib/status-api/app.py" <<'PYTHON'
#!/usr/bin/env python3
import json
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
STATUS_FILE = Path("/srv/status-api/status.json")
VERSION_FILE = Path("/usr/lib/status-api/version")
LISTEN_ADDRESS = "127.0.0.1"
LISTEN_PORT = 8088
class StatusHandler(BaseHTTPRequestHandler):
server_version = "StatusAPI"
sys_version = ""
def send_bytes(self, status_code, content_type, body):
self.send_response(status_code)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self.send_bytes(200, "text/plain; charset=utf-8", b"okn")
return
if self.path == "/api/status":
try:
status = json.loads(STATUS_FILE.read_text(encoding="utf-8"))
version = VERSION_FILE.read_text(encoding="utf-8").strip()
body = json.dumps(
{"service_version": version, "status": status},
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
self.send_bytes(
200,
"application/json; charset=utf-8",
body,
)
except (OSError, json.JSONDecodeError) as error:
print(f"status_read_error: {error}", file=sys.stderr)
self.send_bytes(
500,
"application/json; charset=utf-8",
b'{"error":"status_unavailable"}',
)
return
self.send_bytes(
404,
"application/json; charset=utf-8",
b'{"error":"not_found"}',
)
def log_message(self, message_format, *arguments):
print(
f'client={self.client_address[0]} '
f'message="{message_format % arguments}"',
file=sys.stderr,
)
def main():
server = ThreadingHTTPServer(
(LISTEN_ADDRESS, LISTEN_PORT),
StatusHandler,
)
print(
f"status_api_ready address={LISTEN_ADDRESS} port={LISTEN_PORT}",
file=sys.stderr,
)
server.serve_forever()
if __name__ == "__main__":
main()
PYTHON
chmod 0755 "${STAGING}/usr/lib/status-api/app.py"
cat >"${STAGING}/usr/lib/status-api/healthcheck.py" <<'PYTHON'
#!/usr/bin/env python3
import sys
import urllib.error
import urllib.request
URL = "http://127.0.0.1:8088/health"
try:
with urllib.request.urlopen(URL, timeout=3) as response:
body = response.read()
if response.status != 200 or body != b"okn":
raise RuntimeError(
f"răspuns neașteptat: HTTP {response.status}, corp={body!r}"
)
except (OSError, RuntimeError, urllib.error.URLError) as error:
print(f"healthcheck_failed url={URL} error={error}", file=sys.stderr)
sys.exit(1)
print(f"healthcheck_ok url={URL}")
PYTHON
chmod 0755 "${STAGING}/usr/lib/status-api/healthcheck.py"
cat >"${STAGING}/usr/lib/systemd/system/status-api.service" <<'UNIT'
[Unit]
Description=API local pentru starea facturării
After=network.target
ConditionPathIsReadable=/srv/status-api/status.json
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/lib/status-api/app.py
Restart=on-failure
RestartSec=5s
DynamicUser=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
RestrictNamespaces=yes
RestrictRealtime=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictAddressFamilies=AF_UNIX AF_INET
SystemCallArchitectures=native
SystemCallFilter=@system-service
IPAddressDeny=any
IPAddressAllow=localhost
BindReadOnlyPaths=/srv/status-api/status.json
MemoryMax=96M
CPUQuota=20%
TasksMax=32
LogNamespace=status-api
[Install]
WantedBy=multi-user.target
UNIT
cat >"${STAGING}/usr/lib/systemd/system/status-api-health.service" <<'UNIT'
[Unit]
Description=Verifică endpointul local status-api
After=status-api.service
Requires=status-api.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /usr/lib/status-api/healthcheck.py
DynamicUser=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressDeny=any
IPAddressAllow=localhost
MemoryMax=48M
TasksMax=16
LogNamespace=status-api
UNIT
cat >"${STAGING}/usr/lib/systemd/system/status-api-health.timer" <<'UNIT'
[Unit]
Description=Rulează periodic verificarea status-api
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
RandomizedDelaySec=15s
Unit=status-api-health.service
[Install]
WantedBy=timers.target
UNIT
rm -f "${STAGING}/etc/machine-id"
rm -rf "${STAGING}/var/lib/apt/lists/"*
systemd-analyze verify
--root="${STAGING}"
"${STAGING}/usr/lib/systemd/system/status-api.service"
"${STAGING}/usr/lib/systemd/system/status-api-health.service"
"${STAGING}/usr/lib/systemd/system/status-api-health.timer"
portablectl inspect "${STAGING}"
if portablectl is-attached "${IMAGE_NAME}" >/dev/null 2>&1; then
systemctl disable --now
status-api-health.timer
status-api.service || true
portablectl detach "${IMAGE_NAME}"
fi
rm -rf -- "${PREVIOUS}"
if [[ -e "${IMAGE}" ]]; then
mv -- "${IMAGE}" "${PREVIOUS}"
fi
mv -- "${STAGING}" "${IMAGE}"
rollback() {
printf '%sn' "Instalarea nouă a eșuat; restaurez imaginea precedentă." >&2
systemctl disable --now
status-api-health.timer
status-api.service >/dev/null 2>&1 || true
portablectl detach "${IMAGE_NAME}" >/dev/null 2>&1 || true
rm -rf -- "${IMAGE}"
if [[ -e "${PREVIOUS}" ]]; then
mv -- "${PREVIOUS}" "${IMAGE}"
portablectl attach "${IMAGE_NAME}"
systemctl enable --now
status-api.service
status-api-health.timer
printf '%sn' "Imaginea precedentă a fost restaurată." >&2
else
printf '%sn' "Nu exista o imagine precedentă pentru restaurare." >&2
fi
exit 10
}
portablectl attach "${IMAGE_NAME}" || rollback
systemctl enable --now
status-api.service
status-api-health.timer || rollback
for attempt in 1 2 3 4 5; do
if curl
--silent
--show-error
--fail
--max-time 3
http://127.0.0.1:8088/health |
grep --quiet '^ok$'; then
printf 'status-api %s a trecut verificarea.n' "${VERSION}"
exit 0
fi
sleep 2
done
rollback
BASH
sudo chmod 0750 /usr/local/sbin/build-status-api-portable
sudo bash -n /usr/local/sbin/build-status-api-portable
set -euo pipefail oprește constructorul la o comandă nereușită, la o variabilă nedefinită sau la o eroare ascunsă într-un pipeline. Directorul temporar este creat pe același filesystem ca destinația, astfel încât mutarea imaginii să nu devină o copiere parțială.
DynamicUser=yes alocă identitatea de execuție fără a crea un cont permanent. CapabilityBoundingSet= și AmbientCapabilities= lasă procesul fără capabilități Linux. IPAddressDeny=any, urmat de IPAddressAllow=localhost, limitează comunicația la loopback. Resursele sunt plafonate la 96 MB, 20% dintr-un CPU și 32 de taskuri.
Un bash -n reușit nu produce output. Verificarea explicită este:
exit status 0
Punct de control: dacă bash -n indică un număr de linie, nu executați constructorul; comparați delimitatoarele BASH, PYTHON și UNIT cu fișierul complet.
Atașăm imaginea fără să pornim un runtime de containere
Pasul 3 — Construim versiunea 1.0.0 și inspectăm atașarea
Avertisment: dacă există deja /var/lib/portables/status-api, scriptul îl mută în status-api.previous. O copie precedentă cu acest nume este eliminată. Faceți un backup separat dacă acel arbore conține ceva ce trebuie păstrat.
sudo /usr/local/sbin/build-status-api-portable 1.0.0
sudo portablectl is-attached status-api
sudo portablectl inspect status-api
systemctl is-enabled status-api.service status-api-health.timer
systemctl is-active status-api.service status-api-health.timer
debootstrap descarcă sistemul minim și Python în imagine. portablectl attach nu pornește un sistem de inițializare în interiorul arborelui: extrage unitățile compatibile și creează legăturile necesare pentru ca systemd-ul gazdei să le ruleze cu acel arbore drept filesystem.
Unitățile încep cu status-api, același prefix ca imaginea. Astfel, portablectl le identifică fără a primi o listă suplimentară de prefixuri.
Fragmentele așteptate sunt:
attached
enabled
enabled
active
active
portablectl inspect trebuie să enumere status-api.service, status-api-health.service și status-api-health.timer. Liniile exacte despre imagine și căi pot varia.
Punct de control: dacă serviciul nu devine active, consultați imediat journalctl --namespace=status-api -u status-api.service -b; nu reporniți repetat înainte de a vedea prima eroare.
Administrarea ulterioară se face cu aceleași comenzi systemd folosite pentru serviciile obișnuite. Pentru recapitularea stărilor și a dependențelor, consultați diagnosticarea serviciilor cu systemctl.
Dovedim izolarea, limitele și monitorizarea periodică
Pasul 4 — Testăm endpointul și citim jurnalul separat
Solicităm cele două rute, verificăm socketul și rulăm manual health check-ul înainte de prima declanșare a timerului:
curl --fail --silent --show-error
http://127.0.0.1:8088/health
curl --fail --silent --show-error
http://127.0.0.1:8088/api/status
printf 'n'
sudo ss -ltnp 'sport = :8088'
sudo systemctl start status-api-health.service
sudo systemctl list-timers status-api-health.timer
sudo journalctl
--namespace=status-api
-u status-api.service
-u status-api-health.service
--since=-10min
--no-pager
Răspunsurile așteptate sunt:
ok
{"service_version":"1.0.0","status":{"component":"facturare-interna","state":"operational"}}
LISTEN 0 ... 127.0.0.1:8088 ...
healthcheck_ok url=http://127.0.0.1:8088/health
În lista timerelor, coloanele NEXT și LEFT depind de ora serverului și de întârzierea aleatorie. Important este ca status-api-health.timer să aibă o următoare declanșare.
Confirmați proprietățile aplicate procesului:
systemctl show status-api.service
-p RootDirectory
-p DynamicUser
-p MemoryMax
-p CPUQuotaPerSecUSec
-p TasksMax
-p LogNamespace
sudo systemd-analyze security
--no-pager
status-api.service
Un exemplu de proprietăți este:
RootDirectory=/var/lib/portables/status-api
DynamicUser=yes
MemoryMax=100663296
TasksMax=32
LogNamespace=status-api
Valoarea afișată pentru CPU și evaluarea systemd-analyze security pot avea altă reprezentare. Raportul este un instrument de inspecție, nu dovada unei frontiere echivalente cu o mașină virtuală.
Punct de control: dacă ss arată 0.0.0.0:8088 sau [::]:8088, opriți serviciul și verificați LISTEN_ADDRESS din imagine; proiectul trebuie să asculte numai pe 127.0.0.1.
Pasul 5 — Verificăm de pe stația administratorului prin tunel SSH
Serviciul nu este expus în LAN. Testul de pe altă mașină folosește un tunel SSH către loopback-ul serverului. În exemplu, admin@server.example.com este contul și domeniul de documentație care trebuie înlocuite cu valorile reale.
Pe stația administratorului:
ssh
-N
-L 18088:127.0.0.1:8088
admin@server.example.com
Păstrați sesiunea deschisă, apoi rulați într-un al doilea terminal al aceleiași stații:
curl --fail --silent --show-error
http://127.0.0.1:18088/api/status
printf 'n'
Rezultatul trebuie să fie același JSON:
{"service_version":"1.0.0","status":{"component":"facturare-interna","state":"operational"}}
-N îi spune clientului SSH să nu execute o comandă la distanță. -L leagă portul local 18088 de 127.0.0.1:8088 văzut de server. Nu adăugăm o regulă de firewall și nu schimbăm adresa pe care ascultă API-ul.
Punct de control: dacă SSH funcționează, dar curl primește Connection refused, verificați pe server systemctl is-active status-api.service și ss -ltn 'sport = :8088'.
Facem un upgrade controlat și păstrăm calea de întoarcere
Pasul 6 — Livrăm versiunea 1.0.1 și probăm rollback-ul operațional
Constructorul recreează imaginea din surse, în loc să modifice în loc arborele activ. Aceasta reduce driftul și păstrează ultima imagine funcțională în status-api.previous.
sudo /usr/local/sbin/build-status-api-portable 1.0.1
curl --fail --silent --show-error
http://127.0.0.1:8088/api/status
printf 'n'
sudo test -d /var/lib/portables/status-api.previous
sudo cat /var/lib/portables/status-api/usr/lib/status-api/version
sudo cat /var/lib/portables/status-api.previous/usr/lib/status-api/version
Outputul așteptat este:
{"service_version":"1.0.1","status":{"component":"facturare-interna","state":"operational"}}
1.0.1
1.0.0
Dacă testul HTTP din constructor nu reușește după cinci încercări, funcția rollback detașează imaginea defectă, mută status-api.previous în poziția activă și repornește unitățile. Pentru un rollout pe mai multe servere, această procedură se potrivește cu principiul descris în actualizările canary înainte de rollout complet: validați întâi o singură gazdă și continuați numai după verificarea jurnalelor.
Schimbarea trebuie înregistrată împreună cu versiunea, comenzile de validare și calea de revenire. Un model practic este disponibil în ghidul despre documentarea schimbărilor pe server.
Punct de control: dacă versiunea din răspuns nu coincide cu fișierul din imaginea activă, verificați să nu existe un alt proces pe portul 8088 și inspectați RootDirectory prin systemctl show.
Mesajele care indică o imagine incompatibilă sau un sandbox prea strict
No suitable portable service image found
Cauza probabilă este lipsa fișierului os-release, o imagine incompletă sau lipsa unităților cu prefixul status-api.
Verificați:
sudo test -r /var/lib/portables/status-api/etc/os-release
sudo find /var/lib/portables/status-api/usr/lib/systemd/system
-maxdepth 1 -name 'status-api*.service' -o -name 'status-api*.timer'
sudo portablectl inspect status-api
Nu creați manual un os-release fictiv. Reconstruiți imaginea cu debootstrap.
Failed to attach image: Image status-api is already attached
Imaginea are deja unități atașate. Nu ștergeți arborele de sub un serviciu activ.
sudo systemctl disable --now
status-api-health.timer
status-api.service
sudo portablectl detach status-api
sudo portablectl attach status-api
sudo systemctl enable --now
status-api.service
status-api-health.timer
status-api.service: Failed to set up mount namespacing: ...
Cauza este, de regulă, o cale absentă pentru BindReadOnlyPaths= sau o restricție a mediului în care rulează serverul.
sudo test -r /srv/status-api/status.json
sudo test -e
/var/lib/portables/status-api/srv/status-api/status.json
sudo systemctl status status-api.service --no-pager
Ambele căi trebuie să existe. Nu eliminați izolarea ca soluție permanentă.
status_read_error: urmat de o eroare JSON
Fișierul este montat și citit, dar nu mai conține JSON valid. Serviciul răspunde cu HTTP 500 și {"error":"status_unavailable"}.
python3 -m json.tool /srv/status-api/status.json
sudo systemctl start status-api-health.service
sudo journalctl
--namespace=status-api
-u status-api-health.service
-n 20
--no-pager
Corectați fișierul sursă; nu editați copia-placeholder din imagine.
healthcheck_failed ... <urlopen error [Errno 111] Connection refused>
Timerul a rulat, dar API-ul nu ascultă. Cauzele concrete sunt serviciul oprit, restartarea repetată sau portul indisponibil.
systemctl is-active status-api.service
sudo ss -ltnp 'sport = :8088'
sudo journalctl
--namespace=status-api
-u status-api.service
-n 50
--no-pager
Dacă alt proces ocupă portul, identificați proprietarul și decideți administrativ care serviciu trebuie mutat; nu terminați automat un PID necunoscut.
Eliminăm serviciul și restaurăm configurația gazdei
Avertisment: comenzile următoare opresc API-ul și șterg atât imaginea activă, cât și copia precedentă. Fișierul /srv/status-api/status.json este eliminat numai de ultima comandă.
sudo systemctl disable --now
status-api-health.timer
status-api.service || true
if sudo portablectl is-attached status-api >/dev/null 2>&1; then
sudo portablectl detach status-api
fi
sudo rm -rf --
/var/lib/portables/status-api
/var/lib/portables/status-api.previous
sudo rm -f /usr/local/sbin/build-status-api-portable
if sudo test -e
/etc/systemd/journald@status-api.conf.d/retention.conf.bak; then
sudo mv
/etc/systemd/journald@status-api.conf.d/retention.conf.bak
/etc/systemd/journald@status-api.conf.d/retention.conf
else
sudo rm -f
/etc/systemd/journald@status-api.conf.d/retention.conf
fi
sudo rmdir
/etc/systemd/journald@status-api.conf.d 2>/dev/null || true
sudo journalctl --namespace=status-api --rotate
sudo journalctl --namespace=status-api --vacuum-time=1s
sudo rm -rf -- /srv/status-api
sudo systemctl daemon-reload
sudo systemctl reset-failed
Pachetele debootstrap și systemd-container pot fi eliminate după ce confirmați că nu sunt folosite de alte proiecte:
sudo apt remove --yes debootstrap systemd-container
sudo apt autoremove --yes
Nu eliminați python3-minimal fără să verificați dependențele sistemului Debian.
Bifele care confirmă o livrare completă
- ☐ Gazda rulează Debian 12 și systemd 252.
- ☐
portablectl inspect status-apienumeră cele trei unități. - ☐
status-api.serviceșistatus-api-health.timersuntactive. - ☐ Socketul ascultă numai pe
127.0.0.1:8088. - ☐
/api/statuspublică versiunea imaginii active și JSON-ul gazdei. - ☐
DynamicUser,MemoryMax,TasksMaxșiLogNamespaceapar însystemctl show. - ☐ Health check-ul manual scrie
healthcheck_okîn namespace-ul jurnalului. - ☐ Timerul are o declanșare viitoare în
systemctl list-timers. - ☐ Testul prin tunel SSH reușește fără expunerea portului în rețea.
- ☐
status-api.previousconține ultima versiune funcțională după upgrade.
Următorul proiect logic este semnarea și verificarea artefactului înainte de instalare, astfel încât serverul să accepte numai imaginea produsă de pipeline-ul autorizat. A doua extindere este integrarea eșecurilor status-api-health.service cu sistemul central de alertare, fără a include credențiale în unitate. Pentru mai multe gazde, scriptul și fișierele trebuie păstrate într-un depozit controlat și reconciliate prin mecanismul descris în GitOps pentru detectarea driftului.
Întrebări frecvente
Portable Services pornește un daemon de containere?
Nu. Serviciul este administrat de systemd-ul gazdei, iar tutorialul nu instalează un daemon Docker, Kubernetes sau un registru de imagini.
De ce API-ul nu ascultă direct în rețeaua locală?
Proiectul îl leagă de 127.0.0.1:8088 și permite numai loopback prin politica systemd. Accesul administrativ de pe altă mașină este testat printr-un tunel SSH.
Se păstrează datele când imaginea este înlocuită?
Da. Fișierul status.json rămâne în /srv/status-api pe gazdă și este montat read-only în imagine, deci constructorul poate înlocui arborele portabil fără să-l șteargă.
Cum este restaurată versiunea precedentă?
Înainte de instalare, imaginea activă este mutată în status-api.previous. Dacă atașarea, pornirea sau testul HTTP eșuează, funcția rollback reinstalează automat acea imagine.

















































