PostgreSQL and MySQL in Docker: Setup, Compose, Volumes and Backups

How do you run PostgreSQL and MySQL in Docker?
To run PostgreSQL and MySQL in Docker, you pull the official image, start a container, pass the password through an environment variable or a secret, and mount the data directory on a volume. Without a volume, deleting the container deletes the data. For development, docker run or Compose is enough, and the port should stay on localhost.
This guide is for website owners and developers who want to run a database on their own computer or their own VPS. Also, you can follow the commands step by step. If your hosting provider manages the database for you, you will at least learn which questions to ask them.
We are a digital marketing and web team, not a hosting company. So everything here rests on the official Docker, PostgreSQL image and MySQL image documentation. We do not re-explain what Docker is; for the basics, read our guide to Docker containers.
The end of the article also says plainly when you should not do this yourself. So keep in mind that a live store database is a very different responsibility from a learning project.
When should you run a database in Docker?
A database container helps most in development, testing and experiments. For example, you start the same version on every team computer with one command. Then, when the project ends, you delete the container and your machine stays clean. That removes most of the "it works on my machine" problems.
Production is a more careful decision. A container alone does not give you backups, monitoring or disaster recovery. So you must build those separately. The table below summarizes a general split from our reading of the documentation; it is a decision frame, not a guarantee.
| Scenario | Database in Docker | Note |
|---|---|---|
| Local development | Very suitable | Pin the version and keep data on a volume |
| Testing and CI | Very suitable | Start from clean data on every run |
| Small project on one VPS | Suitable, with care | You need a backup and update plan |
| Live online store | Decide carefully | Also consider a managed database service |
What do you need before you start?
You need Docker Engine or Docker Desktop with the Compose plugin. First, install it from the official documentation. Then confirm that it works with the two commands below. If both print a version number, you are ready.
docker --version
docker compose version
Next, decide three things. First, which database you will use. Second, where the data will live. Third, who will connect: only the application on the same machine, or a remote tool as well?
Because of that, the third question shapes the security section of this guide. If the answer is "only my application", you may not need to expose the port at all. In practice, that is the right answer for most projects.
Which image tag should you pick for PostgreSQL and MySQL in Docker?
Docker Hub hosts official images named postgres and mysql. Also, their pages list the supported tags. If you write "latest" as the tag, the next pull may bring a different major version. That can cause problems such as a data directory that no longer matches.
Instead, choose a tag in the current stable line that your application supports. Then write it in your Compose file and update it on purpose. To learn which version fits your software, read that software's own documentation.
Also re-read the notes on the official image page before every major version change. Details such as the PostgreSQL volume path change you will see below appear only there.
How do you set up PostgreSQL in Docker with docker run?
First, the shortest path is the docker run command. Next, the official PostgreSQL image requires the POSTGRES_PASSWORD variable; according to its Docker Hub page, the variable must not be empty or undefined. The example below uses a named volume and publishes the port on localhost only.
docker run --name example-postgres -d \
-e POSTGRES_PASSWORD=YOUR_STRONG_PASSWORD \
-e POSTGRES_USER=appuser \
-e POSTGRES_DB=appdb \
-v pgdata:/var/lib/postgresql/data \
-p 127.0.0.1:5432:5432 \
postgres:YOUR_TAG
Replace YOUR_TAG with the tag of the version you chose. Do not use the latest tag in anything that resembles production, because the next pull can jump to a new major version. Instead, you can see the current tag list on the Docker Hub page.
One detail matters, because the path changed. According to the Docker Hub documentation, PostgreSQL 18 and later move the PGDATA path to a version-specific directory. The mount path /var/lib/postgresql/data in the example applies to version 17 and earlier. If you pull a newer version, read the volume note for that version on the official page.
How do you set up MySQL in Docker with docker run?
In the MySQL image, the required variable is MYSQL_ROOT_PASSWORD. MYSQL_DATABASE, MYSQL_USER and MYSQL_PASSWORD create a database and a user on first start. The data directory is /var/lib/mysql, and you should mount a volume there for persistence; the official page says the same.
docker run --name example-mysql -d \
-e MYSQL_ROOT_PASSWORD=YOUR_ROOT_PASSWORD \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=YOUR_APP_PASSWORD \
-v mysqldata:/var/lib/mysql \
-p 127.0.0.1:3306:3306 \
mysql:YOUR_TAG
The MySQL documentation also lists MYSQL_RANDOM_ROOT_PASSWORD. If you give it a non-empty value such as yes, the image generates the initial password itself, and you read it later from the container log. Also, the MYSQL_ALLOW_EMPTY_PASSWORD option allows a blank root password. We do not recommend it, so leave it out.
Typing passwords on the command line is handy but risky, because they stay in your shell history. That is why the next sections cover the .env file and the secret method.
Which database should you choose: PostgreSQL or MySQL?
Both are widespread open source relational databases, and you run both in Docker the same way. So your application usually decides the choice. For example, pick whichever your software or framework supports. Ready-made systems such as WordPress expect MySQL or a compatible edition.
| Topic | PostgreSQL | MySQL |
|---|---|---|
| Required variable in Docker | POSTGRES_PASSWORD | MYSQL_ROOT_PASSWORD |
| Data directory | /var/lib/postgresql/data (17 and earlier) | /var/lib/mysql |
| Default port | 5432 | 3306 |
| Health check | pg_isready | mysqladmin ping |
| Backup tool | pg_dump | mysqldump |
If your project starts from scratch and no software dictates the choice, pick the system your team already knows. Running an unfamiliar system makes backups and updates harder.
How do volumes keep data safe in PostgreSQL and MySQL in Docker?
A container's file system is temporary. So if you remove the container, the data inside goes with it. To keep the data, you mount the data directory on a volume. There are two common ways: a named volume that Docker manages, and a bind mount that points to a folder on the host.
| Feature | Named volume | Bind mount |
|---|---|---|
| Management | Docker manages it | You choose a folder |
| File permissions | Usually trouble free | User and permission mismatches can appear |
| Direct file access | Harder | Easy |
| Recommendation for databases | Usually the first choice | When you have a deliberate reason |
These two commands list volumes and show the details of one.
docker volume ls
docker volume inspect pgdata
Watch out for one trap. With Compose, docker compose down removes the containers but keeps the volumes. In contrast, docker compose down -v removes the volumes as well. So a single flag can wipe your whole database.
How do you manage environment variables and passwords?
First, do not write the password straight into the Compose file. Instead, create a .env file in the same folder and read the values from it. Also keep that file out of version control; if you use git, list it in .gitignore. For git basics, see our Git and GitHub guide.
# .env file (example values, generate your own)
POSTGRES_PASSWORD=YOUR_STRONG_PASSWORD
MYSQL_ROOT_PASSWORD=YOUR_ROOT_PASSWORD
The Compose documentation states that values in the environment attribute override values from env_file. So if you define the same variable in both places, the environment value wins. To generate a strong password, you can use our password generator.
One more warning: you can read environment variables in the docker inspect output. So anyone with access to the server can read them. For a stricter method, continue to the next section.
How do you pass database passwords with Docker secrets?
Both the PostgreSQL and the MySQL image read a value from a file when you add _FILE to the variable name. The Docker Hub pages recommend this especially for secrets. If you define a secret in Compose, it appears inside the service as a read-only file under /run/secrets/NAME.
services:
db:
image: postgres:YOUR_TAG
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Keep the password file out of version control and restrict its permissions, for example so that only your user can read it. The same idea applies to MySQL: the MYSQL_ROOT_PASSWORD_FILE variable points to a file under /run/secrets/.
Remember, though, that this method keeps the password out of your shell history and the docker inspect output, but an attacker who takes over the server can still read the file. In short, security comes in layers. The security section below and our OWASP Top 10 guide cover the other layers.
How do you set up PostgreSQL in Docker with Compose?
Because Compose gathers all settings in one file, reruns become easy. The file below gives PostgreSQL a volume, a secret, a health check and a port that is open on localhost only. Save it as compose.yaml in a folder and start it with docker compose up -d.
services:
db:
image: postgres:YOUR_TAG
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_DB: appdb
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
pgdata:
The restart policy unless-stopped brings the container back after the computer restarts. The Compose service reference lists no, always, on-failure and unless-stopped for restart, with no as the default.
How do you set up MySQL in Docker with Compose?
So the MySQL file follows the same logic. The differences are the variable names and the health check command. For the health check, you can use mysqladmin ping. However, that command can return success even on an authentication error as long as the server is alive; so it only tells you that the process responds.
services:
mysql:
image: mysql:YOUR_TAG
restart: unless-stopped
environment:
MYSQL_DATABASE: appdb
MYSQL_USER: appuser
MYSQL_PASSWORD_FILE: /run/secrets/mysql_password
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root
secrets:
- mysql_password
- mysql_root
volumes:
- mysqldata:/var/lib/mysql
ports:
- "127.0.0.1:3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
secrets:
mysql_password:
file: ./secrets/mysql_password.txt
mysql_root:
file: ./secrets/mysql_root.txt
volumes:
mysqldata:
The Compose documentation also defines the start_period attribute: it is the time you give the container to start up. On first start the database prepares its files, so keep this value generous and tune it by watching the real start time on your own machine.
Can you run PostgreSQL and MySQL in Docker in the same Compose file?
Yes, you can. First, put both services in one file and merge the two blocks above. The only clash to watch is host ports. PostgreSQL uses 5432 by default and MySQL uses 3306, so they do not collide. If you run two containers of the same kind, you must change the host port of one.
If your application also runs inside Compose, you do not need to publish the database port on the host at all. Services on the same Compose network reach each other by service name. So your application connects to db:5432 or mysql:3306.
| Who connects | Address | Need to publish a port? |
|---|---|---|
| App in the same Compose file | db or mysql (service name) | No |
| App on the host machine | 127.0.0.1 | Yes, on localhost only |
| Remote admin tool | Through an SSH tunnel | No, do not expose it |
How do you connect inside the container and follow the logs?
After setup, the first job is to see that the database really answers. If you work with Compose, you can open the client program inside the container. Two example commands follow; change the user and database names to match your setup.
docker compose exec db psql -U appuser -d appdb
docker compose exec mysql mysql -u appuser -p appdb
The MySQL command asks for the password, and you type it yourself. That way the password never enters your command history. To leave, type \q in PostgreSQL and exit in MySQL.
For logs, the docker compose logs command is enough. Also, if you add the -f flag, you follow the stream live. On first start especially, you see in this output whether the init scripts ran and whether any password or permission errors occurred.
docker compose ps
docker compose logs -f db
The docker compose ps output also shows the health state. Do not start your application until the state reads healthy. If you hit a problem, copy the log output into a note; when you ask your provider or developer for help, that output speeds things up a lot.
Why do you need a health check, and how does depends_on use it?
A running container does not mean the database accepts connections. On first start the database prepares its files and refuses connections in the meantime. If your application starts in that gap, it fails. So a health check closes the gap.
For PostgreSQL you use pg_isready. According to the official documentation, exit code 0 means the server accepts connections, 1 means the server rejects them (for example during startup), 2 means no response, and 3 means no attempt was made because of invalid parameters. Docker turns these codes into a healthy or unhealthy decision.
services:
app:
image: YOUR_APP_IMAGE
depends_on:
db:
condition: service_healthy
According to the Compose documentation, the service_healthy condition waits until the dependency passes its health check. So your application does not start before the database is ready. You can see the health state in the docker compose ps output.
Why is exposing the database port to the internet risky?
This is the most important section of the guide. The Docker port publishing documentation says it plainly: publishing a port makes it available not only to the Docker host but to the outside world as well. So writing 5432:5432 under ports can mean offering your database to the whole internet.
Worse still, your firewall may not protect you. According to the Docker firewall documentation, when you publish a port, Docker diverts the traffic before it reaches the ufw rules. Docker routes container traffic in the nat table, so packets never reach the INPUT and OUTPUT chains that ufw uses. Saying "I closed it with ufw" is therefore not enough.
The fix is simple: write the IP address together with the port. According to the documentation, if you add 127.0.0.1 or ::1 to the publish flag, only the Docker host can reach that port. That is why every example above uses the 127.0.0.1 prefix. In the Compose short syntax, a port without a host IP binds to all interfaces.
The Docker documentation also notes that in releases older than 28.0.0, hosts on the same network segment can reach ports published to localhost. Keep your Docker version current. To see which addresses are open to the outside, you can use our IP lookup tool.
How do you connect to a remote database safely?
If you want to connect from a management tool on your computer to a database on a server, use an SSH tunnel instead of opening the port. The tunnel carries a local port through the SSH connection to the server's localhost. The database still listens on localhost only.
ssh -N -L 5433:127.0.0.1:5432 user@server.example.com
While this command runs, a tool that connects to 127.0.0.1:5433 on your computer reaches PostgreSQL on the server. We picked local port 5433 because a PostgreSQL may already run on your computer. For MySQL you apply the same idea with port 3306.
Then, this method shrinks the attack surface even if the database password is weak. An attacker must first get past your SSH key. Still, keep using a strong password; one layer of defense never replaces another.
How do you load starting data with init scripts?
Both official images run the files in the /docker-entrypoint-initdb.d folder on first start. The PostgreSQL page names the .sql, .sql.gz and .sh extensions. The MySQL page adds .sql.bz2, .sql.xz and .sql.zst, and says the files run in alphabetical order.
volumes:
- pgdata:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d:ro
The critical detail is this: according to the PostgreSQL page, these scripts run only when the data directory is empty. So if you edit a script later and restart the container, the change does not take effect. To retry the script, you must delete the volume and accept that the data disappears.
For that reason, do not leave schema changes to init scripts. For schema changes on live data, use a migration tool. Frameworks such as Laravel, Django or Prisma, for example, ship such a tool themselves.
How do you back up a database that runs in Docker?
Backups are the least negotiable rule of this guide. Copying volume files from a running database may not give a consistent backup. So prefer a logical dump. The MySQL page gives a docker exec example with mysqldump directly; for PostgreSQL, pg_dump does the same job.
# PostgreSQL backup
docker compose exec -T db pg_dump -U appuser appdb > backup-pg.sql
# MySQL backup (based on the example on the official page)
docker exec example-mysql sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' > backup-mysql.sql
The official MySQL example reads the password from the environment variable. If you use secrets, that variable is not defined; in that case you must read the password from the file instead. To restore, you do the reverse and feed the file to the mysql command with docker exec -i.
# PostgreSQL restore
docker compose exec -T db psql -U appuser appdb < backup-pg.sql
The only way to know a backup works is to restore it. Also, try it once a month in a test environment. For the general frame of backup strategy, read our website backup strategy guide.
How do you handle major version upgrades and updates?
Minor updates usually mean changing the tag and recreating the container. In practice, major version jumps are different. Database software cannot always open the data directory of an old major version with a new major version. So take a backup first, and then test the new version with a separate volume.
You can follow a safe three-step order. First, take a dump from the current database. Then start the new version with an empty volume and load the dump into it. Finally, connect your application to the new database and test it. Delete the old volume only after everything checks out.
- Pin the tag and avoid latest.
- Always take a dump before updating.
- Try the major version jump in a test environment first.
- Keep the old volume for a while as a way back.
The migration steps are documented separately for each database. So do not run anything before you read the official upgrade notes for your own version.
What should you watch when you run a database container in production?
In production, the container carries data you cannot afford to lose. So build every step on purpose. The list below collects the topics to review before going live.
- Bind the port to localhost only, and reach it remotely through an SSH tunnel.
- Also pass passwords with secrets or _FILE variables.
- Keep data on a volume and copy backups to a place away from the server.
- Define a health check and a restart policy.
- Watch disk usage and container logs regularly.
- Pin the tag and plan your updates.
For a live store or booking system, every item on this list needs an owner. An item without an owner is the one nobody can answer for when trouble comes. For this reason, many businesses find a managed database service safer. For your hosting decision, see our guide on choosing web hosting.
Which mistakes do people make most often with PostgreSQL and MySQL in Docker?
Most mistakes cluster around a few themes. The table below pairs symptoms with causes, so that you quickly find which section to return to when you hit a problem.
| Symptom | Likely cause | Fix |
|---|---|---|
| Data vanished after removing the container | No volume mounted | Mount the data directory on a volume |
| I changed the password but the old one still works | Variables are read only on first start | Change the password inside the database |
| Init script did not run | Data directory is not empty | Try with a new volume |
| Application cannot connect | Database is not ready yet | Add a health check and service_healthy |
| Port already in use | Another service on the host | Change the host port |
The database creation variables take effect only when the data directory is empty. So do not expect a password change in the env file alone to work. After the first start, you must change the password with the database's own command.
When should you not do this yourself and leave it to your hosting provider?
So let us be honest: running the database yourself is not the right call for every project. In a live system that holds customer data, orders or payment details, backups and security updates need constant attention. Because of that, if nobody can give that attention, leave the job to the provider.
Do not do it yourself in these cases. You run a store where a database outage directly costs sales. Also, nobody will maintain the server regularly. You have never tried to restore a backup. You hold personal data and carry GDPR duties. In these cases, ask your provider for a managed database, automatic backups and monitoring.
The questions to ask a provider are simple, too. How often do they take backups, and where do they keep them? How long does a restore take? Who applies the security updates? If you get no clear answers, look for another provider. To build your database skills, our SQL learning roadmap and our Redis and Memcached article will help.
What is the checklist for PostgreSQL and MySQL in Docker?
When you finish the setup, go through this list from top to bottom. Also, each item closes a risk we described in this guide. If you can tick them all, you have built a solid base for development or a small project.
- You used the official image and a pinned version tag.
- The data directory sits on a volume.
- Passwords come from .env or secrets, away from version control.
- The port is limited with 127.0.0.1.
- A health check exists, and your application waits for the service_healthy condition.
- You have taken a backup and tested a restore.
- A plan covers major version jumps.
If you want to build this infrastructure as part of a website or application project, take a look at our team's custom software development service.



