Run several Django applications on a single machine without them taking each other down: label-driven routing, automatic certificates, network and resource isolation, independent deploys, per-app backups, and knowing when one server is no longer enough.
Running several small applications on one server is often the right economic decision. Each project uses a fraction of a modern machine, and the alternative — a server per application — means paying for idle capacity and maintaining several operating systems instead of one.
The risk is coupling. Done carelessly, one application's runaway process takes down every site you host, one compromised container reaches every database, and deploying a small change to one project requires touching configuration shared by all of them. This guide covers doing it so that the applications share a machine without sharing failures.
Only one process can hold port 443. With several applications, something must accept every connection and decide, based on the requested hostname, which one should handle it. That is the reverse proxy's job, and it usually acquires a second: terminating TLS and managing certificates centrally, so individual applications never handle them.
You can do this with a traditional web server and one configuration file per site. It works well and it is what most people already know. The friction appears as the number of applications grows: every addition means editing shared configuration and reloading a process that every other site depends on.
The alternative is a proxy that discovers its own configuration. Each application declares its hostname and port as metadata on its own container; the proxy watches for changes and reconfigures itself. Adding a site means starting a container — no shared file, no reload of anything the other applications rely on.
services:
web:
image: myapp:latest
networks: [internal, proxy]
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.routers.myapp.entrypoints=websecure"
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.services.myapp.loadbalancer.server.port=8000"
The trade is discoverability: configuration now lives distributed across projects rather than in one file you can read top to bottom. For a handful of applications maintained by one team, the reduction in shared state is usually worth it.
The proxy itself needs little. Two entrypoints, one redirecting to the other, a certificate resolver, and — importantly — a default of not exposing anything unless explicitly told to.
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.httpchallenge=true"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.email=ops@example.com"
- "--certificatesresolvers.le.acme.storage=/acme.json"
The "not exposed by default" setting matters. Without it, every container you ever start becomes publicly routable — including a database you brought up for five minutes to check something.
Centralising certificates in the proxy means each application stops caring about TLS entirely. It speaks plain HTTP on an internal network; the proxy handles the public side.
Protect the certificate store file — it holds private keys — and back it up, or accept that you will re-issue everything after a rebuild. Re-issuing is usually fine, but be aware of authority rate limits if you host many hostnames and rebuild during an incident.
Here is where multi-application hosting goes wrong. If every container shares one network, every application can reach every other application's database. A vulnerability in the least important site becomes a path to the most important one.
Give each application its own internal network for its own services, and attach only its web container to the shared proxy network.
networks:
internal: # app + its database and cache; no external access
proxy:
external: true # only the web container joins this
The database is then unreachable from anything except its own application, and the proxy can reach only what it needs to route.
By default a container may consume all available memory and CPU. A memory leak in a side project can therefore trigger the kernel's out-of-memory killer, which will terminate whichever process it considers most expendable — quite possibly your production database.
deploy:
resources:
limits:
memory: 1G
cpus: "1.0"
reservations:
memory: 256M
Set limits on everything, including the small internal tool. The limit is not there to make the application efficient; it is there to contain the failure of the application to the application.
Running a single database server for all applications saves memory and creates a shared failure domain: one migration lock, one restart, one connection limit exhausted by whichever application misbehaves, one backup to restore when only one application needs it.
Give each application its own database container. The memory overhead is modest, and the benefit is that you can restore, upgrade or restart one application's data layer without scheduling downtime across every site you host.
Each application should have its own compose file, its own environment file, and its own build. Deploying is then scoped to that project.
cd /srv/apps/myapp
docker compose --env-file .env.prod up -d --build web
Nothing about that command can affect a neighbouring application, which is exactly the property you want at eleven at night. Compare it with editing a shared web server configuration and reloading: one syntax error and every site is down.
With several applications on one machine, "the server is slow" is not a diagnosis. Tag logs by application and keep resource usage visible per container.
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
docker compose logs --tail=100 -f web
Configure log rotation explicitly. Container logs default to growing without bound on many setups, and a full disk on a shared host takes down everything at once — the exact failure mode you adopted isolation to avoid.
Each application gets its own environment file, owned by the account that runs it, mode 600, and outside every served path. Do not maintain one file with every application's credentials: that file is a single point of compromise, and it guarantees that anyone deploying the smallest site holds the credentials of the largest.
Back up each application separately, so a restore is scoped to the application that needs it. A single combined dump forces you to restore everything to recover one table, which in practice means you will not do it.
for app in myapp shopapp blogapp; do
docker compose -f /srv/apps/$app/compose.yml exec -T db \
pg_dump -U appuser -Fc appdb > /var/backups/$app-$(date +%F).dump
done
Shared hosting means a compromise in one application starts on the same kernel as all the others. Isolation reduces the damage but does not eliminate it.
Run containers as a non-root user, drop capabilities you do not need, mount filesystems read-only where possible, and keep the container runtime patched. And apply judgement about what belongs together: an experimental project handling untrusted input does not belong on the same machine as the application that processes payments.
user: "1000:1000"
read_only: true
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
Three signals. When the applications' peak loads coincide and you are sizing the machine for the sum rather than the largest. When one application's compliance requirements would otherwise apply to everything sharing the machine. And when a restart for maintenance requires coordinating with more stakeholders than it is worth.
Until then, one well-organised server with proper isolation is simpler to operate, cheaper, and easier to reason about than a fleet — and simplicity has real operational value.
Convention beats memory once you host more than a couple of projects. Give every application the same shape, so that six months later you can operate one you have not touched without reading it first.
A predictable layout means a runbook can say "go to the application directory and run the deploy command" rather than enumerating special cases. It also means a new colleague can deploy any application on the machine after learning the pattern once, instead of learning six arrangements.
The rule worth enforcing: nothing application-specific lives outside the application's own directory. The moment one project needs a file somewhere central, you have reintroduced the shared state you adopted this arrangement to avoid.
Each application produces its own static assets and accepts its own uploads, and they must not collide. The reliable approach is to let each application serve its own assets from inside its own container, with the proxy simply routing to it.
Serving static files from the application process is often dismissed as inefficient. For most sites it is entirely adequate, particularly with a middleware that handles compression and long-lived cache headers, and it keeps a clean boundary: an application is one unit, with no external directory it silently depends on. If you later need a content delivery network, it sits in front and nothing about the arrangement changes.
Uploads are different, because they must survive a container being rebuilt. Give each application its own volume, and never a path shared between applications — a shared uploads directory is a route from one application's file handling bug into another application's data.
Restarting a container drops connections, and on a shared host that is noticeable because it happens more often — every application's deploy is another small outage for its users.
The pattern that avoids it: build the new image first, start the new container alongside the old one, wait until it reports healthy, let the proxy route to it, and only then stop the old one. A proxy that watches container state does most of this for you, provided you give it a health check worth trusting.
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/healthz"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
Make the health endpoint meaningful. One that returns 200 unconditionally tells you the process started, which you already knew. One that checks the database connection and any cache the application cannot work without will actually stop a broken deploy from taking traffic.
Run migrations as an explicit step, not automatically on container start. Automatic migration on start means every replica races to apply the same change, and a failed migration leaves you with containers restarting in a loop while the proxy dutifully sends traffic to whichever briefly answers.
Run them once, confirm success, then deploy the new image. On a shared host this discipline matters more than usual: a migration holding a lock affects only its own database, but a restart loop consumes CPU that every other application on the machine needs.
Two components are genuinely shared: the host operating system and the container runtime. Both need patching, and both affect every application when they restart.
Schedule those upgrades deliberately rather than letting them happen during an unattended update window. Announce a maintenance window covering all hosted applications, apply the update, and verify each site afterwards — not just the one you were thinking about. The failure mode here is subtle: a runtime upgrade that changes networking behaviour can break one application's assumptions while the others carry on, and you will not notice unless you check each of them.
Use a reverse proxy that discovers configuration from the applications themselves, so adding a site does not mean editing shared state. Keep it from exposing anything by default. Centralise certificates. Give every application its own internal network and its own database, attaching only the web container to the shared proxy network. Set memory and CPU limits on everything so one failure stays local. Keep deploys, secrets and backups scoped per application. Run containers unprivileged and rotate the runtime. And revisit the arrangement when peaks coincide or requirements diverge — one server is a good default, not a permanent commitment.