Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Error finder: look up self-hosting error messages

Container won't start, certificate missing, mail stuck? Search your error message – every answer comes from a setup we got running ourselves.

Every card here comes from a tutorial on this site – from an error we actually ran into while setting things up on the test server. Type the error message (address already in use, NXDOMAIN, Unauthorized) or describe the symptom in your own words.

264 findings from real setups

The logs say „middleware … does not exist" and the service answers with 404.

Almost always the provider suffix is missing: a middleware defined in the file provider is called name@file from the perspective of Docker labels. Without the suffix Traefik looks for it among the Docker labels – and finds nothing. The same applies the other way round for name@docker.
Read on: Hardening Traefik →

BasicAuth rejects the correct password.

The $ characters in the bcrypt hash were interpreted as variables by Docker Compose. In Compose they have to be doubled ($$2y$$05$$…). docker compose config shows the value that really arrives.
Read on: Hardening Traefik →

The rate limit doesn’t seem to work.

Sequential curl calls are too slow: every process opens a new connection, so you stay below the limit. Test in parallel, e.g. with seq 40 | xargs -P 8 -I{} curl … – then the 429s appear.
Read on: Hardening Traefik →

After setting the IP allowlist you get 403 yourself.

Traefik does not see your IP but that of the proxy in front of it (Cloudflare, load balancer). Check with traefik/whoami what X-Forwarded-For contains, and use ipallowlist.ipstrategy.depth for multi-stage setups.
Read on: Hardening Traefik →

A v2 configuration with ipWhiteList stops working after the upgrade.

In Traefik v3 the middleware is called ipAllowList; the old name was removed, so the rule no longer applies. Rename every occurrence when migrating.
Read on: Hardening Traefik →

The browser still refuses plain HTTP even though you fixed the configuration.

That is HSTS, not a bug: the browser remembers max-age and enforces HTTPS even when your server no longer offers it. A private window or a different browser helps for testing – and that is why the warning about preload exists above.
Read on: Hardening Traefik →

Traefik answers with 404 although the container is healthy.

Usually traefik.http.services.dockge.loadbalancer.server.port=5001 is missing. Inside the container Dockge listens on 5001; without that line Traefik guesses. Also check that the container is attached to the proxy network.
Read on: Dockge: Manage Compose Stacks in the Browser →

The deployment aborts with „no such file or directory".

The host and container paths of the stacks directory do not match. It must be mounted as /opt/stacks:/opt/stacks with DOCKGE_STACKS_DIR=/opt/stacks – Dockge has the host’s Docker daemon do the work, and that daemon only knows host paths.
Read on: Dockge: Manage Compose Stacks in the Browser →

A stack is visible but all buttons are missing.

Then it lives outside /opt/stacks and Dockge shows „This stack is not managed by Dockge." Move the folder as in step 7 – Dockge neither adopts it by itself nor makes a copy.
Read on: Dockge: Manage Compose Stacks in the Browser →

The stack name is rejected.

Only lowercase letters, digits and hyphens are allowed, because they become the folder and Compose project name. My App won’t do, my-app will.
Read on: Dockge: Manage Compose Stacks in the Browser →

After a restart the login is gone.

Then ./data was not mounted persistently – that is where the database and JWT secret live. Check that /opt/dockge/data exists and is bound in the Compose file.
Read on: Dockge: Manage Compose Stacks in the Browser →

The interface is in the wrong language.

Dockge follows the browser and falls back to English for unknown combinations; the settings let you pin the language.
Read on: Dockge: Manage Compose Stacks in the Browser →

The library stays empty after you create it.

The first scan does not run automatically – that is not a bug but how Audiobookshelf behaves. Trigger the scan in the library view. If it stays empty afterwards, check with docker compose exec audiobookshelf ls -R /audiobooks whether the container sees the files at all, and with ls -ln /opt/audiobookshelf/audiobooks that everything is owned by UID 1000.
Read on: Audiobookshelf: Self-Host Audiobooks & Podcasts →

Four MP3 files become four audiobooks instead of four chapters.

The files sit directly in the author folder instead of a shared title folder. Audiobookshelf groups by Author/Title/ – move the files into a subfolder named after the book and scan again.
Read on: Audiobookshelf: Self-Host Audiobooks & Podcasts →

Traefik answers with 404 even though the container is running.

Usually traefik.http.services.abs.loadbalancer.server.port=80 is missing. Inside the container Audiobookshelf listens on 80; without that line Traefik guesses and misses. Also check that the container really is attached to the proxy network.
Read on: Audiobookshelf: Self-Host Audiobooks & Podcasts →

The address jumps to /audiobookshelf/.

That is normal: the frontend ships with this fixed router path, and ROUTER_BASE_PATH changes nothing at runtime because the path is baked into the image. https://YOUR_DOMAIN remains a valid entry point.
Read on: Audiobookshelf: Self-Host Audiobooks & Podcasts →

After switching devices the language is back to English.

The language choice lives locally in the browser, not on the account. Set it once per device via your username → Language.
Read on: Audiobookshelf: Self-Host Audiobooks & Podcasts →

The podcast is not found.

The search queries the iTunes catalogue; a typo or a missing entry there leads to „no search results". Use the podcast’s RSS URL instead – the same field accepts both.
Read on: Audiobookshelf: Self-Host Audiobooks & Podcasts →

Redirect loop (ERR_TOO_MANY_REDIRECTS) or mixed-content warnings.

The HTTPS detection is missing. Check that the WORDPRESS_CONFIG_EXTRA block with HTTP_X_FORWARDED_PROTO and the $$ signs is set exactly (see the warning in step 2), and restart the WordPress container.
Read on: Running WordPress Cleanly with Docker & Traefik →

WordPress won’t start, reports “Error establishing a database connection”.

Usually a race condition or a password mismatch. Check that depends_on: condition: service_healthy is set and WORDPRESS_DB_PASSWORD matches MARIADB_PASSWORD exactly. Logs: docker compose logs db.
Read on: Running WordPress Cleanly with Docker & Traefik →

The first start fails even though everything runs later.

The database needs a few seconds to initialize on the very first start. The health check covers this; without it, another docker compose up -d helps.
Read on: Running WordPress Cleanly with Docker & Traefik →

Media uploads fail for large files.

PHP limits the upload size. Create your own uploads.ini and mount it to /usr/local/etc/php/conf.d/ with e.g. upload_max_filesize = 64M and post_max_size = 64M.
Read on: Running WordPress Cleanly with Docker & Traefik →

After a domain change the site points nowhere.

WP_HOME/WP_SITEURL are hard-wired in the Compose file – for a new domain, change them there and restart the container (the values override the database setting).
Read on: Running WordPress Cleanly with Docker & Traefik →

Login fails with a CSRF error.

LD_CSRF_TRUSTED_ORIGINS: https://YOUR_DOMAIN is missing or wrong (must be given with https:// and no path). Add it and docker compose up -d (see the warning in step 1).
Read on: linkding: Self-Host Your Bookmarks →

The page doesn’t load (Traefik 404) even though the container is running.

The health check isn’t healthy yet – Traefik deliberately doesn’t route then. Wait about 30 seconds after start; check the status with docker inspect -f '{{.State.Health.Status}}' linkding-linkding-1.
Read on: linkding: Self-Host Your Bookmarks →

Title/description aren’t fetched automatically.

The target page blocks automated access or responds too slowly. You can then simply enter title and description by hand – the fields are in the same form.
Read on: linkding: Self-Host Your Bookmarks →

The admin account wasn’t created.

LD_SUPERUSER_NAME/LD_SUPERUSER_PASSWORD only take effect on the first start with an empty database. If the data database already exists, a later change does nothing. Create the account afterwards: docker compose exec linkding python manage.py createsuperuser.
Read on: linkding: Self-Host Your Bookmarks →

The extension won’t connect.

Almost always the API token or the URL. Regenerate the token under Settings → Integrations and enter exactly https://YOUR_DOMAIN as the server.
Read on: linkding: Self-Host Your Bookmarks →

The library stays empty.

Either there are no files in the mounted music folder, or the permissions are wrong. Check: docker compose logs | grep -i scan shows tracksImported. If it’s 0, check the path and that the music folder is owned by UID 1000 (see Users & permissions).
Read on: Navidrome: Stream Your Own Music Like Spotify →

Tracks appear unsorted or without cover art.

The files’ metadata is incomplete. Navidrome sorts by tags, not by filenames – re-tag the files (e.g. with Picard) and let Navidrome rescan.
Read on: Navidrome: Stream Your Own Music Like Spotify →

New music doesn’t show up.

The scan only runs hourly (ND_SCANNER_SCHEDULE). Trigger an immediate scan via the refresh icon at the top of the web interface, or wait for the next interval.
Read on: Navidrome: Stream Your Own Music Like Spotify →

The app won’t connect.

Almost always the server URL: it must be https://YOUR_DOMAIN (with https://, no path). Also check that username/password are exactly right – some apps additionally require enabling Subsonic compatibility, which is on by default in Navidrome.
Read on: Navidrome: Stream Your Own Music Like Spotify →

Playback stutters on large FLAC files over mobile.

The bandwidth isn’t enough for the original. Enable transcoding for the mobile player (see the tip in step 7).
Read on: Navidrome: Stream Your Own Music Like Spotify →

Container won’t start: “unable to open database file”.

DB_NAME: /data/hc.sqlite is missing or the data folder isn’t owned by UID 1000. Set the path and chown -R 1000:1000 /opt/healthchecks/data (see the warning in step 2).
Read on: Healthchecks setup →

The page responds with HTTP 500.

Most common cause: a non-ASCII character in SITE_NAME (or another text environment variable). Switch to pure ASCII and restart. To diagnose, temporarily set DEBUG: "True" – but switch it back to False afterwards.
Read on: Healthchecks setup →

Login fails / CSRF error on submit.

CSRF_TRUSTED_ORIGINS: https://YOUR_DOMAIN must be set – Django otherwise rejects POST requests behind the reverse proxy. And ALLOWED_HOSTS must contain your domain exactly.
Read on: Healthchecks setup →

The check won’t turn “green” even though the job runs.

Check whether the curl ping actually runs and hits the right UUID: curl -v https://YOUR_DOMAIN/ping/UUID should return OK. On the detail page the log shows whether and from which IP pings arrive.
Read on: Healthchecks setup →

I get no notification on “down”.

No integration channel is assigned, or (for email) the SMTP settings are missing. Set up a channel under “Integrations” and assign it to the check.
Read on: Healthchecks setup →

The page doesn’t load (Traefik 404) even though the container is running.

The health check isn’t healthy yet – Traefik deliberately doesn’t route then. Wait 20–30 seconds after start; check the status with docker inspect -f '{{.State.Health.Status}}' stirling-stirling-pdf-1.
Read on: Stirling-PDF: The PDF Toolbox on Your Own Server →

Login with admin/stirling fails.

Either the password was already changed (then use the new one), or the config in the configs volume is inconsistent. To reset, stop the app and check the user config in the configs folder; if in doubt clear the folder (note: this resets all settings).
Read on: Stirling-PDF: The PDF Toolbox on Your Own Server →

OCR can’t find my language.

The matching .traineddata is missing from the tessdata volume. Get the file from the Tesseract language packs and place it in the data folder (see step 5).
Read on: Stirling-PDF: The PDF Toolbox on Your Own Server →

A conversion (e.g. Office → PDF) fails.

Such conversions need extra tools (LibreOffice) that are only included in the larger image variants. For the full feature set use the -fat variant of the image (stirlingtools/stirling-pdf:2.14.2-fat).
Read on: Stirling-PDF: The PDF Toolbox on Your Own Server →

Large files cause errors or long waits.

OCR and conversion are memory-intensive. On a small VPS RAM can run short – then either process smaller files or move to a larger product.
Read on: Stirling-PDF: The PDF Toolbox on Your Own Server →

The container won’t start / restart loop with permission denied.

The permissions on the config or data folder are wrong. chown -R 1000:1000 /opt/syncthing/config /opt/syncthing/data and restart (see the warning in step 1).
Read on: Syncthing setup →

The interface gives a Traefik 404 even though the container is running.

The health check isn’t healthy yet – Traefik deliberately doesn’t route then. Wait up to 60 seconds after start and check the status (see the warning in step 3).
Read on: Syncthing setup →

Two devices won’t connect.

Check three things: is port 22000 (TCP+UDP) open in the firewall? Did you enter the ID on both devices (pairing is mutual)? And is the ID exact (typos are caught easily thanks to a built-in checksum)? A “Disconnected” state with correctly entered IDs almost always points to the firewall port.
Read on: Syncthing setup →

A folder is stuck at “Syncing 0%”.

Usually the share confirmation is missing on the other side, or the folder ID doesn’t match – it must be identical on both devices. Also check the write permissions in the target folder.
Read on: Syncthing setup →

No access after setting the GUI password.

Forgot the password? You can reset it in config.xml (<gui> block) by removing the <user> and <password> lines and restarting Syncthing – then the interface is reachable without a login again (and you set it anew immediately).
Read on: Syncthing setup →

The wizard reports write-permission errors on the data folder.

The container runs as www-data (UID 33). With a bind-mounted ./data the permissions must match: chown -R 33:33 /opt/freshrss/data (see Users & permissions).
Read on: FreshRSS: Your Own RSS Reader →

Feeds don’t refresh automatically.

Check whether CRON_MIN is set and the internal cron is running: docker compose logs freshrss | grep -i cron. You can trigger a refresh anytime with the refresh button at the top right or docker compose exec -u www-data freshrss ./cli/actualize-user.php --user admin.
Read on: FreshRSS: Your Own RSS Reader →

A feed stays empty / is marked “inactive”.

The source doesn’t serve valid RSS/Atom, or the URL is an HTML page instead of the feed. Many sites hide the feed – search the page source for application/rss+xml. Under “Statistics → Inactive feeds” you’ll find problem sources gathered.
Read on: FreshRSS: Your Own RSS Reader →

Login fails even though the password is correct.

FreshRSS encrypts the password in the browser (JavaScript). If JavaScript is disabled or the connection isn’t really HTTPS (certificate warning), the login fails. Via Traefik HTTPS is guaranteed – check the certificate if in doubt.
Read on: FreshRSS: Your Own RSS Reader →

Articles are missing after an update.

FreshRSS purges old articles after a configurable period (default: a few weeks). That’s intentional and keeps the database small; raise the retention per feed in its settings if needed.
Read on: FreshRSS: Your Own RSS Reader →

systemctl start fails, status shows status=203/EXEC.

systemd couldn’t execute the program from ExecStart; the journal says Unable to locate executable. Either the path is wrong, or the file is missing its executable bit (chmod +x). Without a leading /, systemd only searches /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin (check with systemd-path search-binaries-default) – so your own script under /opt or /root needs an absolute path.
Read on: Understanding systemd →

systemd warns “The unit file, source configuration file or drop-ins of … changed on disk”.

You edited a unit file, but systemd doesn’t pick up changes automatically. After every change under /etc/systemd/system/: systemctl daemon-reload.
Read on: Understanding systemd →

The timer doesn’t show up in list-timers.

It wasn’t enabled. systemctl enable --now SERVICE.timer – and remember the timer triggers the service, so you need both files (.service and .timer).
Read on: Understanding systemd →

systemctl enable answers “The unit files have no installation config”.

The unit is missing the [Install] section with WantedBy=; in systemctl status you spot it by the static in the Loaded: line. Without [Install] systemd doesn’t know when to start the unit automatically – for timers it needs WantedBy=timers.target.
Read on: Understanding systemd →

The journal is huge / eats disk.

By default it grows in bounds, but you can trim it: journalctl --vacuum-time=14d deletes entries older than 14 days, journalctl --disk-usage shows the consumption.
Read on: Understanding systemd →

“Host validation failed” instead of the dashboard.

The most common trap: HOMEPAGE_ALLOWED_HOSTS is missing or has the wrong domain. Enter exactly the domain you access it under, then docker compose up -d.
Read on: Homepage dashboard setup →

The resource widgets show no or wrong values.

Homepage reads CPU and RAM from within the container (no socket needed). If disk: / shows the container filesystem instead of the host disk, mount the desired path as an additional volume (e.g. - /:/host:ro and disk: /host).
Read on: Homepage dashboard setup →

An icon doesn’t load.

The name doesn’t match the icon catalog. Write the service name lowercase and without spaces (nextcloud.png), or place a custom image in the icons folder (mounted to /app/public/icons) and reference it as /icons/name.png.
Read on: Homepage dashboard setup →

The container tile shows no status.

The value at container: must be the exact container name. Find it with docker ps --format '{{.Names}}' – with Compose it’s usually folder-service-1 (e.g. traefik-traefik-1).
Read on: Homepage dashboard setup →

Changes to the YAML files don’t take effect.

A syntax error aborts the parsing. YAML is indentation-sensitive – check the logs with docker compose logs homepage for error lines and use consistent spaces (no tabs).
Read on: Homepage dashboard setup →

You get “Authentication error – Auth is disabled or misconfigured” instead of the sign-in form.

Then HOMEPAGE_AUTH_ENABLED is set but the secret or the password arrives empty in the container – usually because the .env sits in the wrong folder or is missing a line. The log says Password auth is enabled but required settings are missing; docker compose config shows you which values Compose actually substitutes.
Read on: Homepage dashboard setup →

The container stays unhealthy.

Almost always the IPv6 trap from step 4: the health check queries localhost but nginx only listens on IPv4. Switch to http://127.0.0.1/. Check with docker inspect --format '{{.State.Health.Status}}' CONTAINER.
Read on: Host your website with Hugo →

Traefik shows 404 page not found (instead of your site).

That’s the Traefik 404, not the nginx one – Traefik finds no matching router. Most common causes: the container isn’t healthy yet, the Host() rule has the wrong domain, or the proxy network isn’t external. See the 502/404 chapter in Understanding Docker networks.
Read on: Host your website with Hugo →

All links and images are broken.

The baseURL in hugo.toml doesn’t match the real domain. Hugo bakes absolute URLs based on this value – fix it and rebuild.
Read on: Host your website with Hugo →

A post doesn’t appear.

Check the date in the front matter: if it’s in the future, Hugo won’t build the post (see the warning box in step 3). A draft: true also hides content in a normal build.
Read on: Host your website with Hugo →

theme "PaperMod" not found during the build.

The theme folder is missing from the build context – usually because it’s a Git submodule that wasn’t copied, or a .dockerignore excludes it. Store the theme as a real folder as in step 1.
Read on: Host your website with Hugo →

Container can’t reach another by name (bad address).

Both aren’t on the same named network, or one uses the default bridge (no DNS). Check which networks a container is on with docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' CONTAINER.
Read on: Understanding Docker networks →

Traefik returns 502 Bad Gateway.

Almost always Traefik and the app are on different proxy networks because the network was created twice. Make sure external: true is set in Compose and check with docker network inspect proxy whether both containers are listed.
Read on: Understanding Docker networks →

network proxy declared as external, but could not be found on up.

The external network doesn’t exist yet. Create it once: docker network create --ipv6 --subnet fd00:cafe::/64 proxy. Do not let Traefik create the network itself – it then comes up without IPv6, and behind the proxy IPv6 visitors show up as the gateway address instead of their real IP.
Read on: Understanding Docker networks →

Database has internet despite internal.

It’s also attached to a non-internal network (e.g. proxy). A container has the sum of the access of all its networks – the DB belongs only on the internal network, never on the proxy network.
Read on: Understanding Docker networks →

Two stacks collide on the subnet.

Docker assigns 172.x ranges automatically, but with many networks the pool can run short (could not find an available, non-overlapping IPv4 address pool). Clean up unused networks: docker network prune.
Read on: Understanding Docker networks →

Permission denied even though the permissions look right.

Check the permissions of the parent directory: if it’s missing the x bit, you can’t reach the file at all, no matter what its own permissions are. ls -ld /path/to/folder shows it.
Read on: Understanding Linux Users, Groups & File Permissions →

docker ps still says “permission denied” after usermod -aG docker.

The new group isn’t active in the current session yet – log out and back in (see the warning box in step 7). id in the new session must show docker.
Read on: Understanding Linux Users, Groups & File Permissions →

New files have unexpected permissions.

That’s set by the umask – it subtracts permissions from the defaults. The Debian default umask 0022 produces 644 for files and 755 for directories. Check it with the umask command (no argument).
Read on: Understanding Linux Users, Groups & File Permissions →

A container won’t write to the volume.

The process inside the container runs under a specific UID (often not root). Set the owner of the host directory to match: chown -R 1000:1000 ./data – the image’s docs name the correct UID (environment variables like PUID/PGID).
Read on: Understanding Linux Users, Groups & File Permissions →

usermod dropped the user from groups.

You used -G without -a. -G replaces the secondary groups. Always use usermod -aG. Repair: re-add the missing groups with usermod -aG group1,group2 user.
Read on: Understanding Linux Users, Groups & File Permissions →

The search returns nonsense after the model switch.

The photos are still (or partially) indexed with the old model – old and new vectors don’t mix. Start the All run under Job Queues → Smart Search and wait until the queue is empty. Exactly this mixed state also occurs if you save the setting but forget the re-index.
Read on: Optimizing Immich (Part 2) →

With an nllb-… model the search is no better than before.

Known behaviour in v3.1.0: the language hint these models expect cannot be set in Immich at all (see the warning box in step 3). Switch to an XLM-… or SigLIP2 model and re-index.
Read on: Optimizing Immich (Part 2) →

The ML container exits with exit 137 after the model switch.

Out of memory: the new model no longer fits next to the server, the database and your other services. Either pick a smaller model or add RAM – the XLM base model really uses around 2 GB for the ML container alone.
Read on: Optimizing Immich (Part 2) →

The external library stays empty after the scan.

Almost always the path is wrong: the import path must be the container path (/mnt/archive), not the host path. Check with docker compose exec immich-server ls /mnt/archive whether the container sees the files at all – if nothing shows up, the volume entry is missing or you skipped docker compose up -d after the change.
Read on: Optimizing Immich (Part 2) →

The phone only backs up while the app is open.

On Android, battery optimization kills the background worker (exempt the app, see step 1); on iOS, Background App Refresh is off or iOS deprioritizes the app – opening it more often genuinely helps. And: by default, backup runs on Wi-Fi only – photos taken on the go are only “missing” until you’re back home.
Read on: Optimizing Immich (Part 2) →

Browser shows NET::ERR_CERT_AUTHORITY_INVALID, curl says self-signed certificate.

The server is serving Traefik’s built-in fallback certificate (TRAEFIK DEFAULT CERT) because issuance failed. Almost always: the A/AAAA record points to the wrong IP (check with dig YOUR_DOMAIN) or port 80 is closed in the firewall – the HTTP-01 challenge never arrives. The exact cause is in the Traefik logs: docker compose logs traefik | grep -i acme.
Read on: How HTTPS works →

The issuer says (STAGING) Ersatz Emmer YR2.

Let’s Encrypt’s test CA is still active – browsers distrust it on purpose. Remove the staging line from the Traefik configuration, empty acme.json, restart the container (details in the Traefik tutorial).
Read on: How HTTPS works →

certificate has expired.

Automatic renewal has been failing for at least 30 days – same list of causes as the first error, just long unnoticed. If the message appears on one device only: check that device’s system clock, TLS is time-sensitive.
Read on: How HTTPS works →

Let’s Encrypt reports too many certificates already issued.

Rate limit hit, usually from trial-and-error loops against the production CA. Switch to the staging CA until your setup works – and back up acme.json instead of re-issuing certificates.
Read on: How HTTPS works →

unable to get local issuer certificate on an old client.

The system is missing the root certificate from step 3 – its root store is outdated. On Debian: apt update && apt install ca-certificates.
Read on: How HTTPS works →

The container won’t start, the log says “listen udp :53: bind: address already in use”.

systemd-resolved (or another DNS service) occupies port 53. disable the stub listener as in step 1, then docker compose up -d again.
Read on: AdGuard Home: network-wide ad and tracking blocker →

docker compose up fails with “cannot assign requested address” for 10.8.0.1.

the WireGuard interface wg0 with 10.8.0.1 doesn’t exist (yet) – Docker can’t bind the port to a non-existent address. first set up the WireGuard VPN and bring wg0 up (ip -br addr show wg0 must show 10.8.0.1), then start the container.
Read on: AdGuard Home: network-wide ad and tracking blocker →

After the setup, the web interface is no longer reachable.

in the wizard, the admin port was set to 80 (the suggestion) instead of 3000 – but Traefik forwards to 3000. in ./conf/AdGuardHome.yaml under http: correct the address to 0.0.0.0:3000 and docker compose restart.
Read on: AdGuard Home: network-wide ad and tracking blocker →

DNS doesn’t filter, even though the device is connected.

the browser uses DNS-over-HTTPS (DoH) and thus bypasses your server completely – Firefox and Chrome have this active by default in part. disable “Secure DNS” / “DNS over HTTPS” in the browser settings. You can check with dig @10.8.0.1 … (always takes effect).
Read on: AdGuard Home: network-wide ad and tracking blocker →

A website is suddenly broken (empty pages, missing images, no login).

overblocking – a block list blocks a domain the site really needs. find the blocked domain in the query log, allow it via right-click/menu (exception) or disable the overly aggressive list.
Read on: AdGuard Home: network-wide ad and tracking blocker →

The runner restarts and reports “cannot ping the docker daemon … certificate is valid for docker, …, not fjr-docker”.

The DinD service is named something other than docker, but its TLS certificate is issued for docker. name the DinD service exactly docker (as above) and address it via DOCKER_HOST: tcp://docker:2376 – then the name matches the certificate.
Read on: Forgejo Actions: your own CI/CD runner with Docker →

The job starts but fails at actions/checkout with a connection error.

The runner was registered with an internal instance URL (http://forgejo:3000). The job containers in DinD can’t resolve this name. re-register with the public URL https://YOUR_DOMAIN (delete the .runner file in the volume first, or recreate the volume).
Read on: Forgejo Actions: your own CI/CD runner with Docker →

The runner doesn’t appear in the overview at all / the registration fails.

Wrong or already-used token, or the runner can’t reach Forgejo. get a fresh token (step 1) and check that the runner container reaches https://YOUR_DOMAIN (docker compose run --rm runner wget -qO- https://YOUR_DOMAIN/api/healthz).
Read on: Forgejo Actions: your own CI/CD runner with Docker →

A job stays “pending” forever.

No runner has a matching label. The workflow uses runs-on: docker, so the runner must carry the label docker. check the labels when registering; the runner overview shows the labels per runner.
Read on: Forgejo Actions: your own CI/CD runner with Docker →

actions/checkout can’t find the action.

Forgejo loads actions from a configured registry (by default data.forgejo.org). If the server is completely cut off from the internet, that fails. allow outbound HTTPS access or mirror actions in an internal registry.
Read on: Forgejo Actions: your own CI/CD runner with Docker →

mail-tester shows “reverse DNS does not match”.

The PTR entry is missing, not propagated yet, or doesn’t match the A record. Set the PTR in the provider panel to mail.YOUR_DOMAIN and check with dig -x; make sure mail.YOUR_DOMAIN points forward to the same IP.
Read on: Email deliverability →

DKIM fails (DKIM: FAIL or “no signature”).

The DNS record was adopted incorrectly (genuinely truncated key, wrong selector) or not propagated yet. Copy the value exactly from the mail server UI, match the selector in the record (SELECTOR._domainkey) with the one configured in the server, then counter-check with dig. That dig shows the key as two quoted blocks is normal and not an error – see step 3.
Read on: Email deliverability →

SPF “permerror” or “too many DNS lookups”.

Several SPF records, or too many nested include: (limit: 10 DNS lookups). Check with dig +short TXT YOUR_DOMAIN | grep spf1 that really only one line comes back, consolidate to one SPF record and remove unnecessary include:.
Read on: Email deliverability →

Mails to Outlook/Hotmail land in spam or get rejected.

If you get a hard rejection with 550 5.7.515 Access denied, sending domain … does not meet the required authentication level, it’s not a reputation problem but Microsoft’s authentication rule from 5,000 mails per day – SPF and DKIM must pass and an aligned DMARC record (at least p=none) must exist; the threshold sticks to the domain even if you later send less. If the mails only land in the junk folder, Microsoft is simply strict with new IPs: patience, send little but regularly, and if needed use Microsoft’s SNDS/JMRP program.
Read on: Email deliverability →

Your IP is on a block list.

The IP was with a spammer before you, or a mailbox of yours sends spam. Check on the common block-list checkers, and on a legitimate hit have it removed via the respective delisting form – and fix the cause (compromised account).
Read on: Email deliverability →

The web interface on port 8080 doesn’t respond.

The container is still booting or port 8080 is occupied/blocked. Check docker compose ps, read docker compose logs and make sure the firewall lets port 8080 (and later 443) through. If the log output stays completely empty after the setup, the log destination is still set to Log file – switch it to Console in the console under Settings → Telemetry → Tracers and restart.
Read on: Stalwart mail server →

I missed the bootstrap password.

It’s only logged once. Set a STALWART_RECOVERY_ADMIN=admin:YOUR_PASSWORD under environment: in the compose.yaml and restart with docker compose up -d – that gives you a fixed recovery-admin account.
Read on: Stalwart mail server →

No TLS certificate, the HTTPS address shows a warning.

Either Let’s Encrypt can’t reach your server on port 443 (Stalwart uses the TLS-ALPN-01 challenge over 443), or one of the five hostnames from step 1 is missing in DNS. The log names the culprit: ACME authentication error … NXDOMAIN looking up A for autoconfig.YOUR_DOMAIN. Add the missing records, open port 443 in the firewall – Stalwart retries the certificate request automatically afterwards.
Read on: Stalwart mail server →

Mails to the outside stay stuck, logs show timeouts on port 25.

Your provider blocks outbound SMTP traffic. At netcup the default policy “netcup Mail block” (ports 25/465/587) is to blame: in the SCP under Firewall → Policies, take that netcup template off the server – no ticket needed, see netcup firewall setup. Without it, no delivery to other servers is possible.
Read on: Stalwart mail server →

Other servers don’t accept your mails or they land in spam.

Missing or wrong PTR entry, no SPF/DKIM/DMARC. That’s not a Stalwart error but a matter of DNS/reputation configuration – see the email deliverability tutorial.
Read on: Stalwart mail server →

generate_config.sh aborts with Cannot find command 'jq'.

the jq package is missing. sudo apt install -y jq and start the generator again.
Read on: Your own mail server with Mailcow: setup from scratch →

The generator reports “User declined to create daemon.json” and aborts.

On hosts with active IPv6 you answered the question about the IPv6-capable Docker configuration with n – without it mailcow won’t run there, and an open relay looms. Create /etc/docker/daemon.json containing {"ipv6": true} (Docker 28 and newer), restart Docker with sudo systemctl restart docker and run ./generate_config.sh again – or simply confirm the question with Enter on the second attempt, then the generator does both itself.
Read on: Your own mail server with Mailcow: setup from scratch →

The webmail only answers Unauthorized.

SOGo doesn’t know your mail domain: it reads the domain list exclusively at startup and wasn’t restarted after the domain was created – its logs say No authentication sources defined - nobody will be able to login. Restart SOGo via E-Mail → Restart SOGo. If that doesn’t help, a failed login is still stuck in the cache: docker compose restart memcached-mailcow sogo-mailcow.
Read on: Your own mail server with Mailcow: setup from scratch →

No certificate, acme-mailcow keeps restarting.

Let’s Encrypt can’t reach your server on port 80, or the A record of mail.YOUR_DOMAIN is wrong. check with docker compose logs acme-mailcow whether the hostname and IP match, and that port 80 is open through UFW and the netcup firewall.
Read on: Your own mail server with Mailcow: setup from scratch →

Mails to the outside aren’t delivered, logs show timeouts on port 25.

Your provider blocks outbound SMTP traffic (common as spam protection). At netcup it’s the default policy “netcup Mail block” (ports 25/465/587): in the SCP under Firewall → Policies, take that netcup template off the server – no support ticket needed, details in netcup firewall setup. Without it you can’t send mail to other servers – receiving and local delivery still work.
Read on: Your own mail server with Mailcow: setup from scratch →

Ports 80/443 can’t be bound (address already in use).

A web server or reverse proxy (e.g. Traefik) already runs on the host. For this recipe, mailcow belongs on its own server. Alternatively, bind its nginx to a local port with HTTP_BIND/HTTPS_BIND and HTTP_PORT/HTTPS_PORT and put mailcow behind the proxy – that’s what mailcow’s Traefik guide describes. The mail ports stay directly on the host either way.
Read on: Your own mail server with Mailcow: setup from scratch →

Containers start slowly or are killed by the kernel (OOM).

Too little RAM. go to at least 6–8 GB or disable ClamAV in mailcow.conf (SKIP_CLAMD=y) – the virus scanner is the biggest memory eater.
Read on: Your own mail server with Mailcow: setup from scratch →

The app reports “Cannot connect” or the web client stays empty.

The NTFY_BASE_URL doesn’t match the called address. it must be exactly your public HTTPS URL (https://ntfy.YOUR_DOMAIN, without a trailing slash). After a change, run docker compose up -d again.
Read on: ntfy setup →

curl returns HTTP 401 or 403.

With deny-all, missing or wrong credentials are the most common error. check the user/password (docker exec ntfy ntfy user list) and, when sending, supply -u USER:PASSWORD or the bearer token. A 403 means the user exists but has no rights to this topic – then grant rights with ntfy access.
Read on: ntfy setup →

Messages arrive in the browser but not as push on the phone when the tab is closed.

browser push needs granted notification rights and an active service worker; that’s unreliable once the tab is closed. for real “on the go” push, use the ntfy app – it keeps the connection in the background.
Read on: ntfy setup →

502 Bad Gateway from Traefik.

the container isn’t ready yet or listens on the wrong port. check docker compose logs ntfy and that the label loadbalancer.server.port=80 is set – ntfy listens on port 80 in the container (NTFY_LISTEN_HTTP=":80").
Read on: ntfy setup →

429 Too Many Requests with many messages in quick succession.

ntfy limits the rate per sender by default to prevent abuse. On a private server with your own scripts you rarely hit this – but a loop script without a pause does. bundle messages instead of firing them every second, or raise the limits specifically via the NTFY_VISITOR_* environment variables (described in the ntfy docs under “Rate limiting”). NTFY_BEHIND_PROXY: "true" is a prerequisite so the limit applies per real IP instead of per Traefik container.
Read on: ntfy setup →

The container writes, but the bind-mount folder on the host stays empty.

You used a relative path that points somewhere other than intended, or Docker created the path anew as an empty folder. with docker run, always give bind mounts an absolute path (/opt/app/config, not config). If this host path doesn’t exist yet, Docker silently creates it as an empty folder – so check with ls that you really hit the right one. In the compose.yaml, ./ paths are perfectly fine, on the other hand – they’re relative to the compose.yaml and thus unambiguous. Note also: docker run -v config:/data (without / or ./ in front) is not a bind mount, but creates a named volume called config.
Read on: Docker volumes vs. bind mounts →

Permission denied as soon as the container tries to write into the mount.

The process in the container runs under a different UID than the owner of the host folder – typical with bind mounts. with named volumes this rarely happens (Docker sets the permissions). With bind mounts, give the folder to the matching user (chown -R 1000:1000 /opt/app/data) or use the user: setting of the Compose in the image.
Read on: Docker volumes vs. bind mounts →

After docker compose down all data is gone.

You used docker compose down -v – the -v deletes the named volumes too. for a normal restart, work without -v. Use -v deliberately only when you really want to start from scratch.
Read on: Docker volumes vs. bind mounts →

docker system prune deleted data.

docker volume prune or docker system prune --volumes removes volumes that no container is currently attached to. With docker volume prune, named volumes are protected by default – only anonymous volumes are deleted; named ones only go with --all/-a. (With docker system prune, on the other hand, -a/--all controls the images, not the named volumes.) another strong argument for naming – an sk-demo-vol survives an accidental docker volume prune, an anonymous volume doesn’t. Still, only run prune with volumes when all important stacks are active, or remove specifically with docker volume rm.
Read on: Docker volumes vs. bind mounts →

The browser shows a certificate error or 404 page not found.

Traefik hasn’t fetched the Let’s Encrypt certificate yet, or the DNS record doesn’t point to the server. Check with dig stats.YOUR_DOMAIN that the IP is correct, and look at the Traefik logs. You find the exact container name from your Traefik setup with docker ps | grep traefik, then docker logs <container-name> (for us e.g. docker logs traefik-traefik-1). The HTTP challenge fails if port 80 isn’t reachable from outside – check your firewall and the netcup firewall.
Read on: Self-hosting Matomo →

The installer reports SQLSTATE... Connection refused or hangs at the database.

MariaDB wasn’t ready on the first start. Give the DB a moment and reload the page. Check with docker compose logs db whether it says ready for connections. If an access error appears instead, the DB_PASSWORD in the .env and the already-created DB no longer match – then a clean restart with docker compose down -v (careful: deletes the data) and docker compose up -d helps.
Read on: Self-hosting Matomo →

Warning “It looks like the trusted_hosts setting is not correct”.

Matomo checks, for security reasons, under which hostname it’s called. The warning appears when you change the domain. Confirm the correct hostname via the button in the message – Matomo then enters it in config/config.ini.php.
Read on: Self-hosting Matomo →

No visits appear in the dashboard.

The tracking code is missing, incorrectly embedded, or you’re visiting your own site (Matomo ignores you if your IP is excluded). Open your website in a private window and check in the network tab whether a request to matomo.php goes out. In Matomo, Administration → Diagnostic → Tracking failures helps.
Read on: Self-hosting Matomo →

The dashboard loads very slowly.

The on-the-fly archiving computes on every call. Set up the archiving cron from step 8 and switch report generation to cron.
Read on: Self-hosting Matomo →

The container restarts repeatedly (Restarting), the log says bind: address already in use.

The built-in SSH server collides with itself because SSH_PORT and SSH_LISTEN_PORT don’t match. Set both to the same value (here 2222) – then Forgejo starts cleanly.
Read on: Forgejo: your own Git server behind Traefik →

The container takes forever to become healthy.

By default, Docker runs the first healthcheck only after the interval (30 s) – so the container looks “unhealthy” for 30 s+, even though Forgejo has long been ready in ~2 s. The solution is already in the Compose above: start_interval: 2s checks every 2 seconds during the startup phase and switches to healthy as soon as the app responds. (Requires Docker 25+ / Compose v2.20+ – given on Debian 13.)
Read on: Forgejo: your own Git server behind Traefik →

Traefik returns 502 Bad Gateway.

Almost always the wrong port: Forgejo’s web interface listens internally on 3000, so loadbalancer.server.port=3000 must be set and the container must be on the proxy network.
Read on: Forgejo: your own Git server behind Traefik →

Clone links show localhost or the wrong port.

Then ROOT_URL, SSH_DOMAIN or SSH_PORT are wrong. Correct the values in the Compose and restart with docker compose up -d.
Read on: Forgejo: your own Git server behind Traefik →

SSH clone fails with Permission denied (publickey).

The SSH server is running, but your public key isn’t stored in the account yet (step 6) – or you forgot the port 2222.
Read on: Forgejo: your own Git server behind Traefik →

The live verifier stays on “Waiting” / no hits in the dashboard.

Check in the browser (dev tools → Network) whether hk.js is loaded at all and the send request afterwards comes back with status 2xx. Most common causes: the snippet isn’t in the HTML, the site domain created in HitKeep doesn’t match the hostname of the visited page, or an ad/tracking blocker filters the call. Since you host under your own domain (first-party), most blockers don’t apply – but some lists know the path hk.js.
Read on: HitKeep: self-host privacy-friendly web analytics →

All visitors seemingly come from a single IP, country and provider show “(Unknown)”.

Then HITKEEP_TRUSTED_PROXIES isn’t taking effect: the range you gave doesn’t contain your proxy’s IP, and HitKeep only evaluates Traefik’s container IP. Check which subnet the proxy sits in with docker network inspect proxy – 172.16.0.0/12 covers the usual Docker networks – and restart the stack (docker compose up -d). After that the real client IP from the X-Forwarded-For header counts again.
Read on: HitKeep: self-host privacy-friendly web analytics →

Traefik returns 404 or 502.

A 404 usually means the router rule isn’t matching – check that Host(...) contains your real domain and the container is on the proxy network. A 502 indicates the wrong port: HitKeep listens internally on 8080, so loadbalancer.server.port=8080 must be set.
Read on: HitKeep: self-host privacy-friendly web analytics →

After login you land on the login page again (login loop).

That’s almost always a mismatch in HITKEEP_PUBLIC_URL: the value must match exactly the address through which you call HitKeep (including https://, without a trailing slash). Correct the variable and restart the container.
Read on: HitKeep: self-host privacy-friendly web analytics →

The container won’t start or isn’t healthy.

Look at the logs: docker compose logs -f hitkeep. A missing or empty HITKEEP_JWT_SECRET is a typical start blocker – generate one as in step 1 and enter it.
Read on: HitKeep: self-host privacy-friendly web analytics →

A latest handshake never appears in wg show.

The UDP port isn’t reachable or the keys don’t match. WireGuard is silent with wrong keys – there’s no error message, just no handshake. Check: sudo ufw status (port 51820/udp open?), the netcup firewall (inbound UDP 51820), the Endpoint in the client (correct public IP?) and that the private and public keys aren’t swapped.
Read on: Setting up a WireGuard VPN →

Handshake is there, but no internet arrives in the full tunnel.

Almost always the routing/NAT. Is net.ipv4.ip_forward=1 set (sysctl net.ipv4.ip_forward)? If Docker is running – or UFW is active – the FORWARD DROP trap from the warning box above applies; both set the forward policy to DROP. The rules must sit before the existing chains with -I FORWARD 1. Is the external interface in the MASQUERADE correct (eth0 vs. something else)?
Read on: Setting up a WireGuard VPN →

Full tunnel is up, but DNS queries still run past the tunnel (DNS leak).

Without a DNS = line, your device keeps querying the DNS servers of your local network – on open Wi-Fi the operator still sees which domains you visit. On Linux, check with resolvectl status which DNS server is assigned to the wg0 interface, or run a leak test (e.g. on dnsleaktest.com): if foreign resolvers appear there, enter a real resolver in the client config (DNS = 1.1.1.1) – or the server itself (10.8.0.1) if a dedicated DNS resolver runs there.
Read on: Setting up a WireGuard VPN →

The tunnel is up, but large transfers (SSH, HTTPS, downloads) hang or break off.

An MTU problem. wg-quick sets the MTU to 1420; in some networks (DS-Lite, certain mobile networks) that’s still too high. Try MTU = 1412 or 1280 in the [Interface] section of the client.
Read on: Setting up a WireGuard VPN →

wg-quick up reports resolvconf: command not found.

The DNS = line needs resolvconf. Either install sudo apt install openresolv or remove the DNS line if you don’t need the VPN DNS.
Read on: Setting up a WireGuard VPN →

The connection falls asleep as soon as the phone briefly sends nothing.

The client sits behind NAT/CGNAT. PersistentKeepalive = 25 in the client config keeps the connection open.
Read on: Setting up a WireGuard VPN →

Your figures are well below ours, especially for the CPU.

A VPS shares the physical CPU. Check the steal time (vmstat 1, column st) and top (line %st). If it’s persistently high, neighbors on the same host are computing right now. Measure again at a different time of day – often the difference is gone then.
Read on: netcup VPS 1000 G12 benchmarked: how fast is it really? →

The disk figures are absurdly high (e.g. “10 GB/s random read”).

You’re missing --direct=1 – then fio measures the RAM cache, not the NVMe. Always test with direct I/O, otherwise the numbers are worthless.
Read on: netcup VPS 1000 G12 benchmarked: how fast is it really? →

lsblk shows ROTA=1 (“rotational”) for vda in the column – is that a hard disk instead of NVMe?

No. That’s a virtualization artifact: the virtio driver reports the virtual disk as rotational across the board. The measured 100k+ IOPS and 4 GB/s prove that real flash storage is behind it.
Read on: netcup VPS 1000 G12 benchmarked: how fast is it really? →

The download is much slower than 2 Gbit/s.

Measure against a nearby, fast server (e.g. Falkenstein). A distant target or a slow counterpart limits the measurement, not your VPS. A single curl stream also doesn’t always exhaust the full bandwidth.
Read on: netcup VPS 1000 G12 benchmarked: how fast is it really? →

Every run delivers different numbers.

Normal – benchmarks fluctuate. Measure multiple times, discard the first (“warm”) run and take the median. Also compare only the same tool versions and parameters with each other.
Read on: netcup VPS 1000 G12 benchmarked: how fast is it really? →

docker compose pull pulls no new image, even though a new version exists.

Your tag points to a fixed version (:1.37.2) or to a major tag (:18), under which there’d only be a new major. pull only fetches what the same tag now points to. For a version jump you have to bump the tag in the compose.yaml yourself.
Read on: Keeping your whole Docker stack safely up to date →

The app no longer starts after the update or throws database errors.

Usually a breaking change or a failed migration. Check docker compose logs, compare with the release notes. Set the tag back to the old version and up -d; if the new version already migrated the data, restore the backup from step 3.
Read on: Keeping your whole Docker stack safely up to date →

Diun reports nothing, even though updates exist.

Check that the containers carry the label diun.enable=true and Diun can read the socket (DIUN_PROVIDERS_DOCKER=true, socket mounted). For newer version tags the container additionally needs diun.watch_repo=true – without it, Diun only sees changes to the exactly pinned tag. And: whatever Diun sees for the first time is only written to the database, not reported (watch.firstCheckNotif is false) – to test the notification path, set DIUN_WATCH_FIRSTCHECKNOTIF=true once.
Read on: Keeping your whole Docker stack safely up to date →

The disk fills up, even though you regularly run docker image prune.

docker image prune only clears dangling images, not the old tagged versions. Remove them specifically with docker rmi <image>:<tag> or – with care – docker image prune -a. The build cache (docker builder prune) can grow too.
Read on: Keeping your whole Docker stack safely up to date →

On pulling you get toomanyrequests / a Docker Hub rate limit.

diun.watch_repo=true on many images queries many tags. Set the watch interval less often (e.g. once daily) and narrow with diun.include_tags to relevant versions instead of scanning the whole repo.
Read on: Keeping your whole Docker stack safely up to date →

A freshly set ban doesn’t take effect, the IP keeps getting 200.

The bouncer caches the decisions and, in stream mode, pulls new ones only after the pull interval (up to ~60 seconds). Wait briefly. Whoever wants to reduce the reaction time lowers updateIntervalSeconds in the middleware (step 3).
Read on: CrowdSec: modern, collaborative intrusion prevention →

cscli metrics show acquisition shows no line for the log file.

CrowdSec isn’t reading the logs – and because cscli omits empty tables, the line is missing entirely instead of showing Lines read: 0. Most common cause: the container started before the access.log existed; then docker logs crowdsec contains the line No matching files for pattern /var/log/traefik/access.log, and CrowdSec won’t pick the file up by itself later – cd ~/crowdsec && docker compose restart fixes it. Otherwise check that Traefik really writes to /var/log/traefik/access.log (step 1), that both containers mount the same host path, and that the acquis.yaml names exactly this path.
Read on: CrowdSec: modern, collaborative intrusion prevention →

After restarting Traefik every app returns 403 – including for you.

The bouncer couldn’t reach the LAPI at startup and then blocks in doubt instead of letting traffic through unprotected. The Traefik log says crowdsecQuery:unreachable with the URL http://crowdsec:8080/v1/decisions/stream. Check with docker ps that the crowdsec container is Up – as soon as the next pull succeeds (up to ~60 seconds), requests go through again on their own. That’s safe, but makes CrowdSec a critical component: if the container isn’t running, your site is affected. restart: unless-stopped (set above) brings it back automatically after a server reboot.
Read on: CrowdSec: modern, collaborative intrusion prevention →

CrowdSec bans lots of harmless or no real visitors – the detected IP is always your CDN’s.

If a CDN like Cloudflare sits in front of Traefik, Traefik only sees its IP. Then have the real client IP read from the X-Forwarded-For header: fill forwardedHeadersTrustedIPs in the middleware with the CDN networks – otherwise you end up banning the CDN. Directly at netcup (without a CDN) there’s nothing to do here.
Read on: CrowdSec: modern, collaborative intrusion prevention →

docker compose up -d on Traefik reports a plugin error.

Traefik downloads the plugin from the net on startup. Check that the server may go out (the netcup firewall only blocks SMTP outbound, UFW nothing) and that modulename and version are exactly right.
Read on: CrowdSec: modern, collaborative intrusion prevention →

CrowdSec bans nothing even though the attacks are in access.log – or it bans 172.18.0.1.

Then Traefik never sees the real source address in the first place. This happens with IPv6 visitors when the proxy network was created without IPv6: Docker pushes the connection through a helper process and replaces the source address with the bridge gateway’s. Every IPv6 request then carries the same 172.x.x.x in access.log, and a ban on that either hits nobody or everybody. Recreate the network with --ipv6; the instructions are in Reverse proxy with Traefik.
Read on: CrowdSec: modern, collaborative intrusion prevention →

dd overwrote the wrong disk – data gone.

dd doesn’t ask and isn’t mockingly called “disk destroyer” for nothing. Check the target (of=) always beforehand with lsblk, and never confuse sda with sdb. There’s no undo – when in doubt, read it three times.
Read on: The essential terminal commands for your server →

kill -9 ended a service, but the database is corrupted afterwards.

-9 gives the process no chance to close cleanly. Always first use kill without -9 and give the process a few seconds. Only when it really hangs does the hard variant follow.
Read on: The essential terminal commands for your server →

rsync copied (or deleted) much more than expected.

The trailing slash decides: rsync -a /source/ copies the contents of source, rsync -a /source copies the folder along with it. And --delete deletes everything in the target that’s missing in the source. Test with -n first, always.
Read on: The essential terminal commands for your server →

A tool reports “command not found”.

The package isn’t installed. Install it (see tip box above) or check the name. Some tools like iotop additionally need sudo to see any data at all.
Read on: The essential terminal commands for your server →

After tmux, on the next login “everything is gone”.

You started a new session instead of attaching. tmux ls lists running sessions, tmux attach -t 0 attaches you to the first. Just tmux on its own creates a fresh one every time.
Read on: The essential terminal commands for your server →

A target is at up = 0 or “DOWN” in Prometheus.

Prometheus can’t reach the exporter. For cadvisor, check that it’s on the same monitoring network and the name and port are exactly right (cadvisor:8080). For the node job, check the extra_hosts entry on the Prometheus service and the target host.docker.internal:9100 – without both, Prometheus doesn’t find the node-exporter in the host network. After changes to the prometheus.yml, restart the Prometheus container: docker compose restart prometheus.
Read on: Monitoring with Grafana & Prometheus →

The cAdvisor dashboard partly shows “No data” or cAdvisor won’t start.

If the mounts or privileged: true are missing, cAdvisor doesn’t see the containers. Check the cadvisor block (mounts, devices: /dev/kmsg). Individual “No data” panels are normal – some metrics (like CPU throttling) only exist if you’ve set CPU limits on the containers.
Read on: Monitoring with Grafana & Prometheus →

Grafana doesn’t load correctly behind Traefik – login fails or the layout is broken.

Almost always GF_SERVER_ROOT_URL is wrong. It must be exactly the public address (https://grafana.YOUR_DOMAIN). Also check that the Traefik label loadbalancer.server.port=3000 is set – Grafana listens on 3000 internally.
Read on: Monitoring with Grafana & Prometheus →

Grafana shows “No data” in the panels, even though the targets are up.

Usually the time range. A fresh stack has no history yet – set “Last 15 minutes” at the top right and wait a few minutes for data to accumulate. Also check that the Prometheus data source was selected on dashboard import.
Read on: Monitoring with Grafana & Prometheus →

The disk space on the server grows steadily.

That’s the Prometheus TSDB. Reduce --storage.tsdb.retention.time (e.g. to 15d) or monitor fewer targets. The prom_data volume grows with the number of metrics × retention time.
Read on: Monitoring with Grafana & Prometheus →

502 Bad Gateway on access, even though the container is running.

Traefik reaches the container but hits the wrong port. Jellyfin listens on 8096 – check the label traefik.http.services.jellyfin.loadbalancer.server.port=8096 for typos. If a 504 Gateway Timeout comes back after a few seconds instead, the port isn’t the problem, the network is: then networks: [proxy] is missing and Traefik is running against an address it can’t reach at all.
Read on: Self-hosting Jellyfin →

The libraries stay empty, even though files are there.

Almost always a permission problem. The container runs as 1000:1000 (step 4), so the media must belong to that user: sudo chown -R 1000:1000 /mnt/media. And: in Jellyfin the container path must be entered (/media/filme), not the host path. Afterwards start “Scan all libraries” under Dashboard → Scheduled Tasks.
Read on: Self-hosting Jellyfin →

Video stutters/buffers, docker stats shows Jellyfin at ~100% CPU.

It’s transcoding. Under Dashboard → Playback the activity monitor shows whether it says “Transcode” instead of “Direct Play”. Set the client quality to original, change exotic codecs to a widely supported format, or give the server more (dedicated) cores.
Read on: Self-hosting Jellyfin →

After a reboot the library is gone and Jellyfin shows empty folders.

The block storage wasn’t mounted – usually a missing or wrong fstab entry (step 2). Check with df -h /mnt/media and sudo mount -a. The line must use the UUID, not /dev/vdb1.
Read on: Self-hosting Jellyfin →

The login page loads, but the app can’t find the server / links point to an internal address.

JELLYFIN_PublishedServerUrl is missing or wrong. Set it in the compose.yaml to https://jellyfin.YOUR_DOMAIN and restart with docker compose up -d (a restart isn’t enough for the environment change to take effect).
Read on: Self-hosting Jellyfin →

Uploads of large videos abort after about a minute (error 502 or 499).

Traefik’s readTimeout (default 60 s) kicks in. Set it to 600s (or higher) as in step 4 and restart Traefik. This is by far the most common Immich-behind-proxy error.
Read on: Self-hosting Immich →

The immich-machine-learning container crashes or exits with “exit 137”, search and face recognition don’t work.

Too little RAM – the container was killed by the system (out of memory). Give the server more memory, or disable ML by removing the service immich-machine-learning from the compose.yaml. Immich then runs without face recognition and smart search, but upload and timeline work normally.
Read on: Self-hosting Immich →

After an update the containers no longer start or report migration errors.

Server, machine learning and database must run on the same version. Set the new IMMICH_VERSION in the .env and update all containers together (docker compose pull && docker compose up -d). A downgrade after a migration isn’t possible – only a reset from the backup.
Read on: Self-hosting Immich →

Immich shows “maintenance mode” / “temporarily unavailable”.

Immich v3 starts into a maintenance mode under certain database states. The log (docker compose logs immich-server) then contains a URL with a one-time token (…/maintenance?token=…) – through it you log into the maintenance mode and choose “restart” or end it. After that the normal interface is back.
Read on: Self-hosting Immich →

Container won’t start, “permission denied” on the database or upload directory.

UPLOAD_LOCATION or DB_DATA_LOCATION have the wrong access rights or are on a network drive. Put both on a local disk and make sure Docker may write into them.
Read on: Self-hosting Immich →

On login, “Forbidden (403)” or “CSRF verification failed” appears.

PAPERLESS_URL is not set or set wrong. It must exactly match your HTTPS address (https://paperless.YOUR_DOMAIN, without a trailing slash). After the correction, docker compose up -d.
Read on: Self-hosting Paperless-ngx →

Files in the consume folder aren’t processed, “permission denied”.

Most common cause: the folders weren’t created beforehand (step 3), but generated by the Docker daemon on the first start – then they belong to root. With sudo chown -R $(id -u):$(id -g) ~/paperless/consume ~/paperless/export they belong to you again. Otherwise: USERMAP_UID/USERMAP_GID don’t match the folder’s owner – determine your ID with id -u and id -g, enter the values and restart.
Read on: Self-hosting Paperless-ngx →

Uploaded documents get stuck “in processing”.

The background processing runs via Redis. Check that the broker container is running, and look under File Tasks for the error message of the failed task.
Read on: Self-hosting Paperless-ngx →

Text recognition returns gibberish or recognizes nothing.

Wrong OCR language. Set PAPERLESS_OCR_LANGUAGE=eng (or eng+deu for mixed documents). Only installed languages work.
Read on: Self-hosting Paperless-ngx →

Office documents (Word, Excel) aren’t accepted or end in a timeout.

Gotenberg and Tika are responsible for those. Check that both containers are running and PAPERLESS_TIKA_ENABLED=1 plus the two endpoint variables are set.
Read on: Self-hosting Paperless-ngx →

During bulk import the server gets very slow, the CPU is constantly maxed out.

OCR is compute-intensive, and Paperless uses all cores by default. On small servers you can throttle the load by limiting the number of workers or threads (PAPERLESS_TASK_WORKERS, PAPERLESS_THREADS_PER_WORKER). Then the import takes longer, but the interface stays usable.
Read on: Self-hosting Paperless-ngx →

After the Traefik restart, the HSTS header doesn’t appear in the curl output.

Usually the middleware isn’t on the router. Check that the line traefik.http.routers.nc.middlewares names both middlewares (nc-dav,nc-secure) and that the two nc-secure header labels are in the nc-app block. Then docker compose up -d nc-app. Traefik only picks up changed labels when recreating the container.
Read on: Hardening Nextcloud (Part 2) →

The email test fails (“There was a problem sending the email”).

Almost always port/encryption or authentication. Combine SSL/TLS with port 465 or STARTTLS with port 587 – not crosswise. If your provider uses 2FA, you need an app password (see step 4). Details are in the Nextcloud log: docker exec -u www-data nc-app php occ log:tail 20.
Read on: Hardening Nextcloud (Part 2) →

You can’t get in yourself after enforcing 2FA.

You hadn’t set up a second factor yet. Lift the requirement via the command line, set up your factor calmly and arm it again afterwards: docker exec -u www-data nc-app php occ twofactorauth:enforce --off.
Read on: Hardening Nextcloud (Part 2) →

In the brute-force box a 172.x.x.x address appears instead of your real IP.

TRUSTED_PROXIES doesn’t match the actual proxy subnet. Determine it with docker network inspect proxy -f '{{(index .IPAM.Config 0).Subnet}}' and enter the value in the nc-app environment (part 1, step 2). Otherwise the brute-force protection bans the proxy in an emergency and thus all users.
Read on: Hardening Nextcloud (Part 2) →

After a reboot, Nextcloud reports “Redis went away” or gets very slow.

The app container started before Redis. Make sure nc-redis is in depends_on (part 1) and runs with restart: unless-stopped – then Docker catches the start-order case itself.
Read on: Hardening Nextcloud (Part 2) →

Bad Gateway (502) when opening status.YOUR_DOMAIN.

Almost always the port label traefik.http.services.kuma.loadbalancer.server.port=3001 is missing or has a wrong port. Traefik then reaches the container but knocks on the wrong port. Check the label and run docker compose up -d again.
Read on: Installing Uptime Kuma →

404 page not found instead of Kuma.

As with every app behind Traefik: traefik.enable=true set? Container on the proxy network? Is the domain in the Host(...) rule correct and does the DNS record status.YOUR_DOMAIN point to the server? The Traefik dashboard shows under “HTTP Routers” whether kuma is registered.
Read on: Installing Uptime Kuma →

The interface loads, but the live update stutters / breaks off.

Kuma uses WebSockets. Traefik forwards those correctly by default – if the problem still occurs, it’s usually an upstream CDN/proxy (e.g. Cloudflare in “proxy” mode) that blocks WebSockets. For direct operation behind Traefik, no extra configuration is needed.
Read on: Installing Uptime Kuma →

After a re-setup all monitors are gone.

The kuma-data volume was deleted (e.g. by docker compose down -v). All configuration and history lives solely in this volume – that’s why it’s at the top of the next section.
Read on: Installing Uptime Kuma →

“Access through untrusted domain” instead of the login page.

Your domain isn’t in trusted_domains. Check NEXTCLOUD_TRUSTED_DOMAINS in the Compose file. To set it afterwards, use occ: docker exec -u www-data nc-app php occ config:system:set trusted_domains 1 --value=cloud.YOUR_DOMAIN.
Read on: Self-hosting Nextcloud: your own cloud behind Traefik →

The security check reports “Your web server is not set up properly to resolve .well-known/caldav”.

The CalDAV/CardDAV redirect isn’t working. Check the nc-dav middleware labels (step 3) and that the router includes them via ...routers.nc.middlewares=nc-dav. The regex must match the full https://… URL – redirectregex checks the complete URL, not just the path.
Read on: Self-hosting Nextcloud: your own cloud behind Traefik →

Warning “The ‘Strict-Transport-Security’ HTTP header is not set”.

The HSTS header is missing. Set it as a Traefik middleware and attach it to the router: traefik.http.middlewares.nc-secure.headers.stsSeconds=15552000. Attach it additionally to the nc-dav middleware on the router (...routers.nc.middlewares=nc-dav,nc-secure). The header must come from the proxy, not from Nextcloud. In part 2 we set up exactly this nc-secure middleware fully (including includeSubdomains).
Read on: Self-hosting Nextcloud: your own cloud behind Traefik →

Large uploads break off or end with a timeout / “413”.

The PHP limit is too small. Increase PHP_UPLOAD_LIMIT and PHP_MEMORY_LIMIT (step 3) and restart the container. In rare cases these variables don’t take effect – then mount your own php.ini snippet into the image.
Read on: Self-hosting Nextcloud: your own cloud behind Traefik →

On the first start the installation aborts with “MySQL server has gone away” or “Connection refused”.

The app container was faster than the database. That’s exactly what the healthcheck with depends_on: condition: service_healthy is for – check that both are present in your Compose file, and restart with docker compose up -d.
Read on: Self-hosting Nextcloud: your own cloud behind Traefik →

The container exits again immediately, the log says No persistent volume!.

The volumes: mapping is missing. Vaultwarden then aborts with the box “It looks like you did not configure a persistent volume!” and exits with code 1, so your passwords don’t end up in an ephemeral container. Add ./vw-data:/data as in step 2 and restart.
Read on: Vaultwarden: your own password manager behind Traefik →

The web interface shows “You need to enable HTTPS!” or the login fails with crypto errors.

Vaultwarden uses the browser’s Web Crypto API, which is only available in a secure context (real HTTPS). You opened the page over http:// or with an invalid certificate. Make sure Traefik fetched a valid Let’s Encrypt certificate (check the Traefik log) and that you reach the page over https://. The DOMAIN variable must also start with https://.
Read on: Vaultwarden: your own password manager behind Traefik →

The admin panel rejects your password, even though it’s correct.

Probably the dollar signs in the ADMIN_TOKEN aren’t doubled. In the compose.yaml every $ must become $$. Check with docker compose exec vaultwarden printenv ADMIN_TOKEN how the token really arrives inside the container – there it has to be a single $ again. (docker compose config won’t help: it shows the file with the doubled $$.) Also remember: at login you enter the password, not the hash.
Read on: Vaultwarden: your own password manager behind Traefik →

The phone app can’t find the server or reports “Server URL invalid”.

The server URL must be the full https:// address without a trailing path (https://vault.YOUR_DOMAIN). Also check whether the domain is reachable from outside via a browser and the certificate is valid – apps are stricter about certificate errors than browsers.
Read on: Vaultwarden: your own password manager behind Traefik →

Despite SIGNUPS_ALLOWED=false, someone was able to register.

The setting was probably set in the admin panel and overrides the environment variable, or the container wasn’t restarted after the change. Check the value under Settings → General settings in the panel and restart with docker compose up -d.
Read on: Vaultwarden: your own password manager behind Traefik →

subprocess ssh: Host key verification failed with the SFTP target.

Restic can’t confirm the SSH host key of the backup server because it isn’t known yet. Connect once manually (ssh BACKUP_USER@YOUR_BACKUP_HOST) and confirm the fingerprint – or store it with ssh-keyscan YOUR_BACKUP_HOST >> ~/.ssh/known_hosts. After that Restic runs through.
Read on: Restic backups: encrypted and off-site →

wrong password or repository does not exist.

RESTIC_REPOSITORY or RESTIC_PASSWORD_FILE isn’t set or points nowhere – so check the EnvironmentFile in the systemd service. A manually set export only applies in the current shell.
Read on: Restic backups: encrypted and off-site →

repository is already locked.

An aborted run left a lock. Check that really no backup is running anymore, then restic unlock. Never unlock blindly while a run is active in parallel.
Read on: Restic backups: encrypted and off-site →

The backup gets huge / backs up nonsense.

Restrict it with --exclude/--exclude-file (caches, logs, temporary files) and use --one-file-system so Restic doesn’t dive into mounted foreign filesystems.
Read on: Restic backups: encrypted and off-site →

prune takes forever or was aborted.

No reason to panic – prune is resumable and the repository stays valid. Start it again and afterwards run restic check once. Separate --prune from the daily backup on large repos (see step 6).
Read on: Restic backups: encrypted and off-site →

The timer runs, but a success message never arrives in Uptime Kuma.

Look at the last run with journalctl -u restic-backup.service. Usually the script aborts before the curl call (e.g. the DB dump failed) – thanks to set -euo pipefail it then stops cleanly instead of reporting a broken backup as success. So the missing push isn’t a bug, but exactly the alarm signal you want.
Read on: Restic backups: encrypted and off-site →

You locked yourself out.

ignoreip was missing or contained the wrong IP. Connect via the console in the netcup SCP (independent of SSH) and unban yourself with sudo fail2ban-client set sshd unbanip YOUR_OWN_IP. Then enter your IP in ignoreip and reload. That’s why the whitelist comes first in step 2.
Read on: Setting up Fail2ban →

fail2ban.service won’t start (systemctl status shows “failed”).

Almost always a typo in jail.local. Check the syntax with sudo fail2ban-client -t (test mode) – the command names the faulty line.
Read on: Setting up Fail2ban →

Your values from jail.local don’t take effect.

Bans only last 10 minutes, or your own IP gets thrown out despite ignoreip. Almost always the reload is missing – the service was already running before you wrote your first own line, and start/enable --now does nothing on a running service. What actually applies is shown by sudo fail2ban-client get sshd bantime (600 = Debian default, 3600 = your hour) and sudo fail2ban-client get sshd ignoreip; it is applied with sudo systemctl reload fail2ban.
Read on: Setting up Fail2ban →

No one is ever banned, even though the log is full of failed attempts.

Check with sudo fail2ban-client status sshd whether Total failed rises at all. If it stays at 0, the filter isn’t finding the entries – usually because an outdated guide set a logpath to a non-existent file. On Debian 13, remove the logpath from jail.local and use the journal (step 2).
Read on: Setting up Fail2ban →

Bans “don’t work” – the IP keeps connecting.

On Debian 13, Fail2ban bans via nftables (banaction = nftables from defaults-debian.conf). You can see it with sudo nft list table inet f2b-table: it holds the chain f2b-chain and inside it an address set addr-set-sshd with the banned IPs. sudo iptables -L -n | grep f2b, on the other hand, returns nothing – that’s not a fault but the wrong tool layer, where older guides get stuck. If the table is missing entirely, the interplay with the firewall is stuck – restart the service and look at /var/log/fail2ban.log.
Read on: Setting up Fail2ban →

“Certificate invalid” in the browser, or the Traefik log shows ACME errors.

The three usual reasons: (1) The DNS record doesn’t point to the server yet – check dig +short YOUR_DOMAIN. (2) Port 80 isn’t reachable from outside (firewall/netcup firewall) – the HTTP challenge needs it. (3) You hit the rate limit of the production CA – switch to the staging server (tip in step 3), test, then go back.
Read on: Setting up Traefik: reverse proxy with automatic HTTPS →

404 page not found when calling the app domain.

Traefik doesn’t know the route. Check: Does the container have traefik.enable=true? Is it on the proxy network? Is the domain in the Host(...) rule exactly right (incl. subdomain)? The dashboard (step 7) shows under “HTTP Routers” whether the router was registered.
Read on: Setting up Traefik: reverse proxy with automatic HTTPS →

The browser shows Traefik’s self-signed emergency certificate; the log reads permissions 644 for /acme.json are too open, please use 600.

Traefik is running but skipped the ACME resolver – hence no real certificate. Run chmod 600 acme.json (step 2) and restart the container.
Read on: Setting up Traefik: reverse proxy with automatic HTTPS →

No app is routed; the Traefik log repeats client version 1.24 is too old. Minimum supported API version is 1.40.

Your Traefik version is too old for your Docker engine – the Docker provider can no longer query the socket. Current Docker (Engine 29, API level ≥ 1.40) needs Traefik ≥ v3.6; that’s why this tutorial uses traefik:v3.7. Reproduced against Docker 29: v3.5.6 runs into exactly this error, v3.6.25 talks to the socket cleanly again. So older tags like v3.3 or v3.5 no longer work with new Docker – bump the image tag and run docker compose up -d again.
Read on: Setting up Traefik: reverse proxy with automatic HTTPS →

Basic auth on the dashboard is rejected immediately / the router is missing.

In the compose.yaml, the $ characters of the hash must be doubled ($$). Check the hash once more outside with htpasswd -nbB.
Read on: Setting up Traefik: reverse proxy with automatic HTTPS →

Gateway Timeout or Traefik doesn’t reach the container.

Usually the app is on the wrong network, or Traefik doesn’t know which one is meant. providers.docker.network=proxy in Traefik and networks: [proxy] on the app must match.
Read on: Setting up Traefik: reverse proxy with automatic HTTPS →

502 Bad Gateway, even though the container is running.

Traefik reaches the container but hits the wrong port. If the app doesn’t listen on 80, it needs the label traefik.http.services.<name>.loadbalancer.server.port=<real-port>. This exact case meets you with the first app in the next tutorial (Uptime Kuma on 3001).
Read on: Setting up Traefik: reverse proxy with automatic HTTPS →

The dry run reports No packages found that can be upgraded unattended.

Usually perfectly normal – no security update is currently pending. Check the configuration anyway via the Allowed origins are: line in the --debug run. If no security origins appear there, the Origins-Pattern from step 3 isn’t right.
Read on: unattended-upgrades setup →

Updates come, but the server never reboots despite a kernel update.

Usually Automatic-Reboot is set to "false" (default) – then set it to "true" and give it an Automatic-Reboot-Time (step 3). If the option is right, check the marker file: without /var/run/reboot-required the reboot never triggers. The package’s kernel hook is what sets it – dpkg -S /etc/kernel/postinst.d/unattended-upgrades must report it (step 3).
Read on: unattended-upgrades setup →

/boot fills up, updates fail.

Old kernels pile up. Set Remove-Unused-Kernel-Packages "true" (step 3); clean up once with sudo apt autoremove --purge.
Read on: unattended-upgrades setup →

A package is stubbornly held back (kept back).

unattended-upgrades doesn’t install updates that would remove other packages. You resolve such cases deliberately by hand with sudo apt upgrade and check what happens.
Read on: unattended-upgrades setup →

dig returns no IP or a wrong (old) one.

Usually still propagation. Query a public resolver specifically to bypass your local cache: dig +short YOUR_DOMAIN @1.1.1.1. If it already shows the correct value, only your local/provider cache hasn’t expired yet – wait.
Read on: Connecting a domain to your server (DNS basics) →

The domain resolves, but to the wrong IP – e.g. a registrar’s parking page.

Often an old A record or a redirect/parking setting still exists. Remove contradictory entries; per name and type there should be exactly one value (deliberate exceptions aside).
Read on: Connecting a domain to your server (DNS basics) →

IPv4 works, IPv6 (AAAA) leads to timeouts.

You entered an AAAA record even though the server has no working IPv6 address. Browsers then prefer IPv6 and run into a timeout. Remove the AAAA record until the server really supports IPv6.
Read on: Connecting a domain to your server (DNS basics) →

In some interfaces the value is stored with a trailing dot (YOUR_DOMAIN.) and you’re unsure.

The trailing dot (root of the DNS hierarchy) is normal and correct – many DNS managers add it automatically. No reason to worry.
Read on: Connecting a domain to your server (DNS basics) →

Emails sent from the server land in spam or are rejected (the mail log shows e.g. does not resolve to address or no PTR record).

The reverse-DNS entry (PTR) is missing or doesn’t match the sender hostname. Set the PTR as described in step 5 at the VPS provider and make sure it points to the same name that also resolves via the A/AAAA record.
Read on: Connecting a domain to your server (DNS basics) →

After activating, name resolution stops working (apt update hangs, ping domain.de fails, but ping 1.1.1.1 works).

The DNS replies are being blocked. Check the rule in the base template INBOUND UDP ACCEPT, Src port 53. Important: source port, not destination port.
Read on: netcup firewall setup →

The clock drifts, or TLS certificates are rejected due to the wrong time.

The NTP replies are missing. Add INBOUND UDP ACCEPT, Src port 123.
Read on: netcup firewall setup →

IPv6 no longer works (v4 does).

The ICMPv6 rule is missing. Without Neighbor Discovery, the server can’t even find its neighbor (router) over IPv6.
Read on: netcup firewall setup →

After activating, you can no longer get in via SSH.

The SSH rule is missing or names the wrong port. Via the VNC console (SCP → Display) you can still reach the server; correct the policy and save again. That’s exactly what the “keep a second session open” rule above helps against.
Read on: netcup firewall setup →

The server can’t send emails (outbound port 25 doesn’t work).

That’s not an error in your policy, but netcup’s default policy “netcup Mail block” – it blocks outbound SMTP (ports 25/465/587) as spam protection. For real mail sending you have to disable this netcup template on the server.
Read on: netcup firewall setup →

The server initially runs normally and is suddenly offline after one or two weeks.

If the server uses DHCP, the lease renewal is blocked by the whitelist – the DHCP reply comes from UDP source port 67, which no rule allows, and UDP is stateless. Add INBOUND · UDP · ACCEPT · Src 67 to the base template. Statically configured netcup VPS (the default) aren’t affected.
Read on: netcup firewall setup →

“Create” won’t make a snapshot while a DVD is inserted.

If an .iso image is attached in the virtual drive under Media → DVD Drive, the SCP refuses new snapshots. Eject the image there – the ISO row in the server overview has to show - again – and start the snapshot once more.
Read on: netcup Server Control Panel: snapshots, console & rescue →

The snapshot can’t be created because there isn’t enough disk space.

A snapshot needs room next to your live data. Clean up on the server, hand the freed blocks back to the host with fstrim -av, delete snapshots you no longer need, and trigger the Storage Optimization in the SCP under Media → Disks.
Read on: netcup Server Control Panel: snapshots, console & rescue →

After an online snapshot restore, the database is corrupted.

An online snapshot may catch a running write mid-way. For servers with a database, take an offline snapshot or create a database dump beforehand.
Read on: netcup Server Control Panel: snapshots, console & rescue →

You can’t get into the SCP.

The SCP uses its own credentials (not SSH). They are in the welcome email; a forgotten SCP password you reset in the netcup customer account (CCP).
Read on: netcup Server Control Panel: snapshots, console & rescue →

After ufw enable you can no longer get in via SSH.

The SSH rule was missing or targeted the wrong port. Connect via the console in the netcup SCP (VNC, independent of SSH), allow your SSH port there (sudo ufw allow …) and test again. That’s exactly what step 3 warns about.
Read on: Setting up a firewall with UFW →

ERROR: Could not find a profile matching 'OpenSSH'.

The OpenSSH profile only exists if openssh-server is installed. Use the port number instead: sudo ufw allow 22/tcp (or your port). sudo ufw app list shows the available profiles.
Read on: Setting up a firewall with UFW →

A Docker container is reachable from outside even though UFW doesn’t open the port.

Not your mistake – Docker bypasses UFW. Docker writes its rules directly into iptables and inserts them ahead of the UFW chains. A published container port (ports: in the compose.yaml) is thus open, no matter what UFW says. The clean solution is a second firewall layer in front of the server – at netcup the firewall in the SCP as an upstream perimeter. On the host it also helps to bind container ports only to 127.0.0.1 instead of 0.0.0.0.
Read on: Setting up a firewall with UFW →

yaml: line 7: did not find expected key (or similar YAML errors).

YAML is indentation-sensitive – spaces only, never tabs, and consistent per level (common: 2 spaces). Check the file without starting it: docker compose config resolves everything and complains about exactly the wrong line.
Read on: Understanding Docker Compose →

Error ... address already in use on up.

The host port (left in 8080:80) is already taken. Find the occupant with sudo ss -tlnp | grep 8080 or choose a different host port. Two containers cannot share the same host port.
Read on: Understanding Docker Compose →

An app can’t find its database (could not translate host name).

The hostname must be the service name (e.g. db), not localhost. Inside a container, localhost is the container itself, not the neighboring service. And: both services must be in the same Compose project (the same file).
Read on: Understanding Docker Compose →

After docker compose down all data is gone.

Either the volume wasn’t declared as a named volume under volumes: (then it was only the ephemeral container storage), or down -v was used. Always run persistent services with a declared named volume.
Read on: Understanding Docker Compose →

docker-compose: command not found.

That’s the old Compose v1 (with a hyphen). The current one is docker compose (with a space, plugin). If it’s missing: sudo apt install docker-compose-plugin (see Docker tutorial).
Read on: Understanding Docker Compose →

E: Package 'docker-ce' has no installation candidate.

The package source from step 2 is missing or malformed – apt only knows the name from the Debian packages' dependencies and finds no package behind it. Check the contents of /etc/apt/sources.list.d/docker.list – it must contain your Debian version (e.g. trixie) – and then run sudo apt update again.
Read on: Installing Docker on Debian →

permission denied while trying to connect to the docker API at unix:///var/run/docker.sock.

The docker group membership hasn’t taken effect yet. End the SSH session and reconnect; groups must then include docker. If not, repeat step 4.
Read on: Installing Docker on Debian →

Conflicts during installation with already-present packages.

Another Docker variant is already installed (docker.io, podman-docker, …). Remove the old packages first: sudo apt remove docker.io docker-doc docker-compose podman-docker containerd runc – existing containers/images are preserved.
Read on: Installing Docker on Debian →

docker compose reports docker: unknown command: docker compose.

The Compose plugin is missing – Docker was probably installed differently earlier. Run sudo apt install docker-compose-plugin. Note: the old docker-compose (with a hyphen) is a different, outdated tool.
Read on: Installing Docker on Debian →

SSH keeps asking for the password despite ssh-copy-id.

Usually the file permissions on the server are wrong: SSH ignores authorized_keys if group or others may write to ~/.ssh. The log on the server (sudo journalctl -u ssh --since -5min) then shows Authentication refused: bad ownership or modes for directory /home/koch/.ssh. Fix it with chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys, and also check that you’re connecting with the right user (koch@…, not root@…).
Read on: Hardening SSH access →

Permission denied (publickey) – and you can’t get in at all anymore.

Password login was disabled before the key worked. No drama: open the VNC console in the netcup SCP, log in locally there, set PasswordAuthentication yes, restart SSH and start again at step 2.
Read on: Hardening SSH access →

After the restart the SSH service no longer starts.

Syntax error in the sshd_config – a mistyped PasswordAuthentification is enough (that’s why you always run sshd -t before restarting). Log in via the VNC console; sudo sshd -t then names file, line and option: /etc/ssh/sshd_config: line 125: Bad configuration option: PasswordAuthentification, followed by /etc/ssh/sshd_config: terminating, 1 bad configuration options.
Read on: Hardening SSH access →

The settings seem to take effect, but password login still works.

A file under /etc/ssh/sshd_config.d/ (often 50-cloud-init.conf) overrides your values. sudo sshd -T | grep -i passwordauthentication shows the actually effective setting – adjust matches in the extra files and restart SSH.
Read on: Hardening SSH access →

ssh: connect to host … port 22: Connection timed out.

Usually the IP was mistyped or the server isn’t fully provisioned yet. Check the IP in your customer account and whether the server is shown as “online” there. Wait a few minutes after ordering.
Read on: First steps with a netcup VPS →

Permission denied, please try again on root login.

Wrong password – often a copy-paste issue with invisible trailing spaces, or a different keyboard layout. Copy the password without surrounding spaces directly from the credentials.
Read on: First steps with a netcup VPS →

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!.

The server was reinstalled and has a new host key – SSH rightly raises the alarm. If you reinstalled it, remove the old entry with ssh-keygen -R YOUR_SERVER_IP and reconnect. If you didn’t reinstall, investigate before you connect.
Read on: First steps with a netcup VPS →

sudo: command not found as the new user.

On minimal images the package is sometimes missing. Install it as root: apt install sudo. Then check that the user is in the group: groups koch must contain sudo – otherwise repeat step 3 and log out and back in once.
Read on: First steps with a netcup VPS →