Template · 3 warianty · prawdziwe YAML-e
Deploy: wybierz typ aplikacji
Każdy przycisk pokazuje osobny playbook : inny workflow, inna konfiguracja aplikacji i inny przykład nginx. Nic z API/Swagger nie miesza się z frontendem.
Wersja w rozdziałach (wszystkie trzy obok siebie) →
Konwencja nazw (serwery i katalogi)
Auth (tylko API) Quiz (osobny frontend) PPL (wwwroot w repo)
Katalog aplikacji /home/devops/baseauth/backend/home/devops/quiz/backend/home/devops/ppl
Katalog runnera /home/devops/actions-runner/home/devops/actions-runner-quiz/home/devops/actions-runner-ppl
Usługa systemd baseauthquizhubppl
Usługa runnera actions.runner.kandascan-baseauthbackend.bb-Default-stringactions.runner.kandascan-quiz.bb-Default-stringactions.runner.kandascan-ppl.bb-Default-string
Port Kestrel 5007 5006 5080
Domena auth.pepisboczek.ddns.net quiz.pepisboczek.ddns.net ppl.pepisboczek.ddns.net
DLL BaseAuth.Api.dll QuizHub.Api.dll PPL.dll
Dwie dozwolone konwencje katalogu aplikacji:
/home/devops/<app>/backend — Auth, Quiz
/home/devops/<app> — PPL
Runner: jeden self-hosted runner na jedno repo GitHub , katalog /home/devops/actions-runner-<app> (wyjątek: Auth stoi w /home/devops/actions-runner bez przyrostka).
Pracuj jako bb . Usługi i runner działają jako devops . Nie używaj su devops.
Instalacja runnera GitHub Actions
Robisz to raz na nowe repo , zanim pierwszy workflow ma szansę ruszyć. Token bierz z GitHub: repo → Settings → Actions → Runners → New self-hosted runner.
Katalog (jako bb):
sudo mkdir -p /home/devops/actions-runner-<app>
sudo chown -R devops:devops /home/devops/actions-runner-<app>
cd /home/devops/actions-runner-<app>
Archiwum runnera jest już na serwerze: /home/devops/actions-runner-linux-x64-2.336.0.tar.gz (albo pobierz nowsze z GitHuba).
sudo -u devops tar xzf /home/devops/actions-runner-linux-x64-2.336.0.tar.gz -C /home/devops/actions-runner-<app>
Konfiguracja (jako devops, w katalogu runnera). --url to adres repozytorium , nie strony WWW:
sudo -u devops bash -lc 'cd /home/devops/actions-runner-<app> && ./config.sh --url https://github.com/<owner> /<repo> --token <TOKEN> '
Instalacja i start usługi (jako root):
cd /home/devops/actions-runner-<app>
sudo ./svc.sh install devops
sudo ./svc.sh start
systemctl status actions.runner.kandascan-<repo> .bb-Default-string
Nie dodawaj w YAML kroku actions/setup-dotnet. Runner działa jako devops i nie może pisać do /usr/share/dotnet. Używaj systemowego /usr/bin/dotnet. setup-node przy Quizie jest OK — instaluje Node w katalogu joba, nie w systemie.
W YAML zawsze: runs-on: self-hosted (domyślna etykieta po config.sh).
Wybór wariantu
Po kliknięciu widać tylko ten playbook: workflow, konfigurację app, systemd i nginx.
Tylko backend Auth — API + Swagger, bez Node
Osobny frontend Quiz — npm build → wwwroot → publish
Frontend w wwwroot PPL — statyki już w repo, sam publish
Playbook: tylko backend (Auth)
Wzorzec: BaseAuth . Brak frontendu, brak Node. Testujesz Swagger.
Prawdziwy workflow (działa na serwerze)
name: Deploy BaseAuth
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Publish backend
run: |
cd src/BaseAuth.Api
dotnet publish -c Release -o publish
- name: Deploy to server
run: |
DEPLOY_DIR="/home/devops/baseauth/backend"
mkdir -p "$DEPLOY_DIR"
cp -r src/BaseAuth.Api/publish/* "$DEPLOY_DIR/"
- name: Restart BaseAuth service
run: sudo systemctl restart baseauth
Szablon dla nowego API
name: Deploy <NazwaApp>
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Publish backend
run: |
cd src/<Projekt> .Api
dotnet publish -c Release -o publish
- name: Deploy to server
run: |
DEPLOY_DIR="/home/devops/<app> /backend"
mkdir -p "$DEPLOY_DIR"
cp -r src/<Projekt> .Api/publish/* "$DEPLOY_DIR/"
- name: Restart service
run: sudo systemctl restart <app-service>
Tu nie ma Setup Node.js, npm ani kopiowania dist.
Konfiguracja aplikacji (tylko API)
Za nginx zaufaj X-Forwarded-Proto. W Production wyłącz UseHttpsRedirection — inaczej aplikacja zrzuca na port Kestrel i przeglądarka krzyczy „Not secure”.
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
var app = builder.Build();
app.UseForwardedHeaders();
if (!app.Environment.IsProduction())
{
app.UseHttpsRedirection();
}
Na dedykowanej subdomenie nie ustawiaj SwaggerBasePath: "/prod". Swagger ma być na /swagger. Testuj GET /swagger/index.html, nie curl -I (HEAD często 404).
Serwer i systemd (Auth)
sudo mkdir -p /home/devops/<app> /backend
sudo chown -R devops:devops /home/devops/<app>
sudo nano /etc/systemd/system/<app-service> .service
Działający unit BaseAuth:
[Unit]
Description=BaseAuth App
After=network.target
[Service]
Type=simple
User=devops
WorkingDirectory=/home/devops/baseauth/backend
ExecStart=/usr/bin/dotnet /home/devops/baseauth/backend/BaseAuth.Api.dll --urls http://0.0.0.0:5007
Restart=always
RestartSec=5
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable <app-service>
sudo visudo -f /etc/sudoers.d/devops-<app-service>
devops ALL=(ALL) NOPASSWD: /bin/systemctl restart <app-service>
Nginx — reverse proxy (Auth, bez WebSocket)
Tylko location / na port API. Bez /hubs/.
server {
server_name auth.pepisboczek.ddns.net;
location / {
proxy_pass http://127.0.0.1:5007/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/auth.pepisboczek.ddns.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.pepisboczek.ddns.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
server_name auth.pepisboczek.ddns.net;
return 301 https://$host$request_uri;
}
Przed certbotem wystarczy listen 80 + ten sam location /.
SSL
sudo ln -s /etc/nginx/sites-available/<app> /etc/nginx/sites-enabled/<app>
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d <sub> .pepisboczek.ddns.net
curl -s -o /dev/null -w "%{http_code}\n" https://auth.pepisboczek.ddns.net/swagger/index.html
Checklist — tylko backend
YAML: checkout → publish → cp do .../backend → restart — bez Node
ForwardedHeaders + brak HttpsRedirection w Production
Swagger na /swagger, test GET
systemd --urls http://0.0.0.0:<port>
nginx tylko location /
sudoers + runner w actions-runner-<app>
Playbook: osobny frontend (Quiz)
Wzorzec: QuizHub . Frontend to osobny katalog frontend/ (npm). Podczas deployu buduje się na runnerze i wpada do wwwroot backendu . Jedna usługa, jeden nginx. Nie ma osobnego vhosta na SPA. Nie ma Swaggera w tym playbooku — to aplikacja z UI.
Prawdziwy workflow (działa na serwerze)
name: Deploy Quiz
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Build frontend
run: |
cd frontend
npm ci
npm run build
- name: Copy frontend to backend static folder
run: |
mkdir -p backend/QuizHub.Api/wwwroot
cp -r frontend/dist/* backend/QuizHub.Api/wwwroot/
- name: Publish backend
run: |
cd backend/QuizHub.Api
dotnet publish -c Release -o publish
- name: Deploy to server
run: |
DEPLOY_DIR="/home/devops/quiz/backend"
mkdir -p "$DEPLOY_DIR"
cp -r backend/QuizHub.Api/publish/* "$DEPLOY_DIR/"
- name: Restart Quiz service
run: sudo systemctl restart quizhub
Kolejność jest obowiązkowa: npm build → kopia dist do wwwroot → dopiero publish . Odwrócenie kolejności = pusty UI po deploju.
Szablon dla nowej apki z osobnym frontem
name: Deploy <NazwaApp>
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Build frontend
run: |
cd frontend
npm ci
npm run build
- name: Copy frontend to backend static folder
run: |
mkdir -p backend/<Projekt> .Api/wwwroot
cp -r frontend/dist/* backend/<Projekt> .Api/wwwroot/
- name: Publish backend
run: |
cd backend/<Projekt> .Api
dotnet publish -c Release -o publish
- name: Deploy to server
run: |
DEPLOY_DIR="/home/devops/<app> /backend"
mkdir -p "$DEPLOY_DIR"
cp -r backend/<Projekt> .Api/publish/* "$DEPLOY_DIR/"
- run: sudo systemctl restart <app-service>
setup-node zostaw. setup-dotnet nadal nie dodawaj.
Konfiguracja aplikacji (UI + API, bez Swagger-path)
Kestrel serwuje wybuildowany frontend z wwwroot i API. Bind na wszystkich interfejsach:
ExecStart=/usr/bin/dotnet .../QuizHub.Api.dll --urls http://0.0.0.0:5006
W Production nie przekierowuj na HTTPS w Kestrelu (robi to nginx). ForwardedHeaders jak w API, jeśli logujesz schemat/HTTPS za proxy — ale nie konfigurujesz SwaggerBasePath .
Serwer i systemd (Quiz)
sudo mkdir -p /home/devops/quiz/backend
sudo chown -R devops:devops /home/devops/quiz
Działający unit:
[Unit]
Description=QuizHub App
After=network.target
[Service]
Type=simple
User=devops
WorkingDirectory=/home/devops/quiz/backend
ExecStart=/usr/bin/dotnet /home/devops/quiz/backend/QuizHub.Api.dll --urls http://0.0.0.0:5006
Restart=always
RestartSec=5
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
devops ALL=(ALL) NOPASSWD: /bin/systemctl restart quizhub
Nginx — reverse proxy + WebSocket (Quiz)
Oprócz location / jest location /hubs/ (SignalR). Tego nie kopiuj do Auth ani PPL, jeśli nie masz hubów.
server {
server_name quiz.pepisboczek.ddns.net;
location / {
proxy_pass http://127.0.0.1:5006/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /hubs/ {
proxy_pass http://127.0.0.1:5006;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/quiz.pepisboczek.ddns.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/quiz.pepisboczek.ddns.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
server_name quiz.pepisboczek.ddns.net;
return 301 https://$host$request_uri;
}
SSL
sudo certbot --nginx -d quiz.pepisboczek.ddns.net
curl -s -o /dev/null -w "%{http_code}\n" https://quiz.pepisboczek.ddns.net/
Checklist — osobny frontend
Repo: katalogi frontend/ i backend/<Projekt>.Api/
YAML: Node 20 → npm ci/build → cp dist do wwwroot → publish → deploy → restart
Katalog /home/devops/<app>/backend
nginx: / + ewentualnie /hubs/
Bez SwaggerBasePath, bez setup-dotnet
Playbook: frontend już w wwwroot (PPL)
Wzorzec: PPL . HTML/CSS/JS (Vue z wwwroot) są w repo. Nie ma npm, nie ma kroku Node, nie ma Swaggera. Sam dotnet publish pakuje statyki.
Katalog deploy jest płaski : /home/devops/ppl (bez /backend). To druga dozwolona konwencja.
Prawdziwy workflow (działa na serwerze)
name: Deploy PPL
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Publish
run: |
cd PPL
dotnet publish -c Release -o publish
- name: Deploy to server
run: |
DEPLOY_DIR="/home/devops/ppl"
mkdir -p "$DEPLOY_DIR"
cp -r PPL/publish/* "$DEPLOY_DIR/"
- name: Restart PPL service
run: sudo systemctl restart ppl
Tu nie ma Setup Node.js ani kopiowania dist — bo nie ma osobnego frontendu.
Szablon
name: Deploy <NazwaApp>
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Publish
run: |
cd <ProjektDir>
dotnet publish -c Release -o publish
- name: Deploy to server
run: |
DEPLOY_DIR="/home/devops/<app> "
mkdir -p "$DEPLOY_DIR"
cp -r <ProjektDir> /publish/* "$DEPLOY_DIR/"
- run: sudo systemctl restart <app-service>
Jeśli wolisz konwencję z /backend, zmień DEPLOY_DIR i WorkingDirectory razem.
Konfiguracja aplikacji (PPL)
Statyki z wwwroot. Bind przez zmienną środowiska, nie --urls (tak jest na serwerze):
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://0.0.0.0:5080
Nie konfigurujesz Swaggera ani npm. W Production nie zrzucaj HTTPS na Kestrel.
Serwer i systemd (PPL)
sudo mkdir -p /home/devops/ppl
sudo chown -R devops:devops /home/devops/ppl
Działający unit:
[Unit]
Description=PPL Test
After=network.target
[Service]
WorkingDirectory=/home/devops/ppl
ExecStart=/usr/bin/dotnet /home/devops/ppl/PPL.dll
Restart=always
RestartSec=5
User=devops
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://0.0.0.0:5080
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
devops ALL=(ALL) NOPASSWD: /bin/systemctl restart ppl
Nginx — reverse proxy (PPL, bez WebSocket)
server {
server_name ppl.pepisboczek.ddns.net;
location / {
proxy_pass http://127.0.0.1:5080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/ppl.pepisboczek.ddns.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ppl.pepisboczek.ddns.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
server_name ppl.pepisboczek.ddns.net;
return 301 https://$host$request_uri;
}
SSL
sudo certbot --nginx -d ppl.pepisboczek.ddns.net
curl -s -o /dev/null -w "%{http_code}\n" https://ppl.pepisboczek.ddns.net/
Checklist — wwwroot w repo
YAML bez Node: checkout → publish → cp → restart
Katalog /home/devops/<app> (płaski) albo świadomie .../backend
ASPNETCORE_URLS=http://0.0.0.0:<port>
nginx tylko location /
Bez setup-dotnet, bez SwaggerBasePath
Gotchas (wszystkie warianty)
Runner = devops, praca = bb, nigdy su devops.
Bez setup-dotnet (Permission denied na /usr/share/dotnet).
Sudoers: wyłącznie systemctl restart <ta-usługa>.
Testuj GET, nie HEAD.
Kestrel na 0.0.0.0, nie sam localhost.
Playbooki oparte na rzeczywistych plikach: BaseAuth, Quiz, PPL (sierpień 2026).