Operating .NET Apps on Dokploy: Backups, Recovery, and Proxy Safety

aspnetcoredeploymentdevopsdockerself-hosting
Operating .NET Apps on Dokploy: Backups, Recovery, and Proxy Safety

To operate a .NET app on Dokploy safely, trust forwarded headers only from Traefik, deploy immutable images you can roll back to, send backups off the server and rehearse restoring them, set resource limits, and keep the panel private and updated. Dokploy makes the first deployment feel like a managed platform, and this guide covers the work that remains after it.

It assumes the panel is installed and DNS already resolves. If you haven't got that far, start with how to deploy a .NET app to a VPS with Dokploy.

An HTTPS client request reaches Traefik, which terminates TLS and routes over the shared dokploy-network to the API container on port 8080, while Postgres sits on a private network that the proxy never touches

What This Guide Assumes

Dokploy manages Docker workloads and Traefik routing on a server you own. The baseline here is a working deployment that now needs production operating rules.

The Production Compose Contract

Dokploy Domains is the recommended routing path. When you attach a domain in the UI, Dokploy adds the Traefik labels and proxy network to the selected service during deployment. Use Preview Compose to inspect that generated contract before a release.

The manual alternative keeps the routing contract in Git. It requires the following network and labels:

services:
  api:
    image: ghcr.io/you/app-api:${GIT_SHA}
    restart: unless-stopped
    networks:
      - dokploy-network # without a shared network, Traefik returns 504
      - appdb-network # private link to the DB, not exposed to the proxy
    environment:
      # Behind a reverse proxy: honor X-Forwarded-* so the REAL client IP drives the
      # per-IP rate limiter (otherwise every request looks like the proxy and the whole
      # app shares one bucket). Also fixes https redirects.
      Proxy__Enabled: 'true'
      Proxy__KnownProxy: '${TRAEFIK_IP}' # from docker inspect dokploy-traefik
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.api.rule=Host(`api.example.com`)'
      - 'traefik.http.routers.api.entrypoints=websecure'
      - 'traefik.http.routers.api.tls.certresolver=letsencrypt'
      - 'traefik.http.services.api.loadbalancer.server.port=8080'

networks:
  dokploy-network:
    external: true # created by the platform, you join it
  appdb-network:
    driver: bridge

The manual path has three rules:

1. Join the external proxy network. Traefik can only route to containers it shares a Docker network with. Dokploy creates dokploy-network at install time. Your service declares it external: true and joins it when you are not using isolated deployments.

Keep your database on a separate private network that the proxy never touches.

2. Add the Traefik labels. The four labels say: route requests for api.example.com, on the HTTPS entrypoint, with a Let's Encrypt certificate, to this container's port 8080 (the default ASP.NET Core container port). Those labels replace separate load balancer and TLS configuration. (If you have used YARP, this is the same reverse-proxy concept turned into declarative labels.)

3. Do not publish host ports. No ports: section on the api service. The proxy reaches the container over the shared network, so a published port would only open a second path directly to your app, without TLS or the proxy's protections. The only ports open on the host should be 80, 443, and SSH.

Why Does Every Request Come From the Same IP?

When an ASP.NET Core app first moves behind a reverse proxy, two request values change without any error or warning.

From your app's point of view, every request now arrives over a TCP connection from Traefik. Unless you tell ASP.NET Core otherwise:

  • HttpContext.Connection.RemoteIpAddress is the proxy's IP, for everyone.
  • Request.Scheme is http, because Traefik terminated the TLS and forwarded plain HTTP internally.

Both produce confusing symptoms:

Symptom 1: redirects and links downgrade to http. Anything that builds absolute URLs from the request (auth redirects, Location headers on 201s, OIDC callbacks) emits http:// URLs even though users are on https://. Browsers then either warn, loop, or get blocked by the OIDC provider's redirect URI validation.

Symptom 2: your per-IP rate limiter rate-limits everyone as one user. If you use rate limiting partitioned by client IP, every request now shares the proxy's IP, so the entire user base shares one bucket. One curious user with a retry loop can 429 your whole product. Audit logs, geo lookups, and abuse detection get the same wrong IP.

The proxy already sends the client IP in X-Forwarded-For and the original scheme in X-Forwarded-Proto. Your app just has to be told to honor them:

using System.Net;
using Microsoft.AspNetCore.HttpOverrides;

// Without this, HttpContext.Connection.RemoteIpAddress is the proxy, and Request.Scheme
// is http even when the user is on https. Both break things (rate limiting, redirects).
var proxyEnabled = builder.Configuration.GetValue<bool>("Proxy:Enabled");

if (proxyEnabled)
{
    var knownProxyText = builder.Configuration["Proxy:KnownProxy"]
        ?? throw new InvalidOperationException(
            "Proxy:KnownProxy must contain Traefik's IP address.");

    if (!IPAddress.TryParse(knownProxyText, out var knownProxy))
    {
        throw new InvalidOperationException(
            "Proxy:KnownProxy must be a valid IP address.");
    }

    builder.Services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders =
            ForwardedHeaders.XForwardedFor |
            ForwardedHeaders.XForwardedProto;

        // Accept forwarded headers only from the Traefik instance.
        options.KnownProxies.Add(knownProxy);
    });
}

var app = builder.Build();

if (proxyEnabled)
{
    // Keep this before auth, redirects, and rate limiting.
    app.UseForwardedHeaders();
}

Two rules apply to this snippet.

UseForwardedHeaders() must run early. It rewrites RemoteIpAddress and Request.Scheme, so it has to execute before anything that reads them: HTTPS redirection, authentication, rate limiting. First in the pipeline is the safe default.

Trust only Traefik. X-Forwarded-For is just a header, and any client can send one. Clearing KnownNetworks and KnownProxies makes ASP.NET Core accept that header from any direct peer, which Microsoft explicitly advises against.

Read Traefik's address from docker inspect dokploy-traefik, set it as TRAEFIK_IP, and keep the application's host port unpublished. Recheck the address after Traefik restarts. If it changes, ASP.NET Core ignores the forwarded headers, and every client shares one rate-limit bucket again until you update the setting and redeploy.

For an isolated Docker network with a controlled subnet, .NET 10 can instead add that narrow CIDR to KnownIPNetworks. Do not trust the CIDR of a shared dokploy-network, because another workload on it could spoof a client IP.

If the same image also runs directly during local development, register and invoke the middleware only when Proxy__Enabled is true. The environment flag in the Compose file does not change behavior by itself, because your startup code has to read it.

The Rest of the App Config

The remaining wiring is standard, so in brief:

  • Secrets via environment variables. Dokploy's panel manages per-service env vars, which map straight onto .NET configuration keys (ConnectionStrings__AppDb). Details in environment variables in Docker.
  • The database is either a Dokploy-managed Postgres or a service in your compose file on the private network. Either way the connection string points at the service name, not localhost.
  • Migrations: run them at startup for simple single-instance apps, or build an EF Core migration bundle and run it as a deploy step for anything you care about.
  • Health checks: expose separate liveness and readiness endpoints (see health checks in ASP.NET Core). Dokploy's Compose deployments run docker compose up -d without waiting for health, so request /health/ready yourself after each release.
  • CORS and auth: your allowed origins and OIDC authority URLs change per environment, so keep them in env vars next to everything else.

What Do Traefik's 404, 502, and 504 Errors Mean?

You deploy, everything looks green, and the domain returns an error. The status code tells you which layer failed, so read it before you change anything. I broke a container in each of these ways behind Traefik v3.6 and recorded the response:

What is wrongTraefik returnsWhat to check
Nothing200 from your appNothing to do
No router matches the hostname404 page not found, immediatelyThe router rule, the domain on the Domains tab, and DNS
The container is marked unhealthy404 page not found, immediatelyThe Docker healthcheck, which removes the container from routing
Wrong target port502 Bad Gateway, immediatelyThe port your app listens on inside the container
No shared Docker network504 Gateway Timeout, after 30 secondsWhether the service joined dokploy-network

The second 404 is easy to misread, because the routing configuration is correct and the container is running. Traefik drops a container from routing when its healthcheck fails. That's a good reason to point the Docker healthcheck at liveness instead of readiness: with a readiness check, a database outage turns your application's 503 into a confusing 404.

For the port, modern .NET images listen on 8080 unless you changed ASPNETCORE_HTTP_PORTS.

Build a Release You Can Roll Back

A Git-triggered build is convenient, but rollback is only reliable when the previous artifact is immutable and still available. Use CI to build the image once, tag it with a commit or release version, and let Dokploy deploy that exact image. Do not rebuild an old branch during an incident and call it a rollback.

Request the application's readiness endpoint after each release. Keep the previous image tag through the observation window, and record which environment variables and secrets belong to each release. Database changes must remain compatible with both versions until the rollback window closes.

If readiness or a smoke test fails, redeploy the previous image and investigate away from production. Dokploy can do this for you only on Swarm-based Applications, through its health-check rollback settings. The blue-green deployment guide covers the same verification sequence when you keep two environments running.

Backups Need a Restore Path

Back up every piece of state that cannot be recreated from Git:

  • consistent database dumps
  • persistent volumes containing uploads or application data
  • Dokploy configuration and deployment metadata
  • the environment-variable inventory, with secret values kept in a separate secret manager

Send backups to storage outside the VPS. A provider snapshot is useful for disaster recovery, but it does not replace an application-consistent database backup with retention.

Schedule a restore drill onto a disposable host. Verify that the database opens, migrations agree with the deployed image, uploaded files are present, and the application passes its smoke tests. The drill gives you a real recovery time and exposes missing credentials while the original server still exists.

Patch and Protect the Host

Use SSH keys, disable password authentication, restrict inbound ports to 80, 443, and the SSH access path, and keep the Dokploy panel behind a VPN when possible.

Apply operating-system security updates continuously and plan reboots instead of postponing them indefinitely. Upgrade Docker and Dokploy deliberately, with a current backup and a rollback path.

Dokploy needs the same attention as the operating system, because it published dozens of critical advisories in 2026, some of them exploitable without an account. Dokploy vs Coolify lists examples, and the deployment checklist shows how to confirm from outside that the panel isn't reachable.

Watch Capacity Before It Fails

A single host couples the API, database, proxy, image cache, and logs to one disk and one memory budget. Add alerts for disk usage, memory pressure, container restarts, certificate expiry, and failed backups.

Set CPU and memory limits so one leaking service cannot starve the database. Use Dokploy's image cleanup controls after confirming which images and volumes are still required. Avoid blind prune commands in automation because an unused-looking volume may be the only copy of important state.

Send uptime checks from outside the VPS. Local health checks can report green while DNS, TLS, the firewall, or the whole host is unreachable.

Recovery Checklist

When the service is down, isolate the failed layer in this order:

  1. Confirm DNS and TLS reach the expected host.
  2. Check whether Traefik has a matching router and can see the container on dokploy-network.
  3. Check the container health status and application logs.
  4. Check disk, memory, and database availability.
  5. Roll back to the previous immutable image if the failure began with a release.
  6. Restore onto a replacement host if the machine or persistent data is lost.

Summary

Dokploy can make deployment simple, but a single VPS is still yours to operate. Trust proxy headers only when the network prevents direct access to the application, deploy immutable images, store backups off the host, rehearse restores, and alert on capacity.

Use a private network to reach the panel, database, dashboards, and SSH without publishing those services to the internet. My self-hosting course practices this same operating routine on one server.

Thanks for reading.

And stay awesome!


Frequently Asked Questions

What should I back up in a Dokploy installation?

Back up the application database, persistent volumes that contain state, and Dokploy configuration. Send copies to storage outside the VPS and rehearse the restore on a replacement host while the original server still exists.

Why does my app behind Traefik see every request coming from the same IP?

The TCP connection your app sees belongs to the proxy, so RemoteIpAddress is the proxy address unless you enable ForwardedHeaders middleware and explicitly trust Traefik as the forwarder. Until you do, per-IP rate limiting effectively puts all users in one bucket.

Why does Traefik return 404 for my deployed container?

Traefik returns 404 when no router matches the request or when the container is marked unhealthy, because it removes unhealthy containers from routing. A wrong target port returns 502 instead, and a container that shares no network with Traefik returns 504 after a timeout.

Should I publish container ports when running behind Traefik?

No. The proxy reaches your container over the shared Docker network, so publishing host ports is unnecessary and dangerous: it exposes the app directly, bypassing TLS and any proxy-level protections.

Browse all Self-Hosting guides →
Loading comments...

Whenever you're ready, there are 5 ways I can help you:

  1. Pragmatic Self-Hosting: Learn to deploy and run your apps on a VPS with Dokploy and Tailscale. Keep administration private, automate releases, and practice restoring backups, with source code and a runbook you can reuse.
  2. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  5. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

One practical .NET tip, every Saturday.

Join 66,000+ engineers who read it. Free, and you can unsubscribe any time.