Tutorial

Running Docker Apps Next to Your Websites: The Complete 2026 Guide

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

Most servers end up in the same place: a handful of PHP websites that pay for the machine, and a growing pile of containerised software next to them — an analytics platform, a Git server, an uptime monitor, an AI model, a photo library. The two workloads have nothing in common except the kernel they share, and that sharing is where every real problem comes from. This guide is about making them coexist properly.

Everything here was checked against a live install rather than a changelog: Docker 29.6.2 with Compose v5.3.1, 99 ready-to-deploy application templates, and 107 container-management API endpoints behind the interface. Where a claim is about behaviour, it is behaviour I could reproduce.

Two runtimes, one kernel

Your websites run as Linux users with PHP-FPM pools behind a web server. Your containers run as isolated process trees with their own filesystems. They meet at exactly two points: the kernel scheduler, and the reverse proxy.

That means the two failure modes worth designing against are resource starvation and routing. Everything else — image builds, registries, Compose syntax — is detail you can learn as you go. If a container eats the RAM, your websites go down even though nothing was wrong with them. If routing is sloppy, your container is exposed to the internet on a naked port with whatever default password the image shipped with.

The resource question, answered properly

A container with no memory limit is not "unlimited", it is "first in line for the OOM killer's attention when the host runs out". And the OOM killer does not preserve your priorities — it scores processes and frequently picks the database, because the database is large.

The correct baseline for every container on a shared machine:

docker run -d --name myapp \
  --memory=512m \
  --memory-swap=512m \
  --cpus=1.0 \
  --restart=unless-stopped \
  myimage:latest

--memory-swap set equal to --memory is the line people leave out. Without it Docker grants an equal amount of swap on top, so a 512 MB cap silently becomes 512 MB of RAM plus 512 MB of swap. The container never gets killed; it just becomes unusably slow while thrashing the disk that your websites are also trying to read from. A hard limit that kills a runaway container is almost always better than a soft one that degrades the whole machine.

--cpus is in cores, not percent. On an 8-core box, --cpus=1.5 is a core and a half. Under cgroup v2 this becomes a CPU quota, and it is a ceiling rather than a reservation — the container can use less, never more.

For sizing, the memory floors from a real template catalogue are a useful anchor. A voice server or an uptime monitor is comfortable at 128 MB. An S3-compatible object store wants 256 MB. A WordPress stack with its own database, 512 MB. A workflow automation engine like n8n, 1 GB. Anything carrying a model — a photo library with face recognition, a local LLM runtime — starts at 2 GB and is happier with more. Those are floors for the thing to run, not targets for it to run well.

One trap worth naming: free and top run inside a container report the host's memory unless lxcfs is providing the cgroup-aware view. Monitoring agents that read /proc/meminfo will therefore tell you a container capped at 512 MB has 32 GB available. Read the cgroup files instead — /sys/fs/cgroup/memory.max and memory.current — or use a panel that reads them for you.

The routing question, answered properly

Do not publish containers to the world. Bind them to loopback and let the web server be the only thing listening publicly:

ports:
  - "127.0.0.1:3000:3000"

This is not paranoia, it is the difference between a private service and a public one. Object stores, model runners, admin panels and databases regularly ship with either no authentication or a documented default, on the assumption that you would not put them on the internet. People put them on the internet. The scanners find them in minutes, and it is the same story every time: an unauthenticated port, discovered before the owner finished configuring it.

Then route a hostname to it. The vhost needs four things that hand-written configs routinely miss:

location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

location ^~ /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
    default_type text/plain;
    try_files $uri =404;
}

The Upgrade and Connection headers carry WebSockets. Without them the application loads and every live feature in it — logs, terminals, chat, notifications — fails in a way that looks like an application bug.

X-Forwarded-Proto tells the app it is being served over HTTPS. Miss it and any app that constructs its own URLs will redirect HTTPS visitors to HTTP, producing an infinite redirect loop that everyone initially blames on Cloudflare.

The ACME location block above the catch-all keeps certificate renewal working. If the challenge path gets proxied into the container, the container answers 404 and renewal fails — sixty days after everything appeared to work perfectly.

TLS 1.2 and 1.3 only. There is no reason left to negotiate anything older.

Backups: dump the data, then take the volume

Copying a volume directory while a database is running produces a crash-consistent snapshot, which is a polite way of saying "sometimes restores". The correct backup of a stack has three parts, and skipping any one of them leaves you with an archive that cannot become a working application again.

A logical dump of each data engine, taken from inside the container. The flags are not cosmetic:

# PostgreSQL — whole cluster, with drop statements
pg_dumpall -c -U postgres

# MySQL / MariaDB — routines, triggers and events are NOT included by default
mysqldump -u root -p"$PASS" --all-databases --routines --triggers --events

# MongoDB
mongodump --archive=/tmp/dump.archive

# Redis — no useful logical dump; trigger a save, then archive the volume
redis-cli BGSAVE

The MySQL flags deserve their own sentence, because their absence is the most common silent data loss in this entire article. A default mysqldump takes your tables and leaves your stored procedures, triggers and scheduled events behind. The restore looks perfect. Three days later somebody notices that the nightly job has not run since the migration.

The volumes, for everything that is files rather than rows — uploads, media, generated assets.

The definition, so the topology can be rebuilt: which containers, which networks, which environment. A pile of SQL and a pile of files with no map between them is a puzzle, not a backup.

And then the part that converts hope into knowledge: restore it somewhere disposable, run one query, throw it away. Quarterly is enough. A backup nobody has restored is a hypothesis about your own competence.

Multi-tenancy: the part standalone Docker UIs cannot do

If the server is only yours, skip this section. If it holds customers, this is the section that matters.

Docker has no concept of your users. It has containers, and anyone who can reach the daemon can do anything to any of them — and because the daemon socket is effectively root on the host, "give the customer Docker access" means "give the customer the machine". This is why handing out a standalone container UI on a shared server is not a policy you can tune; it is all or nothing.

The workable model is ownership metadata plus an API that enforces it. Containers created through the panel carry labels identifying the owning user, the stack they belong to, the template they came from, and — for copies — what they were cloned from and when. Every list operation filters against the caller's role, so a reseller sees their own customers' containers and a customer sees only their own. Plan limits are checked at creation rather than apologised for afterwards.

That label-based ownership is also what makes cleanup survivable. Six months in, the question "what is this container and can I delete it" has an answer written on the container itself instead of in somebody's memory.

What day-to-day management actually needs

Deployment is the easy part and it is the part every tool advertises. The work is in the six months afterwards. In practice the operations that come up over and over are: reading logs and live stats, getting a shell inside a container, editing a config file in a container that has no editor installed, checking whether a newer image has been published, changing a memory limit without rebuilding the container, and cloning something to test an upgrade.

Two of those deserve emphasis because they are the ones people work around badly.

Changing resource limits without a recreate. The naive path — destroy the container, recreate with new flags — is fine until the container has an anonymous volume, at which point the data goes with it. Applying limits to the running container avoids the entire class of mistake.

Editing files inside a container. The usual workaround is docker cp out, edit, docker cp back, restart, discover a typo, repeat. A file manager that operates inside the container filesystem — with upload, download, permissions and ownership — turns a five-step loop into one.

For what it is worth, this is the shape of the Docker surface in Panelica: 107 endpoints covering lifecycle, per-container resources applied live, logs and stats, process list, browser terminal, in-container file management, image pulls and update checks, private registries, networks and volumes, Compose up/down/validate/export, container cloning, application-aware stack backup and restore, and domain linking that writes the vhost and handles the certificate. Ninety-nine application templates sit in front of it, twenty-one of them multi-service so that deploying WordPress or a photo library brings its database with it rather than leaving you to wire one up.

None of that is exotic. All of it is possible with the CLI and a text editor, and plenty of good administrators work exactly that way. The argument for tooling is narrower and more honest than vendors usually make it: the defaults are already correct. The UDP suffix is on the voice port, the object store is bound to loopback, the ACME block is above the catch-all, and the MySQL dump has --routines --triggers --events on it. Those four details are, in my experience, where self-managed container hosting actually breaks.

A checklist you can act on today

  1. Every container has a memory limit, with --memory-swap equal to it.
  2. Every container has a restart policy, or it disappears on the next reboot.
  3. Nothing is published on 0.0.0.0 that does not need to be — check with docker ps --format '{{.Names}}\t{{.Ports}}'.
  4. Databases and object stores pin a major version rather than riding :latest.
  5. Persistent data is on named volumes, not anonymous ones.
  6. Backups dump the data engines logically, and one has been restored in the last three months.
  7. Certificate renewal has been observed working at least once, not merely configured.

Seven lines. Most incidents I have seen on mixed website-and-container servers are a violation of one of them, and usually the first.

Frequently asked questions

Can I run Docker containers on the same server as my websites?

Yes, and it is the normal arrangement. The two requirements are hard resource limits on every container so a runaway process cannot starve the web stack, and routing through the existing web server rather than publishing container ports directly to the internet.

How much memory should I give a container?

Run it unlimited for a day, take the peak from docker stats, add about 30%. As rough floors: 128 MB for lightweight services, 256–512 MB for typical web applications, 1 GB for automation platforms, 2 GB or more for anything running a machine-learning model.

Is Docker enough isolation to host other people's code?

No. A container is a packaging and resource boundary; it shares the host kernel, and root inside a container is a much shorter path to root on the host than most people assume. For multi-tenant hosting, containers should sit on top of user-level isolation rather than replace it.

Why does my containerised app redirect in a loop over HTTPS?

The reverse proxy is not sending X-Forwarded-Proto. The application believes the request arrived over HTTP and redirects to HTTPS, which arrives at the proxy as HTTP again. Add the header and the loop stops.

Why did my Let's Encrypt certificate stop renewing after two months?

The ACME challenge path is being proxied into the container instead of served from disk. The /.well-known/acme-challenge/ location must be declared with ^~ so it takes precedence over the catch-all proxy block.

What is the safest way to back up a database container?

Run the engine's own dump tool inside the container — pg_dumpall, mysqldump with --routines --triggers --events, or mongodump — then archive the volumes separately and keep the Compose definition alongside them. Copying volume files while the database is running produces a snapshot that may not restore.

Security-first hosting panel

Hosting management, the modern way.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
Cgroups v2, native.