Det här sätter du upp
Du har en webbapp som fungerar på din laptop. Den här guiden lägger den på en server med HTTPS, så att git push live main från laptopen är allt som behövs för att driftsätta en ny version.
Appen körs under en egen användare, som en systemd-tjänst som startar när servern startar, startas om om den kraschar och inte kan skriva till sin egen kod. Caddy står framför, hämtar HTTPS-certifikatet och skickar varje besökare vidare till appen, som bara lyssnar på servern. Ingen Docker och ingen processhanterare: Debians egna verktyg räcker.
Välj appens språk. Varje steg som skiljer sig visar det du väljer:
Med Node.js
Exemplet är en liten Express-app. Node.js utvecklas av sina bidragsgivare som ett projekt inom OpenJS Foundation och är öppen källkod under MIT-licensen. Paketen kommer från NodeSources paketförråd, deb.nodesource.com, som servern kontaktar vid varje apt update. npm hämtar appens beroenden från registry.npmjs.org och kollar där som standard också efter en nyare npm, vilket driftsättningen nedan stänger av.
Varje steg nedan har körts på en nyinstallerad Melonslab VC-P Alloy (2 vCPU, 8 GB) med Debian 13:
- En
git pushfrån en dator någon annanstans driftsatte en ny version på ungefär 3 sekunder, och en push där beroendena inte gick att installera lämnade den körande versionen orörd. - Under varje driftsättning fick en slinga med 20 förfrågningar i sekunden via Caddy svar på varenda en: över 220 i rad, ingen misslyckades.
- Appen såg varje besökares riktiga adress över IPv4 och IPv6, och en förfalskad
X-Forwarded-For-header utifrån ignorerades. - Den härdade tjänsten kunde inte skriva till sin egen kod och inte läsa sin fil med hemligheter, men kunde fortfarande anropa andra HTTPS-webbplatser.
- Appen, Caddy och brandväggen startade igen av sig själva efter en omstart av servern, och en uppdatering av Node.js installerades av sig själv via Debians automatiska uppdateringar.
Exempelappen i Node.js använde omkring 70 MB minne, Python-appen omkring 90 MB för gunicorn med två workers, och Caddy omkring 20 MB.
Innan du börjar
Du behöver:
- en server med Debian 13, uppsatt som i säkerhetsguiden: inloggning med nyckel, automatiska säkerhetsuppdateringar och ufw;
- en A-post och en AAAA-post för
app.example.comsom pekar på din server; - inget annat på port 80 och 443 på servern, eftersom Caddy tar dem;
- din app i ett Git-repo på din egen dator.
Exemplen använder app.example.com för appen, 203.0.113.10 för servern, 198.51.100.7 för din egen adress och myapp för appens användare. Byt ut dem genomgående.
1. Installera körmiljön
Med Node.js
Debian 13 levereras med Node.js 20, som Node.js-projektet slutade stödja i april 2026. NodeSource bygger varje versionsserie av Node.js som Debian-paket, så du får den aktuella versionen med långtidsstöd, 24, och dess uppdateringar kommer via apt som allt annat. En versionshanterare som nvm installerar Node.js i en användares hemkatalog och uppdaterar det aldrig av sig själv: bra på en laptop, inte på en server.
Som root lägger du till NodeSources signeringsnyckel och paketförråd:
apt update
apt install -y curl
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /etc/apt/keyrings/nodesource.asc
Skapa /etc/apt/sources.list.d/nodesource.sources:
Types: deb
URIs: https://deb.nodesource.com/node_24.x
Suites: nodistro
Components: main
Signed-By: /etc/apt/keyrings/nodesource.asc
Installera Node.js, som har npm med sig, och Caddy och Git från Debian:
apt update
apt install -y nodejs caddy git
node -v
v24.21.0
Debians automatiska uppdateringar installerar bara från Debian. För att de också ska installera uppdateringar av Node.js skapar du /etc/apt/apt.conf.d/51unattended-upgrades-nodesource:
Unattended-Upgrade::Origins-Pattern {
"site=deb.nodesource.com";
};
Kontrollera att filen läses in:
unattended-upgrade --dry-run --debug 2>&1 | grep 'Allowed origins'
Raden slutar med site=deb.nodesource.com.
2. Skapa appens användare
Appen får en egen användare, och koden ligger i användarens hemkatalog. Användarens skal är git-shell, så nyckeln som kan pusha kod dit kan inte göra något annat: inget skal, inga andra kommandon.
Som root:
useradd -m -s /usr/bin/git-shell myapp
install -d -m 700 -o myapp -g myapp /home/myapp/.ssh
install -m 600 -o myapp -g myapp /root/.ssh/authorized_keys /home/myapp/.ssh/authorized_keys
runuser -u myapp -- git init --bare -b main /home/myapp/app.git
runuser -u myapp -- mkdir /home/myapp/app
Andra och tredje raden låter nycklarna som loggar in som root pusha kod som myapp. app.git är ett bart repo: det tar emot dina pushar. app innehåller den utcheckade koden som körs.
3. Förbered appen
Tre saker i appen gör att den passar in här: den lyssnar bara på 127.0.0.1, så att inget når den utom via Caddy; den litar på besökarens adress som Caddy skickar vidare; och den gör klart öppna förfrågningar när den ombeds stänga.
Med Node.js
Exempelappen, server.js:
const express = require('express');
const app = express();
// Caddy on this server passes the visitor's address in X-Forwarded-For.
app.set('trust proxy', 'loopback');
app.get('/', (req, res) => {
res.send(`Hello from Node.js ${process.version}. Your address: ${req.ip}, via ${req.protocol}\n`);
});
const port = process.env.PORT || 3000;
const server = app.listen(port, '127.0.0.1', () => {
console.log(`Listening on 127.0.0.1:${port}`);
});
// On a restart, finish the requests that are open before exiting.
process.on('SIGTERM', () => {
console.log('SIGTERM: finishing open requests');
server.close(() => process.exit(0));
});
Med trust proxy satt till loopback blir req.ip besökarens adress när förfrågan kommer från Caddy på samma server, och headern ignoreras om den kommer från något annat håll. npm ci på servern installerar exakt det som står i package-lock.json, så checka in den filen.
4. Lägg hemligheter i en miljöfil
Lösenord och API-nycklar hålls utanför Git, i en fil som bara root kan läsa. systemd läser den när appen startar och skickar värdena vidare som miljövariabler. Som root:
Med Node.js
install -m 600 /dev/null /etc/myapp.env
printf 'PORT=3000\nSESSION_SECRET=%s\n' "$(openssl rand -hex 32)" >> /etc/myapp.env
Lägg till egna rader i samma form, NAMN=värde. Appen läser dem som vanligt (process.env.NAMN i Node.js, os.environ["NAMN"] i Python), men användaren myapp kan inte öppna själva filen.
5. Kör appen som en tjänst
Med Node.js
Skapa /etc/systemd/system/myapp.service:
[Unit]
Description=myapp (Node.js)
After=network.target
[Service]
User=myapp
Group=myapp
WorkingDirectory=/home/myapp/app
EnvironmentFile=/etc/myapp.env
Environment=NODE_ENV=production
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=2
# Hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
LockPersonality=yes
CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service
UMask=0077
[Install]
WantedBy=multi-user.target
Hoppa över MemoryDenyWriteExecute=yes, som finns med i andra listor över härdning: Node.js kompilerar JavaScript till maskinkod medan det körs och kraschar direkt med den.
Restart=on-failure startar appen igen om den kraschar: när den dödades med flit var den tillbaka 2 sekunder senare. Härdningen tar inget som appen behöver, och systemd-analyze security myapp ger tjänsten 1,7 till 1,8 i stället för 9,2 av 10, där lägre betyder mindre exponerad:
ProtectSystem=strictochProtectHome=read-onlygör hela filsystemet skrivskyddat för appen, dess egen kod inräknad, så att ett säkerhetshål i appen inte kan plantera kod som överlever en omstart. Om appen behöver skriva filer skapar du en katalog som/home/myapp/datasom ägs avmyappoch lägger tillReadWritePaths=/home/myapp/data.NoNewPrivileges, den tommaCapabilityBoundingSetochSystemCallFilterhindrar appen från att skaffa sig fler rättigheter eller använda systemanrop som är till för administration.RestrictAddressFamiliestillåter fortfarande vanliga nätverksanslutningar, så appen kan anropa API:er på internet.
Aktivera tjänsten utan att starta den än, eftersom det inte finns någon kod förrän efter första pushen:
systemctl daemon-reload
systemctl enable myapp
6. Driftsätt med git push
En hook i det bara repot körs efter varje push till main: den checkar ut koden, installerar beroendena och startar om appen.
För att hooken ska kunna starta om appen utan root låter du myapp köra exakt ett kommando. Skapa /etc/sudoers.d/myapp:
myapp ALL=(root) NOPASSWD: /usr/bin/systemctl reload-or-restart myapp.service
chmod 440 /etc/sudoers.d/myapp
visudo -c
visudo -c slutar med /etc/sudoers.d/myapp: parsed OK.
Med Node.js
Skapa /home/myapp/app.git/hooks/post-receive:
#!/bin/sh
# Deploy every push to main: check out the code, install dependencies, restart.
set -e
while read -r old new ref; do
[ "$ref" = refs/heads/main ] || continue
git --work-tree="$HOME/app" checkout -f -q main
cd "$HOME/app"
npm ci --omit=dev --no-audit --no-fund --no-update-notifier
sudo systemctl reload-or-restart myapp.service
echo "Deployed $new"
done
chown myapp:myapp /home/myapp/app.git/hooks/post-receive
chmod 755 /home/myapp/app.git/hooks/post-receive
reload-or-restart startar appen vid första pushen och laddar om eller startar om den efter det. set -e stoppar hooken vid första felet, så om beroendena inte går att installera fortsätter den körande versionen som förut.
Lägg nu till servern som remote på din egen dator, i appens repo, och pusha:
git remote add live myapp@app.example.com:app.git
git push live main
Med Node.js
remote: added 68 packages in 2s
remote: Deployed 6f3dbefd446c5710ef8d159e346792a021949777
To app.example.com:app.git
* [new branch] main -> main
Om din gren heter master pushar du den med git push live master:main. Från och med nu är git push live main hela driftsättningen.
Kontrollera på servern att appen svarar lokalt:
Med Node.js
systemctl is-active myapp
curl -s http://127.0.0.1:3000/
active
Hello from Node.js v24.21.0. Your address: 127.0.0.1, via http
7. Ställ Caddy framför
Ersätt /etc/caddy/Caddyfile med det här. E-postadressen är den som Let's Encrypt skriver till om ett certifikat får problem:
Med Node.js
{
email you@example.com
}
app.example.com {
reverse_proxy 127.0.0.1:3000 {
# While the app restarts, hold requests for up to 5 seconds instead of answering 502.
lb_try_duration 5s
}
}
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
Caddy hämtar certifikatet inom några sekunder, och journalctl -u caddy | grep 'certificate obtained' visar det. Fler appar på samma server får varsitt eget block, med eget namn och egen port.
Brandväggen behöver ha port 80 och 443 öppna, och HTTP/3 använder UDP 443. Om du följde säkerhetsguiden listar ufw status dem redan. Annars:
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw allow 443/udp
ufw enable
Öppna inte appens egen port: den lyssnar på 127.0.0.1 och går inte att nå utifrån i alla fall.
8. Kontrollera besökarens adress
På din egen dator:
curl https://app.example.com/
Med Node.js
Hello from Node.js v24.21.0. Your address: 198.51.100.7, via https
Din egen adress och https betyder att appen litar på Caddys headrar. Över IPv6 visar appen din IPv6-adress. Försök nu förfalska headern:
curl -H 'X-Forwarded-For: 192.0.2.1' https://app.example.com/
Appen visar fortfarande din riktiga adress: Caddy ersätter headern som kommer utifrån i stället för att skicka den vidare.
9. Läs loggarna
Allt som appen skriver ut hamnar i journalen. Som root:
journalctl -u myapp -f
-f följer nya rader när de kommer, Ctrl+C avbryter. journalctl -u myapp --since today visar dagens rader och journalctl -u myapp -b allt sedan senaste uppstarten, driftsättningar och krascher inräknade. journalctl -u caddy innehåller Caddys loggar, certifikaten inräknade.
10. Driftsätt utan avbrott
En omstart stoppar appen och startar den igen. Mellan de två lyssnar inget på porten, och Caddy svarar 502 Bad Gateway. För att mäta det skickade vi 20 förfrågningar i sekunden via Caddy under driftsättningen.
Med Node.js
En ensam Node.js-process kan inte lämna över sin port till en ny, så varje driftsättning är en omstart. Det du kan göra är att göra den osynlig:
- Utan
lb_try_durationi Caddyfile fick 8 av 228 förfrågningar502under en driftsättning: ett glapp på ungefär 0,4 sekunder. - Med
lb_try_duration 5shåller Caddy kvar nya förfrågningar tills den nya processen lyssnar: 222 av 222 fick svar, ingen misslyckades. SIGTERM-hanteraren iserver.jslåter förfrågningar som redan pågår bli klara. Utan den avslutas Node.js direkt: en POST-förfrågan på 3 sekunder som skickades strax före en omstart fick502. Caddy skickar själv om en avbruten GET, men inte en POST. Med hanteraren blev samma POST klar som vanligt.
Om appen håller anslutningar öppna länge, som WebSockets eller server-sent events, väntar server.close på dem, och systemd avslutar processen efter 90 sekunder, sitt standardvärde (inte testat här). De anslutningarna bryts vid varje driftsättning, och klienten måste ansluta igen.
Flera processer som turas om skulle stänga även det sista glappet, men det kräver en processhanterare eller en andra tjänst och lastbalansering, vilket är mer än de flesta appar behöver.
För att mäta själv kör du det här på servern medan du pushar från din egen dator. Det räknar svaren per statuskod:
for i in $(seq 300); do curl -s -o /dev/null -w '%{http_code}\n' https://app.example.com/ & sleep 0.05; done | sort | uniq -c
11. Testa en omstart
systemctl reboot
Logga in igen och kontrollera:
systemctl is-active myapp caddy
curl -s https://app.example.com/
Båda är active, och appen svarar via Caddy. ufw startar av sig själv med sina regler.
12. Uppdatera körmiljön
Med Node.js
Uppdateringar inom Node.js 24 installeras av sig själva, tillsammans med Debians, tack vare filen från steg 1. Den körande appen behåller den gamla versionen tills den startas om. Så här ser du om den behöver det:
ls -l /proc/$(systemctl show -p MainPID --value myapp)/exe
lrwxrwxrwx 1 myapp myapp 0 Oct 2 19:05 /proc/17446/exe -> /usr/bin/node (deleted)
(deleted) betyder att en nyare Node.js är installerad. Starta om appen:
systemctl restart myapp
För att gå över till nästa serie med långtidsstöd, till exempel 26, ändrar du node_24.x till node_26.x i /etc/apt/sources.list.d/nodesource.sources och kör:
apt update
apt install -y nodejs
Pusha sedan en tom commit från din egen dator, så att npm ci installerar om beroendena för den nya versionen och appen startas om:
git commit --allow-empty -m "Redeploy on Node.js 26"
git push live main
Bytet från 24 till 26 och tillbaka testades på det här sättet.
Felsökning
Pushen skriver ut sudo: unable to resolve host och ett servernamn. Serverns namn saknas i /etc/hosts. Driftsättningen fungerar ändå. Som root rättar du det med sed -i "s/^127\.0\.1\.1.*/127.0.1.1 $(hostname)/" /etc/hosts.
Pushen tas emot, men raden Deployed saknas och webbplatsen visar den gamla versionen. Något i hooken misslyckades, oftast installationen av beroendena, och raderna ovanför visar felet. Den gamla versionen fortsätter att köras, men den nya koden är redan utcheckad, så rätta felet och pusha igen innan appen startas om av någon annan anledning.
fatal: Interactive git shell is not enabled. Du loggade in som myapp med ssh. Den användaren tar bara emot Git. Logga in som root för allt annat.
Webbplatsen svarar 502 Bad Gateway i mer än några sekunder. Appen körs inte. systemctl status myapp och journalctl -u myapp -n 50 visar varför. En app som kraschar om och om igen syns som restart counter is at med ett stigande tal.
Appen kraschar med Read-only file system. Den försöker skriva någon annanstans än i /tmp. Ge den en katalog med ReadWritePaths=, som i steg 5, och kör sedan systemctl daemon-reload och systemctl restart myapp.
gunicorn loggar Control server error: [Errno 30] Read-only file system: '/home/myapp/.gunicorn'. --no-control-socket saknas i ExecStart. gunicorn 26 öppnar en kontrollsocket i användarens hemkatalog, som tjänsten inte kan skriva till.
Appen ser varje besökare som 127.0.0.1. Den litar inte på Caddys headrar: trust proxy i Express, ProxyFix i Flask, som i steg 3.