VPS Debugging Guide
VPS Debugging Guide for Obaki
Section titled “VPS Debugging Guide for Obaki”This guide is a practical reference for debugging a .NET API running on an Ubuntu VPS.
Target setup:
Cloudflare -> Caddy -> Obaki .NET API running through systemd -> database / external APIs / background workersObaki service name:
obakiAPI local port:
127.0.0.1:5000Public API domain example:
api.jjosh.dev1. Debugging mindset
Section titled “1. Debugging mindset”On a PaaS, logs and deployment status are usually built in.
On a VPS, debug by layers:
DNS / Cloudflare -> Caddy -> systemd -> .NET app -> database / external APIs -> background workersCore habit:
1. Check systemd.2. Check journal logs.3. Test local port.4. Test Caddy/domain.5. Check config/env/runtime.6. Only then blame code.2. Check Obaki service status
Section titled “2. Check Obaki service status”sudo systemctl status obaki --no-pagerImportant statuses:
active (running)failedactivating (auto-restart)If it keeps restarting:
journalctl -u obaki -n 100 --no-pager3. Live-tail logs
Section titled “3. Live-tail logs”Live-tail Obaki logs:
journalctl -u obaki -n 100 -fWith timestamps:
journalctl -u obaki -n 100 -f -o short-isoOnly current boot:
journalctl -u obaki -b -fStop live tail:
Ctrl + C4. Check recent logs
Section titled “4. Check recent logs”Last 100 lines:
journalctl -u obaki -n 100 --no-pagerLast 200 lines with timestamps:
journalctl -u obaki -n 200 --no-pager -o short-isoLogs from current boot:
journalctl -u obaki -b --no-pagerLogs since today:
journalctl -u obaki --since today --no-pagerLogs from the last 30 minutes:
journalctl -u obaki --since "30 minutes ago" --no-pagerSearch for errors:
journalctl -u obaki --no-pager | grep -i error5. Restart and watch logs
Section titled “5. Restart and watch logs”sudo systemctl restart obakijournalctl -u obaki -n 100 -fIf the app crashes on startup, the error usually appears immediately.
6. Check Caddy logs
Section titled “6. Check Caddy logs”Caddy live logs:
journalctl -u caddy -n 100 -fRecent Caddy logs:
journalctl -u caddy -n 100 --no-pagerCheck Caddy status:
sudo systemctl status caddy --no-pagerReload Caddy:
sudo systemctl reload caddyValidate Caddy config:
sudo caddy validate --config /etc/caddy/Caddyfile7. Check ntfy logs
Section titled “7. Check ntfy logs”If ntfy is also running on the same VPS:
journalctl -u ntfy -n 100 -fRecent ntfy logs:
journalctl -u ntfy -n 100 --no-pagerCheck ntfy status:
sudo systemctl status ntfy --no-pager8. Check if Obaki is listening
Section titled “8. Check if Obaki is listening”Show listening ports:
sudo ss -tulpnCheck port 5000:
sudo ss -tulpn | grep 5000Expected:
127.0.0.1:5000If nothing listens on port 5000, Obaki is not running or is using a different port.
If it listens on 0.0.0.0:5000, it may be publicly exposed. Prefer 127.0.0.1:5000.
9. Test Obaki locally from the VPS
Section titled “9. Test Obaki locally from the VPS”Test root endpoint:
curl http://127.0.0.1:5000Test health endpoint:
curl http://127.0.0.1:5000/healthIf this fails, Cloudflare and Caddy are not the issue.
10. Test through public domain
Section titled “10. Test through public domain”Test headers:
curl -I https://api.jjosh.devVerbose test:
curl -v https://api.jjosh.devHealth endpoint through Caddy/domain:
curl -v https://api.jjosh.dev/healthInterpretation:
Local 127.0.0.1:5000 works, but domain fails-> Caddy / Cloudflare / DNS / SSL / firewall issue
Local 127.0.0.1:5000 fails-> Obaki app issue11. Check DNS
Section titled “11. Check DNS”From your PC or VPS:
nslookup api.jjosh.devor:
dig api.jjosh.devIf Cloudflare DNS is set to DNS only:
api.jjosh.dev -> your VPS public IPIf Cloudflare proxy is enabled:
api.jjosh.dev -> Cloudflare IPsThat is normal.
12. Check firewall
Section titled “12. Check firewall”Check UFW status:
sudo ufw status verboseUsually allowed:
22/tcp80/tcp443/tcpUsually not exposed directly:
50008080Expected flow:
Public internet -> 80/443 -> Caddy -> 127.0.0.1:5000 for Obaki -> 127.0.0.1:8080 for ntfy13. Check VPS resources
Section titled “13. Check VPS resources”RAM:
free -hDisk:
df -hCPU/process view:
topBetter process viewer:
htopInstall htop:
sudo apt install -y htopCheck dotnet processes:
ps aux | grep dotnetCheck Obaki process:
ps aux | grep obaki14. Check deployment folder
Section titled “14. Check deployment folder”List Obaki folder:
ls -lah /opt/obakiList current release:
ls -lah /opt/obaki/currentList all releases:
ls -lah /opt/obaki/releasesCheck where current points:
readlink -f /opt/obaki/currentCheck DLL files:
ls -lah /opt/obaki/current/*.dllIf systemd uses:
ExecStart=/usr/bin/dotnet /opt/obaki/current/Obaki.Api.dllthen this file must exist:
/opt/obaki/current/Obaki.Api.dll15. Run the app manually
Section titled “15. Run the app manually”Stop the service first:
sudo systemctl stop obakiRun the app as the Obaki Linux user:
sudo -u obaki /usr/bin/dotnet /opt/obaki/current/Obaki.Api.dllThis often shows clearer startup errors.
Start the service again:
sudo systemctl start obaki16. Check systemd service config
Section titled “16. Check systemd service config”Show the full service definition:
sudo systemctl cat obakiRecommended shape:
[Unit]Description=Obaki .NET APIAfter=network.target
[Service]WorkingDirectory=/opt/obaki/currentExecStart=/usr/bin/dotnet /opt/obaki/current/Obaki.Api.dllRestart=alwaysRestartSec=5KillSignal=SIGINTSyslogIdentifier=obakiUser=obaki
EnvironmentFile=/etc/obaki/obaki.envEnvironment=DOTNET_GCHeapHardLimitPercent=40
[Install]WantedBy=multi-user.targetDo not use this unless the app explicitly supports systemd notify:
Type=notify17. Check environment file
Section titled “17. Check environment file”Show env file permissions:
sudo ls -lah /etc/obaki/obaki.envExpected:
-rw-r----- root obaki /etc/obaki/obaki.envFix ownership and permissions:
sudo chown root:obaki /etc/obaki/obaki.envsudo chmod 640 /etc/obaki/obaki.envEnvironment file example:
ASPNETCORE_ENVIRONMENT=ProductionASPNETCORE_URLS=http://127.0.0.1:5000
ConnectionStrings__DefaultConnection=Host=localhost;Port=5432;Database=obaki;Username=obaki;Password=change-this
Resend__ApiKey=re_xxxxxxxxxxxxxxxxx
ExternalProvider__BaseUrl=https://example.com/apiExternalProvider__ApiKey=change-thisDo not log secrets.
Do not commit production .env files.
18. Check .NET runtime
Section titled “18. Check .NET runtime”dotnet --infoList runtimes:
dotnet --list-runtimesFor ASP.NET Core API, you need:
Microsoft.AspNetCore.AppMicrosoft.NETCore.AppIf logs show:
You must install or update .NET to run this application.Framework: 'Microsoft.AspNetCore.App'No frameworks were found.install the ASP.NET Core runtime or publish self-contained.
Example:
sudo apt updatesudo apt install -y aspnetcore-runtime-10.0Then verify:
dotnet --list-runtimesRestart:
sudo systemctl restart obaki19. Add a health endpoint
Section titled “19. Add a health endpoint”Add a simple health endpoint in your .NET API:
app.MapGet("/health", () => Results.Ok(new{ Status = "OK", Time = DateTimeOffset.UtcNow}));Test locally:
curl http://127.0.0.1:5000/healthTest publicly:
curl https://api.jjosh.dev/healthThis tells you:
App alive locallyCaddy routing worksDomain worksSSL works20. Add startup logs
Section titled “20. Add startup logs”At startup, log safe values:
logger.LogInformation("Obaki starting");logger.LogInformation("Environment: {Environment}", app.Environment.EnvironmentName);logger.LogInformation("Content root: {ContentRoot}", app.Environment.ContentRootPath);Never log:
connection stringsAPI keysJWT secretsSMTP passwordsdatabase passwords21. Add background worker logs
Section titled “21. Add background worker logs”For the outbox worker:
logger.LogInformation("Outbox worker tick started.");logger.LogInformation("Outbox worker found {Count} pending messages.", count);logger.LogInformation("Outbox worker completed.");For failures:
logger.LogError(ex, "Outbox worker failed.");For endpoint checker:
logger.LogInformation("Endpoint checker started.");logger.LogInformation("Endpoint checker found new data: {HasNewData}", hasNewData);Since the workers run every 2 minutes, this level of logging is acceptable.
Avoid logging every second.
Avoid dumping full payloads unless needed.
22. Common failure map
Section titled “22. Common failure map”Domain does not resolve
Section titled “Domain does not resolve”Problem:Cloudflare DNS
Check:nslookup api.jjosh.devdig api.jjosh.devHTTPS fails
Section titled “HTTPS fails”Problem:Caddy / SSL / Cloudflare SSL mode
Check:journalctl -u caddy -n 100 --no-pagersudo caddy validate --config /etc/caddy/CaddyfileDomain returns 502
Section titled “Domain returns 502”Problem:Caddy is reachable but cannot reach Obaki
Check:curl http://127.0.0.1:5000/healthsudo ss -tulpn | grep 5000journalctl -u obaki -n 100 --no-pagerLocal curl fails
Section titled “Local curl fails”Problem:Obaki is not running or wrong port
Check:sudo systemctl status obaki --no-pagerjournalctl -u obaki -n 100 --no-pagerService shows activating auto-restart
Section titled “Service shows activating auto-restart”Problem:App is crashing on startup
Check:journalctl -u obaki -n 100 --no-pagersudo -u obaki /usr/bin/dotnet /opt/obaki/current/Obaki.Api.dllMissing framework
Section titled “Missing framework”Problem:.NET runtime missing
Fix:sudo apt install -y aspnetcore-runtime-10.0Permission denied
Section titled “Permission denied”Problem:Wrong file ownership, env permissions, app user permissions, or deploy user permissions
Check:ls -lah /opt/obaki/currentsudo ls -lah /etc/obaki/obaki.envsudo systemctl cat obakiGitHub deploy succeeds but old app still runs
Section titled “GitHub deploy succeeds but old app still runs”Problem:Wrong symlink, wrong ExecStart path, or service did not restart
Check:readlink -f /opt/obaki/currentsudo systemctl status obaki --no-pagerjournalctl -u obaki -n 100 --no-pagerGitHub deploy fails with host key verification
Section titled “GitHub deploy fails with host key verification”Problem:The workflow could not verify the SSH host key. Usually `VPS_HOST` or `VPS_PORT` is wrong, SSH is blocked by the firewall, or `VPS_SSH_KNOWN_HOSTS` has the wrong host key.
Fix:Check `VPS_HOST` and `VPS_PORT`. If you set `VPS_SSH_KNOWN_HOSTS`, make sure it contains a trusted known_hosts line that matches both values.For port 22:
ssh-keyscan -p 22 YOUR_VPS_HOSTFor a custom SSH port, the output should start with:
[YOUR_VPS_HOST]:YOUR_PORTCaddy works but app behavior is wrong
Section titled “Caddy works but app behavior is wrong”Problem:App config, database, external API, or background worker logic
Check:journalctl -u obaki -n 200 --no-pagercurl http://127.0.0.1:5000/health23. Daily VPS debugging cheat sheet
Section titled “23. Daily VPS debugging cheat sheet”sudo systemctl status obaki --no-pagerjournalctl -u obaki -n 100 --no-pagerjournalctl -u obaki -n 100 -fsudo systemctl restart obaki
sudo systemctl status caddy --no-pagerjournalctl -u caddy -n 100 --no-pagersudo caddy validate --config /etc/caddy/Caddyfilesudo systemctl reload caddy
curl http://127.0.0.1:5000/healthcurl -v https://api.jjosh.dev/health
sudo ss -tulpnfree -hdf -hreadlink -f /opt/obaki/currentls -lah /opt/obaki/currentdotnet --list-runtimes24. Recommended minimum observability for Obaki
Section titled “24. Recommended minimum observability for Obaki”Start with:
systemd journalCaddy logshealth endpointstartup logsworker lifecycle logsLater, consider:
SeqGrafana LokiOpenTelemetrySentryBetter StackDo not overbuild observability early.
For the current VPS setup, journalctl plus a health endpoint is enough.
