Isolating Containerized Databases: Postgres and MariaDB Best Practices

Homelab / Self-hosting
A bright editorial homelab scene showing an application reaching a protected database layer across a private internal network boundary.

For a while, I treated database containers like slightly more serious application containers.

They needed persistent storage.
They needed a password.
They needed backups.

That all sounded true enough.
It just was not the whole story.

What I learned later was that databases are where “good enough” container habits become expensive. A web app exposed too broadly may embarrass you. A database exposed too broadly can keep the consequences around much longer. The problem is not only whether the service works. It is how many unnecessary paths are allowed to reach the one service that holds the most durable state in the stack.

That is why I no longer think of database isolation as a bonus hardening step. I think of it as one of the clearest ways a Compose stack tells the truth about which parts of the system deserve the narrowest trust boundary.

In this article

  1. Why database containers deserve a different standard from ordinary services
  2. How Docker Compose networking should usually do the first layer of isolation
  3. Why not publishing database ports is often the best default
  4. How secrets and ordinary configuration should be separated
  5. Where Postgres and MariaDB still need their own access controls
  6. What I think a clean isolated Compose pattern looks like

For the technical baseline below, I use Docker's official documentation and separate documented platform behavior from my own placement and operating recommendations.

A database container is not just another backend service

The first mistake is conceptual.

An application container and a database container may both live in the same docker-compose.yml, but they do not carry the same risk profile. If the database is reachable from places that never needed to reach it, the problem is not merely bad architecture. It is unnecessary trust pointed at the one service most likely to contain long-lived credentials, private application data, and the highest cost of corruption.

That is why isolation matters here in a more literal way than it does for many other services.

I want the database to be:

  • reachable by the applications that genuinely need it;
  • hard to reach accidentally from unrelated services;
  • not casually published to the host or LAN;
  • governed by database-native access rules as well as container networking rules;
  • easy to back up and restore without being easy to bump into.

That combination is what makes the stack feel calm later.

Compose networking should usually be the first boundary

Docker’s Compose networking documentation explains the default behavior very clearly: Compose creates a single network for the application, and each service joins it automatically. Docker’s Compose network reference also makes it clear that you can define named networks explicitly, and that services join only the networks you specify.

That is where database isolation should usually begin.

If every service in a Compose project sits on the same default network forever, then every service in that project inherits a level of mutual reachability that may have nothing to do with what the application actually needs. That is especially wasteful for databases.

This is why I increasingly prefer an explicit backend network for stateful services:

  • the application joins it;
  • the database joins it;
  • the reverse proxy often does not;
  • unrelated utility containers often do not;
  • every service outside that trust story stays outside it.

Docker’s network reference also documents the internal network attribute, which restricts external access from that network. I like this a lot for backend-only database networks because it expresses intent very clearly: this is not the place where outward-facing traffic belongs.

Not publishing the database port is often the cleanest default

This is the second big lesson.

If the application container and the database container already share a backend network, the app can usually reach the database by service name without any host port publishing at all. Docker’s Compose networking docs describe exactly this kind of service discovery: containers on the same Compose network can reach one another by service name.

That means a lot of database port mappings are not solving a real need.

They are just habit.

For a normal self-hosted stack, I now treat these as the default questions:

  • Does anything outside the Compose backend network truly need direct TCP access to the database?
  • Is this for daily application traffic, or only for occasional admin work?
  • Would docker exec, a temporary admin container, or an SSH tunnel be cleaner than leaving the database published all the time?

If the honest answer is “the app is the only normal client,” then ports: usually has no business being there.

That is one of the easiest wins in the whole database-isolation story.

Secrets should not live where ordinary settings live

Docker’s Compose secrets docs make the split explicit. Secrets are granted to services on a per-service basis and are mounted as files under /run/secrets/<secret_name>. The Compose file reference also notes that secrets come from a file or environment source, with host-environment secret sourcing supported by Docker Compose specifically.

That is structurally different from ordinary environment settings.

This matters a lot for databases because people often start with the worst possible pattern:

  • database password inline in environment:;
  • application password inline in environment:;
  • same credential duplicated across services;
  • one big Compose file that becomes its own accidental secret archive.

I no longer think of secrets here as abstract best practice. I think of them as a way to stop lying about what kind of data this is.

If the image supports secret-file or _FILE-style ingestion, I would rather use that for:

  • root or superuser credentials;
  • application database passwords;
  • replication or service credentials;
  • any token that should not be treated like routine config.

That lines up naturally with the broader Compose hygiene in environment variables, secrets, and network structure. A stack becomes easier to maintain when sensitive values are delivered differently from ordinary settings.

Database-native controls still matter after Docker networking

This is another place people over-trust the container boundary.

Docker networking can reduce reachability.
It does not replace the database’s own access model.

For PostgreSQL

PostgreSQL’s current documentation on pg_hba.conf states this very plainly: remote TCP/IP connections are not possible unless the server is started with an appropriate listen_addresses value, and access is then governed by pg_hba.conf.

That means there are two distinct questions:

  1. What interfaces should PostgreSQL listen on?
  2. Which hosts, users, and databases should actually be allowed through?

In a Compose setup, this creates an important nuance that many bare-metal guides blur:

  • if the database truly needs to accept only local in-container connections, listen_addresses = 'localhost' can make sense;
  • if the database must serve another container on the backend network, it needs an address policy that actually allows that containerized path.

So I do not blindly copy “bind only to localhost” into a multi-container stack and declare victory. Instead, I align the listener with the topology, then narrow access with pg_hba.conf to the actual client ranges or specific backend addresses that should exist.

The point is not to worship loopback. The point is to make the PostgreSQL listener and host-based authentication tell the truth about the network.

For MariaDB

MariaDB’s current remote access guide is similarly useful because it separates skip-networking from bind-address.

Their documentation explains:

  • skip-networking disables TCP/IP networking;
  • bind-address controls which addresses the server listens on;
  • binding to 127.0.0.1 limits access to localhost only.

That is very clear, and it creates the same practical lesson as PostgreSQL:

if the app lives in another container, binding MariaDB only to localhost inside the DB container will not help the app reach it.

So again, the real best practice is not “always bind localhost.”
It is:

  • use Docker networking to ensure only the right containers can even try;
  • then configure MariaDB to listen only where the actual deployment requires;
  • and avoid publishing the database service beyond that backend path unless you have a strong reason.

I think this is one of the most misunderstood parts of containerized database advice. A setting that is perfect for a one-box local process model can be wrong for a multi-container backend service model.

The cleanest database network is usually boring

This is what I want the final design to feel like:

  • the app knows the database by service name;
  • the database has no public host port mapping;
  • the backend network is explicit and internal-only where appropriate;
  • secrets are mounted, not pasted inline;
  • database-native auth rules are narrower than “anything that can reach me may log in.”

That is not glamorous, but databases should not need glamour.
They should need fewer surprises.

A Compose pattern I trust

This is the shape I increasingly prefer for a normal application plus database stack:

services:
  app:
    image: example/app:latest
    environment:
      DB_HOST: db
      DB_NAME: appdb
      DB_USER: appuser
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    networks:
      - proxy
      - backend

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    networks:
      - backend
    volumes:
      - db_data:/var/lib/postgresql/data

secrets:
  db_password:
    file: ./secrets/db_password.txt

networks:
  proxy:
  backend:
    internal: true

volumes:
  db_data:

The important part is not the exact image name.
It is the structure:

  • no published database port;
  • application and database share only the backend path they need;
  • the reverse proxy does not need direct database access;
  • credentials are not treated like normal config.

That same pattern maps cleanly onto MariaDB too, even if the exact environment or startup knobs differ.

Admin access should feel deliberate, not permanently available

This is the part many people skip because it feels slightly inconvenient.

If you occasionally need SQL access from your workstation, I would rather do one of these:

  • docker exec into the database container;
  • run a temporary client container on the backend network;
  • use an SSH tunnel when remote administration is genuinely needed;
  • publish the port only briefly for a controlled maintenance task.

What I try not to do anymore is leave 5432:5432 or 3306:3306 permanently published just because “sometimes I use DBeaver.”

That is not really an administration strategy.
It is open convenience.

Isolation is also about recovery discipline

The more I work with self-hosted stacks, the more I think isolation and recovery belong in the same conversation.

A database that is narrowly reachable, explicitly networked, and cleanly credentialed is also easier to restore with confidence. The operator can answer simpler questions:

  • who was supposed to talk to it;
  • what credentials were supposed to exist;
  • which path was supposed to be open;
  • what normal behavior looked like before the incident.

That kind of clarity matters just as much during restore work as it does during ordinary operations.

It also pairs naturally with choosing cleaner storage models and with the broader discipline of keeping containers from reaching farther than they need to.

The most important database best practice is often subtractive

Many containerized databases become safer not by adding elaborate tools, but by removing unnecessary reachability: no host port, no flat all-services network, no inline password sprawl, and no database listener broader than the actual topology requires.

This operating decision also connects to Exposing Container Ports Safely: Host Networking vs Bridge vs Custom Networks, where the same tradeoff appears at a different layer of the homelab.

Conclusion

Isolating containerized Postgres and MariaDB well usually starts with simpler boundaries, not more dramatic ones. Put the database on a dedicated backend network. Do not publish its port to the host unless there is a real reason. Separate secrets from ordinary configuration. Then tighten the database’s own listener and authentication rules so they match the container topology instead of fighting it. The goal is not a magical invisible container. The goal is a database that is reachable only where the application honestly needs it to be.

FAQ

Should I publish Postgres or MariaDB ports in Docker Compose?

Usually not by default. If the database is only meant to serve application containers on the same backend network, host port publishing often adds unnecessary reachability without adding real value.

Is bind-address = 127.0.0.1 always a good idea for MariaDB containers?

No. It is a good local-only setting, but if another container needs to connect over the Compose network, binding only to loopback inside the database container will block that path.

Does Docker networking replace pg_hba.conf in PostgreSQL?

No. Docker networking can reduce which containers can reach PostgreSQL, but listen_addresses and pg_hba.conf still govern whether PostgreSQL itself will accept those connections.

Are Docker Compose secrets worth using for databases?

Yes, especially when the image supports file-based secret input. They keep sensitive values out of the same plain configuration path used for ordinary runtime settings.

What is the easiest database isolation improvement in an existing Compose stack?

Remove unnecessary port publishing and move the database onto a dedicated backend network that only the app and the database share.

Continue reading

More from Homelab / Self-hosting

Related reading from the same topic cluster and nearby categories.

Browse category