Skip to main content
Linux command: mysqldump
# Topics

Linux command: mysqldump

26 August 2026

Every website that stores anything stores it in a database, and a database is the one part of a server you cannot copy by dragging a folder. The files sit in a format only the database engine understands, they change while you are reading them, and half of what matters lives in memory that has not been written to disk yet. So when you need a copy of a MySQL or MariaDB database, you do not copy files. You ask the server to write out a set of instructions that rebuilds the database from scratch. The program that asks is mysqldump.

1. The Basics

The job of mysqldump is narrow: it connects to a running MySQL or MariaDB server as a normal client, reads out the structure and the rows you asked for, and prints plain SQL text to standard output. That text is a recipe. Feed it back into a database server and you get your data back.

This is called a logical backup. It stores what the data means (a CREATE TABLE statement and a list of INSERT statements) rather than how the data is stored (the raw pages inside the ibdata and .ibd files on disk). The difference matters more than it sounds.

 Logical backup (mysqldump)Physical backup (file copy, snapshot)
Output Readable SQL text Binary data files
Size Larger, but compresses very well Roughly the size on disk
Speed Slower to make, much slower to restore Fast both ways
Portability Restores onto a different version, a different engine, a different machine Usually needs the same server version and platform
Editable Yes, it is a text file No
Partial restore Yes, pull out one table with a text editor Difficult

Two properties make mysqldump the default tool for everyday work. First, it is read-only against your data. It runs SELECT statements and reads table definitions; it never modifies the database it is dumping. Second, it is everywhere. It ships with the client package of every MySQL and MariaDB installation, on every hosting platform, in every Docker image. There is nothing to install and nothing to license.

It does not copy your database. It writes down the instructions to build your database again.

The right mental model: mysqldump is not a backup system. It is a program that turns a database into text on standard output. Everything else you want from a backup - a schedule, a destination, compression, retention, a test restore - is your job, done with ordinary shell tools around it.

1.1 Which Program Do You Actually Have?

Check before you write a script, because the answer differs per system:

$ mysqldump --version
mysqldump  Ver 8.0.46-0ubuntu0.24.04.3 for Linux on x86_64 ((Ubuntu))

That is Oracle's MySQL client. On a MariaDB system you get something different, and section 7.2 explains why the name is not always mysqldump at all.

Back to top

2. Where the Name Comes From

The name is a plain compound, with no hidden joke in it:

mysqldump  =  MySQL  +  dump

The interesting half is dump. In computing a dump is a complete, unprocessed write-out of some internal state: a core dump, a memory dump, a hex dump. The word carries the promise that nothing was summarised and nothing was left out. That is exactly the promise this program makes about your tables.

It also sets the right expectation about the format. A dump is not a clever archive. It is the contents, written out in the most obvious way possible, which is why the output is readable SQL that you can open in a text editor and fix by hand at three in the morning.

MariaDB has since renamed the program to mariadb-dump, keeping the same structure with a different vendor prefix. See section 7.2.

Back to top

3. A Short History

mysqldump is older than most of the databases it is used on, and it did not start at MySQL AB at all. The source file still carries the original author's own header comment, which is worth reading because it explains the tool's character:

/* mysqldump.c  - Dump a tables contents and format to an ASCII file
**
** The author's original notes follow :-
**
** AUTHOR: Igor Romanenko (This email address is being protected from spambots. You need JavaScript enabled to view it.)
** DATE:   December 3, 1994
** WARRANTY: None, expressed, impressed, implied
**          or other
** STATUS: Public domain
*/

A public domain utility from December 1994, written by one person, with an explicit promise of no warranty. MySQL AB adopted it, and the features you use every day were each contributed by a different person over the following decade. The header records who added what:

DateMilestone
3 December 1994 Igor Romanenko writes the original, and releases it into the public domain
mid 1990s Adapted and optimised for MySQL by Michael Widenius, Sinisa Milivojevic, and Jani Tolonen
10 September 1998 Jim Faucette adds -w / --where, so you can dump part of a table
2001 Gary Huntress adds XML output, adapted into the tool by Jani Tolonen
6 June 2002 Peter Zaitsev adds --single-transaction, the option that makes lock-free consistent dumps possible
10 June 2003 Alexander Barkov adds SET NAMES handling, which is why character sets survive a dump
2010 MariaDB forks from MySQL and carries the tool along
2015 MySQL 5.7 ships mysqlpump, a parallel rewrite intended to replace it
2019 MariaDB 10.4 introduces the name mariadb-dump as a symlink pointing at mysqldump
2020 MariaDB 10.5 flips it: mariadb-dump becomes the real program and mysqldump the symlink
2023 MySQL 8.0.34 deprecates mysqlpump and points users back to mysqldump

The last two rows are the interesting ones. The replacement got deprecated and the thirty-year-old original is still the recommended tool. That is not nostalgia: it is what happens when a format is simple enough that everything else in the ecosystem learned to read it.

3.1 The Version Number That Is Not the Version Number

MariaDB's output confuses people the first time they see it:

$ mariadb-dump --version
mariadb-dump from 11.4.12-MariaDB, client 10.19 for debian-linux-gnu (x86_64)

The server is 11.4.12 but the "client" says 10.19. That 10.19 is a constant in the source called DUMP_VERSION, and it describes the dump format, not the program. It changes only when the layout of the generated SQL changes. Seeing an old number there is normal and is not a sign that you are running an outdated tool.

Back to top

4. Simple Use Cases

4.1 The Simplest Possible Dump

Name a database. The SQL goes to your terminal:

$ mysqldump -u root -p sitedb

The -u flag (short for user) names the account, and -p (short for password) makes the program prompt you for the password. Do this once, watch the SQL scroll past, and the tool stops being mysterious. Then redirect it into a file, which is how you will always use it:

$ mysqldump -u root -p sitedb > sitedb.sql

Note where the redirect sits. mysqldump writes to standard output, so the shell creates the file, not the program. That single fact explains most of the pipelines later in this article, and most of the traps in section 7.3.

4.2 Reading What Comes Out

Here is a real dump of a small table, with the header comments removed for space:

$ mysqldump -u root -p --skip-dump-date sitedb j6_content

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;

DROP TABLE IF EXISTS `j6_content`;
CREATE TABLE `j6_content` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `alias` varchar(255) NOT NULL,
  `state` tinyint(4) NOT NULL DEFAULT 1,
  `created` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

LOCK TABLES `j6_content` WRITE;
/*!40000 ALTER TABLE `j6_content` DISABLE KEYS */;
INSERT INTO `j6_content` VALUES
(1,'Welcome','welcome',1,'2026-08-01 10:00:00'),
(2,'About us','about-us',1,'2026-08-02 11:30:00'),
(3,'Draft post','draft-post',0,'2026-08-03 09:15:00');
/*!40000 ALTER TABLE `j6_content` ENABLE KEYS */;
UNLOCK TABLES;

Five things are worth pointing out, because they answer questions people ask about dumps constantly.

  • The /*!40101 ... */ comments are executable. This is MySQL's version comment syntax. A normal SQL parser sees a comment and skips it; a MySQL server at version 4.01.01 or newer strips the marker and runs the statement inside. It is how one dump file stays compatible with servers old and new.
  • DROP TABLE IF EXISTS comes first. A restore is destructive by design: it replaces the table rather than merging into it. See section 9.
  • The rows are batched. One INSERT carries many VALUES lists. That is --extended-insert, on by default, and it makes restores far faster than one statement per row.
  • TIME_ZONE='+00:00' is set at the top. Your TIMESTAMP columns are dumped in UTC so they survive a move to a server in another time zone. This is --tz-utc, also on by default.
  • FOREIGN_KEY_CHECKS=0 is set at the top too. Tables are written out in the order the server lists them, which is not dependency order, so a child row can easily be inserted before the parent it points at exists. Switching the checks off for the length of the restore is what makes that work. They are restored to their old value at the end, and the constraints themselves are still there in the CREATE TABLE statements - only the checking was paused. Section 6.3 shows the related trick that mysqldump uses for views.

4.3 Restoring: There Is No mysqlrestore

People look for the matching restore command and do not find one. There is none, and there does not need to be one. The dump is a script of SQL statements, so you restore it by feeding it to the ordinary client:

$ mysql -u root -p sitedb < sitedb.sql

The database itself must already exist, because a plain dump of one database contains no CREATE DATABASE. Create it first:

$ mysql -u root -p -e "CREATE DATABASE sitedb CHARACTER SET utf8mb4"
$ mysql -u root -p sitedb < sitedb.sql

Notice that the target name is on the restore command line. That is what makes it trivial to restore a dump into a differently named database, which is exactly how you clone a live site into a staging copy.

4.4 One Table, Several Tables, Several Databases

The arguments after the database name are table names:

$ mysqldump -u root -p sitedb j6_content              # one table
$ mysqldump -u root -p sitedb j6_content j6_users     # two tables
$ mysqldump -u root -p --databases sitedb shopdb      # two whole databases
$ mysqldump -u root -p --all-databases                # everything on the server

The --databases flag (-B) changes the meaning of every argument: they all become database names, and the output gains a CREATE DATABASE and a USE statement for each one. That is a real difference in behaviour, not just in syntax:

CommandContains CREATE DATABASE?Restores into
mysqldump sitedb No Whichever database you name on the restore
mysqldump --databases sitedb Yes Always sitedb, recreating it

So use the plain form when you might want to restore elsewhere, and --databases when you want a faithful, self-contained rebuild.

4.5 Connecting to the Right Server

The connection flags are the same as for the mysql client:

$ mysqldump -h db.example.com -P 3306 -u backup -p sitedb > sitedb.sql

-h is the host, -P (capital P) is the port, and -p (lower case) is the password. Mixing up the two p flags is a rite of passage. One more subtlety: on Linux the host localhost means "use the local Unix socket", while 127.0.0.1 means "use TCP to this machine". They are not the same thing, and when a dump fails with a socket error, switching to 127.0.0.1 is the first thing to try.

Back to top

5. Moderate Use Cases

5.1 The Flag That Matters Most: --single-transaction

By default mysqldump takes a read lock on each table while it reads it (--lock-tables is on). On a live website that is a problem: for the length of the dump, writes queue up behind the backup. Visitors see a site that has stopped responding.

--single-transaction solves this. It opens one transaction at the REPEATABLE READ isolation level and dumps every table inside it. InnoDB's multi-version storage then hands the dump a frozen view of the whole database as it existed at the start, while the site carries on writing normally:

$ mysqldump --single-transaction -u root -p sitedb > sitedb.sql

Two conditions apply, and both are real:

  • InnoDB only. The consistent view comes from InnoDB's own versioning. A MyISAM table in the same dump is read without any protection and can be captured half-changed. Modern MySQL and MariaDB use InnoDB by default, so this is usually fine - but check an old, inherited database before you trust it.
  • DDL breaks it. If another connection runs ALTER TABLE, DROP TABLE, RENAME TABLE, or TRUNCATE TABLE while the dump is running, the snapshot is not isolated from that change and the dump can come out inconsistent. In practice this means: do not run an extension update or a site migration during your nightly backup.

For a live site on InnoDB, --single-transaction is not an optimisation you add later. It is the difference between a backup and an outage.

5.2 Compressing on the Way Out

SQL text is extremely repetitive, so it compresses hard. Because the output is a stream, you never need to write the large file at all:

$ mysqldump --single-transaction -u root -p sitedb | gzip > sitedb.sql.gz

On the small test database used for this article, 3002 bytes of SQL became 984 bytes compressed. On a real site the ratio is usually better than 5 to 1. Restoring reverses the pipe:

$ gunzip < sitedb.sql.gz | mysql -u root -p sitedb

Use zstd instead of gzip if you have it: it compresses better and several times faster. Whichever you pick, read section 7.3 before you put a pipe like this in a cron job, because piping changes what happens when the dump fails.

5.3 Structure Without Data, and Data Without Structure

Two flags split a dump in half:

$ mysqldump -u root -p --no-data sitedb > schema.sql        # -d: definitions only
$ mysqldump -u root -p --no-create-info sitedb > data.sql   # -t: rows only

--no-data (-d) is genuinely useful on its own. A schema-only dump is small, safe to share, and easy to read into a diff tool, which makes it the quickest way to answer "what did that update change in the database?" Take one before an upgrade and one after, then run diff on them.

5.4 Dumping Part of a Table: --where

The clause is passed straight through to the SELECT:

$ mysqldump -u root -p --where="state=1" sitedb j6_content

INSERT INTO `j6_content` VALUES
(1,'Welcome','welcome',1,'2026-08-01 10:00:00'),
(2,'About us','about-us',1,'2026-08-02 11:30:00');

The unpublished row is gone. This is how you take a sample of a huge table for a development copy:

$ mysqldump -u root -p --where="created > '2026-01-01'" sitedb j6_content

5.5 Leaving Tables Out: --ignore-table

This one needs the database name as well as the table name, every time:

$ mysqldump -u root -p --ignore-table=sitedb.j6_session sitedb > sitedb.sql

Repeat the flag once per table you want to skip. Writing just --ignore-table=j6_session is the classic mistake: it is silently ignored and the table is dumped anyway.

5.6 The Pattern Worth Learning: Skip the Data, Keep the Table

Session tables, cache tables, and log tables are often the biggest tables in a database and the least worth backing up. But you cannot simply drop them from the dump, because the restored site then crashes on a missing table. What you want is the structure without the rows, and that takes two passes:

# pass 1: everything except the session table
$ mysqldump --single-transaction -u root -p \
    --ignore-table=sitedb.j6_session sitedb > sitedb.sql

# pass 2: append the session table's definition, but none of its rows
$ mysqldump --single-transaction -u root -p \
    --no-data sitedb j6_session >> sitedb.sql

Note the >> on the second command: it appends rather than overwrites. The result contains CREATE TABLE `j6_session` with no INSERT statements after it. The site restores, logs everybody out, and carries on - which is exactly what you want. For a Joomla site, the equivalent tables are #__session, #__action_logs, and any large cache tables an extension has added.

Back to top

6. Advanced Use Cases

6.1 The Defaults You Never Set: --opt

New users often copy a long command full of flags without knowing that most of them are already on. The umbrella option --opt is enabled by default and means eight things at once:

Included in --optWhat it does
--add-drop-table Writes DROP TABLE IF EXISTS before each CREATE TABLE
--add-locks Wraps the INSERT statements in LOCK TABLES so the restore runs faster
--create-options Keeps MySQL-specific clauses such as ENGINE=InnoDB and AUTO_INCREMENT
--quick Streams rows straight out instead of buffering a whole table in memory
--extended-insert Batches many rows into one INSERT
--lock-tables Locks each table on the server while reading it
--set-charset Adds SET NAMES utf8mb4 to the top of the dump
--disable-keys Defers index rebuilding until after the rows are loaded

So mysqldump --opt sitedb is identical to mysqldump sitedb. You disable individual pieces with the --skip- prefix, or all of them with --skip-opt.

One pair is easy to confuse, and the names really are that close:

  • --lock-tables locks tables on the server, during the dump. This is the one you turn off with --single-transaction.
  • --add-locks writes LOCK TABLES statements into the output file, to speed up the eventual restore. It has no effect on the live server at all, and --single-transaction leaves it switched on.

6.2 Making a Dump Readable: --compact

When you want to read or diff a dump rather than restore it, the boilerplate gets in the way. --compact strips it:

$ mysqldump -u root -p --compact --no-data sitedb j6_content

CREATE TABLE `j6_content` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `alias` varchar(255) NOT NULL,
  `state` tinyint(4) NOT NULL DEFAULT 1,
  `created` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

It switches off comments, the drop statements, the locks, the key handling, and the charset lines. That makes it excellent for reading and unsuitable as a backup. Do not use it for the dump you actually rely on.

6.3 The Parts That Are Not Tables

A database is more than tables, and mysqldump does not treat all of the extras alike. This catches people out badly, because the dump succeeds and looks complete:

ObjectFlagIncluded by default?
Views none, but the account needs SHOW VIEW Yes
Triggers --triggers Yes
Stored procedures and functions --routines (-R) No
Scheduled events --events (-E) No
Tablespace definitions --no-tablespaces to suppress Yes

The asymmetry is not obvious and there is no warning. A database with stored procedures, dumped without --routines, produces a file that restores cleanly and is missing them:

$ mysqldump -u root -p --no-data sitedb | grep -c PROCEDURE
0
$ mysqldump -u root -p --no-data --routines sitedb | grep -c PROCEDURE
1

If there is any chance the database has routines or events, dump with both flags. They cost nothing when there is nothing to dump.

Views are included without asking, but they are written in a way that looks like a bug the first time you read a dump. Every view appears twice. Once near its alphabetical place among the tables, as a placeholder made of dummy columns:

--
-- Temporary table structure for view `v_published`
--
DROP TABLE IF EXISTS `v_published`;
/*!50001 DROP VIEW IF EXISTS `v_published`*/;
/*!50001 CREATE VIEW `v_published` AS SELECT
 1 AS `id`,
  1 AS `title` */;

And then again at the very end of the file, as the real thing:

--
-- Final view structure for view `v_published`
--
/*!50001 DROP VIEW IF EXISTS `v_published`*/;
/*!50001 CREATE ALGORITHM=UNDEFINED */
/*!50013 DEFINER=`root`@`localhost` SQL SECURITY DEFINER */
/*!50001 VIEW `v_published` AS select `j6_content`.`id` AS `id`,
    `j6_content`.`title` AS `title` from `j6_content` */;

This is restore order again, the same problem the foreign-key switch solves. A view selects from tables that may not exist yet at that point in the file, and another view may select from this view. The placeholder gives anything that refers to the view something valid to bind to while the tables are still being created, and the real definition replaces it once they all exist. Both halves are needed, so do not tidy those SELECT 1 AS ... blocks out of a dump you intend to restore.

6.4 Binary Data: --hex-blob

By default, BLOB and BINARY columns are written as escaped string literals. Most of the time this survives the round trip, but binary bytes inside a quoted string are fragile: an unlucky byte sequence, a charset conversion during the restore, or an editor that "fixes" line endings can corrupt them silently. --hex-blob writes them as 0x hexadecimal literals instead, which cannot be misinterpreted:

$ mysqldump --single-transaction --hex-blob -u root -p sitedb > sitedb.sql

The file gets bigger. If the database stores images, PDFs, or serialised binary data, add the flag anyway.

6.5 Large Values and max_allowed_packet

A row has to cross between server and client in a single packet, and both ends cap how big that packet may be. When one value is larger than the cap, the dump stops - with an error message that sends people hunting in entirely the wrong place:

$ mysqldump --max-allowed-packet=1M -u root -p sitedb big > big.sql
mysqldump: Error 2013: Lost connection to server during query
           when dumping table `big` at row: 0
$ echo $?
3

Nothing was lost and the network is fine. The row simply did not fit. The client reports a lost connection because that is genuinely what it sees when the server refuses an oversized packet, and administrators lose afternoons to it every year. The clue is at row: 0 on a table you know holds large values.

The client default is generous rather than unlimited: 16 MB on MySQL 8.0, 24 MB on MariaDB. Raise it when a table stores images, PDFs, or long TEXT:

$ mysqldump --single-transaction --hex-blob --max-allowed-packet=512M \
    -u root -p sitedb > sitedb.sql

Then remember the other end. A dump that needed a raised limit to be created needs the same room to be restored, so the target server's own max_allowed_packet has to be large enough as well. Otherwise the backup writes perfectly and refuses to load, which you discover on the day you need it.

6.6 Tab-Separated Output: --tab

The -T / --tab option writes two files per table into a directory: a .sql file with the definition and a .txt file with the raw rows.

$ mysqldump -u root -p --tab=/var/lib/mysql-files/out sitedb j6_content
$ ls /var/lib/mysql-files/out/
j6_content.sql  j6_content.txt

$ cat /var/lib/mysql-files/out/j6_content.txt
1	Welcome	welcome	1	2026-08-01 10:00:00
2	About us	about-us	1	2026-08-02 11:30:00
3	Draft post	draft-post	0	2026-08-03 09:15:00

Loading that back with LOAD DATA INFILE is much faster than replaying INSERT statements, and the .txt files feed straight into other tools. But there are two hard restrictions, and they are why most people never use it: mysqldump must run on the same machine as the server, because the server writes the data files itself, and the server needs write permission on the target directory (usually the one named by secure_file_priv). It is a local bulk-transfer tool, not a backup method.

6.7 Replication Coordinates

To build a replica from a dump, the dump has to record the exact binary log position it was taken at. Modern MySQL calls this --source-data:

$ mysqldump --single-transaction --source-data=2 \
    -u root -p --all-databases > full.sql

A value of 1 writes an executable CHANGE MASTER statement; 2 writes it as a comment, which is what you want when you are only taking a backup and do not want a restore to reconfigure replication. Combined with --single-transaction the global read lock is held only briefly at the start rather than for the whole dump.

The older names still work but now print a deprecation notice. The mapping is:

DeprecatedUse instead
--master-data --source-data
--dump-slave --dump-replica
--delete-master-logs --delete-source-logs
--apply-slave-statements --apply-replica-statements

On a server with GTIDs enabled, also think about --set-gtid-purged. Its default is AUTO, which adds a SET @@GLOBAL.GTID_PURGED statement to the dump. That is right for building a replica and wrong for restoring a copy of the site into a staging server, where it will refuse to load or corrupt the target's GTID state. For a plain backup, use --set-gtid-purged=OFF.

6.8 Keeping the Password Off the Command Line

Writing -pSecret123 puts the password in your shell history and in the process list, where any user on the machine can read it with ps. MySQL's client warns you about it every single time:

$ mysqldump -u root -pSecret123 sitedb > sitedb.sql
mysqldump: [Warning] Using a password on the command line interface can be insecure.

MariaDB's client does not print that warning, which makes the habit easier to keep and no less dangerous. For scripts, put the credentials in a file that only the owner can read:

$ cat ~/.my.cnf
[client]
user = backup
password = Secret123

$ chmod 600 ~/.my.cnf
$ mysqldump --single-transaction sitedb > sitedb.sql   # no credentials needed

Better still for a cron job, keep it in its own file and point at it explicitly, so you are not depending on which user the job happens to run as:

$ mysqldump --defaults-extra-file=/etc/backup/db.cnf \
    --single-transaction sitedb > sitedb.sql

That flag must come first on the command line. MySQL also offers mysql_config_editor, which stores credentials obfuscated in ~/.mylogin.cnf for use with --login-path. Obfuscated is not encrypted - it stops a shoulder-surfer, not an attacker with the file.

6.9 A Backup User With Only the Rights It Needs

A backup job does not need root. Give it exactly the privileges a dump uses:

CREATE USER 'backup'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, PROCESS
  ON *.* TO 'backup'@'localhost';
FLUSH PRIVILEGES;

Each grant maps to something the tool does: SELECT to read rows, SHOW VIEW to reproduce views, TRIGGER for triggers, LOCK TABLES for the default locking, EVENT for --events, and PROCESS because MySQL 8.0 needs it to read tablespace information. If you never dump events, drop EVENT. If PROCESS is refused by your host, add --no-tablespaces to the command and you no longer need it.

6.10 Running It Against a Docker Container

Most local development now happens in containers, and that adds one wrinkle worth understanding properly. The database server runs inside the container, while your shell, your redirect, and the dump file all live outside it. Every problem in this subsection comes from that one boundary.

Dumping is the easy direction. Name the service exactly as it appears in your docker-compose.yml and redirect as usual:

$ docker compose exec -T db mysqldump --single-transaction \
    -u root -p sitedb > sitedb.sql

Restoring is where people lose an evening:

$ docker compose exec db mysql -u root -p sitedb < sitedb.sql
the input device is not a TTY

Nothing is restored. The fix is the -T flag, and the reason is worth understanding rather than memorising. By default docker compose exec allocates a pseudo-terminal, as though you had typed the command interactively. A terminal is not a pipe: it does not carry a file on standard input. -T means "no terminal", which lets the redirect through.

Plain docker exec has the same problem spelled the opposite way. It allocates no terminal, but it also does not attach standard input unless you ask for it with -i:

$ docker exec CONTAINER mysql -u root -p sitedb < dump.sql     # does nothing
$ docker exec -i CONTAINER mysql -u root -p sitedb < dump.sql  # correct

The first form is by far the more dangerous of the two, because it prints no error at all. The command returns, the database is untouched, and nothing anywhere tells you the restore did not happen.

You will find plenty of advice saying you must add -T to the dump as well, or the file comes back with Windows line endings. On Compose v2 that is no longer true: it allocates a terminal only when the output is really going to a terminal, so a redirect or a pipe is already safe. For the interactive client I keep a small wrapper that tests [ -t 0 ] and adds -T only when standard input is not a terminal. The same command then gives me a real SQL prompt when I want one, and swallows a dump file when I feed it one.

There is one more container-specific trap, and it is the naming problem from section 7.2 with considerably sharper teeth. The client inside the container is whichever one the image ships, not the one on your laptop. Point a compose file at mariadb:11.4 and the familiar name has gone:

$ docker compose exec -T db mysqldump -u root -p sitedb > sitedb.sql
$ echo $?
127

Now look at the backup that command just produced:

$ ls -l sitedb.sql
-rw-r--r-- 1 peter peter 137 Aug 23 11:04 sitedb.sql

$ cat sitedb.sql
OCI runtime exec failed: exec failed: unable to start container process:
exec: "mysqldump": executable file not found in $PATH: unknown

Docker wrote its own error message into your dump file. The file exists, it is not empty, and its timestamp is today, so a backup check that only tests for a non-zero size accepts it without complaint. Use mariadb-dump for MariaDB 11 images, and read section 7.3 again: this is the empty-backup problem wearing a different hat.

Finally, credentials. A compose stack normally keeps them in its .env file already, so there is no reason to type a password on the command line and hand it to your shell history:

$ docker compose exec -T db mysqldump --single-transaction \
    -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" > sitedb.sql

Be honest about what that buys you. It keeps the password out of your history and out of the script, but the password is still visible in the process list inside the container. On a local development stack that is a fair trade. On a server, use an option file as in section 6.8.

Back to top

7. Something Most Users Do Not Know

7.1 The Sandbox Line at the Top of MariaDB Dumps

Dump anything with a recent MariaDB and the very first line looks like a mistake:

$ mariadb-dump -u root -p sitedb | head -2
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19-11.4.12-MariaDB, for debian-linux-gnu (x86_64)

It is deliberate, and it closes a real security hole. The mysql and mariadb command line clients understand their own commands beyond SQL, including \! and system, which run a shell command on your machine. So a dump file from an untrusted source could execute anything the moment you piped it into the client - and people pipe dump files into clients as root all the time.

The \- command switches the client into sandbox mode, where those escapes are disabled for the rest of the session and cannot be turned back on. Wrapping it in a version comment for version 999999 means no server will ever execute it, so it is invisible to the database itself.

The prefix changed between versions, and the difference is worth knowing when you compare dumps: MariaDB 10.6 writes /*!999999 ...*/, while 11.4 writes /*M!999999 ...*/ with an M, which marks it as MariaDB-only so that other tools ignore it cleanly. Either way, leave the line alone.

7.2 On MariaDB, mysqldump May Not Exist At All

MariaDB renamed its whole client suite, and it did so in three careful steps. First it added the new name as an alias, then it made the new name the real one, then it dropped the old name altogether. You can watch it happen version by version:

# MariaDB 10.4 - the old name is the program, the new name points at it
/usr/bin/mariadb-dump -> mysqldump
/usr/bin/mysqldump                       (4111264 bytes)

# MariaDB 10.5 and 10.6 - the arrow has turned around
/usr/bin/mariadb-dump                    (4150176 bytes)
/usr/bin/mysqldump -> mariadb-dump

For those releases it does not matter which name you type. On MariaDB 11.4 it matters a great deal, because the symlink is gone:

$ ls -l /usr/bin/mysqldump
ls: cannot access '/usr/bin/mysqldump': No such file or directory

$ mysqldump --version
bash: mysqldump: command not found

This is how a backup script that ran for years dies overnight during a routine upgrade. Worse, if the script pipes into gzip it will keep producing files, so nothing looks broken. Write scripts that pick whichever name exists:

DUMP=$(command -v mariadb-dump || command -v mysqldump) || {
    echo "no dump program found" >&2; exit 1; }

7.3 The Exit Code, the Pipe, and the Empty Backup

This is the most expensive thing in the article, so it gets its own demonstration. mysqldump reports failure properly:

$ mysqldump -h 127.0.0.1 -P 13306 -u root -pfake somedb > out.sql
mysqldump: Got error: 2003: Can't connect to MySQL server on '127.0.0.1:13306'
$ echo $?
2

Exit code 2, exactly as it should be. But look at what the shell left behind:

$ ls -l out.sql
-rw-r--r-- 1 peter peter 0 Aug 23 10:22 out.sql

A file exists. It is empty, but it exists, and it has today's date on it. A monitoring check that looks for "was a backup file created today" says yes.

Now add the pipe that nearly every real backup script has:

$ mysqldump -h 127.0.0.1 -P 13306 -u root -pfake db 2>/dev/null | gzip > db.sql.gz
$ echo $?
0

Exit code 0. The shell reports the exit status of the last command in a pipeline, and gzip succeeded - it compressed nothing, perfectly. Your cron job is happy, your log says success, and db.sql.gz is a valid gzip file containing an empty database. Nobody finds out until the restore.

The fix is one line, and every backup script should start with it:

$ set -o pipefail
$ mysqldump -h 127.0.0.1 -P 13306 -u root -pfake db 2>/dev/null | gzip > db.sql.gz
$ echo $?
2

With pipefail, the pipeline returns the failure. Then check the exit code and check the size, and never overwrite yesterday's good backup with today's bad one:

#!/bin/bash
set -euo pipefail

OUT=/backups/sitedb-$(date +%F).sql.gz
TMP=$OUT.part

mysqldump --defaults-extra-file=/etc/backup/db.cnf \
          --single-transaction --routines --events --hex-blob \
          sitedb | gzip > "$TMP"

# a dump smaller than 1 KB is not a real dump
[ "$(stat -c%s "$TMP")" -gt 1024 ] || { echo "dump too small" >&2; exit 1; }

mv "$TMP" "$OUT"

Writing to a .part file and renaming only on success means the final filename never exists unless the dump actually worked. The mv is atomic within a filesystem, so there is no moment where a half-written file looks finished.

While you are checking the status, it is worth logging which failure you got rather than just "backup failed". The codes are few and they point straight at the cause:

Exit codeMeaningWhere to look
0 Success Still check the file size
2 Could not connect, or the server refused the login Host, port, socket, credentials, or a dangling DEFINER (section 7.4)
3 Failed part-way through dumping a table Usually max_allowed_packet (section 6.5) or a privilege the account lacks
6 The named database or table does not exist A typo, or a table dropped since the script was written

Code 3 is the mean one. It means the dump started successfully and stopped in the middle, so the file is not empty - it is truncated. A size check that only tests for "bigger than zero" will happily accept it.

Which is why, on the setups I look after, the thing I alert on is the size of the newest dump measured against last week's, rather than whether the job reported success. A database that grows every day and then produces a backup half its usual size is telling you something, and it is the one signal a green tick in a cron log can never give you.

7.4 The DEFINER That Breaks Your Backups Months Later

Every view, trigger, stored routine, and event remembers the account that created it, and mysqldump writes that account into the dump:

CREATE DEFINER=`u3`@`%` PROCEDURE `p_count`()
/*!50013 DEFINER=`u3`@`%` SQL SECURITY DEFINER */

That is a reference to an account on the source server, and it travels with the file. Restore the dump somewhere that account does not exist and the restore says nothing at all:

$ mysql -u root -p rfull < full.sql
$ echo $?
0

Exit code 0, no warnings, every table present and correct. The failure waits until something actually uses the object:

$ mysql -u root -p rfull -e 'SELECT * FROM v'
ERROR 1449 (HY000): The user specified as a definer ('u3'@'%') does not exist

This is the "the migration went perfectly but the site is broken" bug, and it is the strongest argument there is for testing a restore by using the application rather than by counting rows.

There is a second half that is less known and considerably worse. A dangling definer does not only damage the restored copy. It breaks every future dump of the source database:

$ mysqldump -u root -p full1 > backup.sql
mysqldump: Got error: 1449: "The user specified as a definer ('u3'@'%')
does not exist" when using LOCK TABLES
$ echo $?
2

Read that as an operational story. Somebody tidies up a MySQL account that nobody seems to use. The website carries on working, because nothing queries that view today. That night the backup job fails - and if the job pipes into gzip without pipefail, it reports success while writing an empty file, exactly as section 7.3 describes. The backups have stopped and every signal says they are fine.

So before you drop any database account, list the definers first:

SELECT DISTINCT DEFINER FROM information_schema.VIEWS
UNION SELECT DISTINCT DEFINER FROM information_schema.ROUTINES
UNION SELECT DISTINCT DEFINER FROM information_schema.TRIGGERS
UNION SELECT DISTINCT DEFINER FROM information_schema.EVENTS;
+-----------------------+
| DEFINER               |
+-----------------------+
| mariadb.sys@localhost |
| u3@%                  |
| root@localhost        |
+-----------------------+

Ignore the server's own maintenance accounts and check what is left against SELECT user, host FROM mysql.user. Anything in the first list that is missing from the second is a fault waiting for a quiet night.

That query is worth running on any site you did not build yourself, because inherited databases collect definers pointing at developers and agencies who left years ago, and nothing brings them to the surface until the night a backup stops working.

Fixing it is one of the practical rewards of a backup made of text. You can repoint the objects in the dump before restoring it:

$ sed -i 's/DEFINER=`u3`@`%`/DEFINER=`root`@`localhost`/g' full.sql

To repair the live source instead, recreate the missing account or drop and recreate each affected object with a definer that exists. Recreating the account is usually quicker, and you can leave it without any privileges.

7.5 The Column Statistics Error Nobody Expects

Use a MySQL 8.0 client against a MySQL 5.7 server and the dump fails immediately:

Unknown table 'COLUMN_STATISTICS' in information_schema (1109)

Nothing is wrong with your database. MySQL 8.0 added histogram statistics and turned --column-statistics on by default in the client, so the newer client queries a table the older server does not have. Turn it off:

$ mysqldump --skip-column-statistics -u root -p sitedb > sitedb.sql

You meet this constantly when dumping from an older shared host with a modern local client.

7.6 The Replacement That Got Replaced

MySQL 5.7 shipped mysqlpump, a rewrite with parallel dumping that was meant to succeed mysqldump. It never did. Run it today and it says so itself:

$ mysqlpump --version
WARNING: mysqlpump is deprecated and will be removed in a future version. Use mysqldump instead.
mysqlpump  Ver 8.0.46 for Linux on x86_64

Deprecated in MySQL 8.0.34 and removed in 8.4. The official successor for large databases is now the MySQL Shell dump utilities (util.dumpInstance() and util.loadDump()), which the mysqldump manual page itself recommends for parallel dumping with compression and progress display. If you have ever hesitated to build a workflow on mysqldump because it might be replaced: it has outlived its replacement.

7.7 Knowing Where mysqldump Stops

It is the right tool for a surprisingly wide range and the wrong tool past a certain size. Its cost is in the restore, not the dump: replaying millions of INSERT statements and rebuilding every index takes hours where a physical copy takes minutes.

WhenReach for
Anything up to a few GB: websites, shops, CMS databases mysqldump
Large databases where restore time matters xtrabackup (Percona) or mariabackup, physical and hot
Large databases, still logical, but parallel MySQL Shell util.dumpInstance()
Point-in-time recovery to an exact second A dump as the base, plus binary logs replayed with mysqlbinlog
Continuous availability Replication - which is not a backup: a DROP TABLE replicates too
Whole-server rollback in seconds Filesystem or volume snapshots (LVM, ZFS, cloud disk snapshots)
PostgreSQL instead of MySQL pg_dump, the same idea with a different vendor

The pairing worth remembering is the last-but-two. A nightly mysqldump gives you a restore point at 03:00; the binary logs let you roll forward from there to the moment just before someone deleted the wrong thing. Neither does the job alone.

Back to top

8. Best Practices

  • Always use --single-transaction on a live InnoDB site. Without it you are locking the database against your own visitors for the length of the backup.
  • Add --routines --events unless you have checked the database has neither. They are not included by default and their absence is silent.
  • Add --hex-blob if any column stores binary data, and raise --max-allowed-packet on both the dump and the restore when rows are large.
  • List the definers before you delete a database account. A view or routine left pointing at a missing account breaks every dump of that database from then on.
  • Start every backup script with set -euo pipefail, then verify both the exit code and the file size. A backup script that cannot fail loudly is worse than no script.
  • Write to a temporary name and rename on success, so a broken run never replaces a good backup.
  • Keep credentials out of the command line - use --defaults-extra-file with a chmod 600 file, and pass it as the first argument.
  • Use a dedicated backup account with SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, PROCESS, not root.
  • Compress in the pipe with gzip or zstd. SQL text compresses several times over and you avoid writing the large file at all.
  • Detect the program name in scripts, so a MariaDB upgrade that removes the mysqldump symlink does not silently break the job.
  • Move the dump off the server. A backup stored next to the database protects you against nothing except your own mistakes, and it is a file an attacker would love to find.
  • Restore it somewhere, on a schedule. An untested dump is a guess. Restoring into a scratch database takes ten minutes and is the only thing that proves the backup works.
  • Remember what is not in it: users and grants live in the mysql system database, not in your site's database, and they are not in a single-database dump.
  • Read the documentation with man mysqldump, or mysqldump --help for the flags plus a full list of every default value.
Back to top

9. Common Mistakes

9.1 Common Myths

MythReality
"A dump file appeared, so the backup worked." The shell creates the file before mysqldump runs. A failed dump leaves a 0-byte file with today's date on it, and a failed dump through a pipe leaves a perfectly valid, perfectly empty .gz.
"Restoring a dump merges it into the current database." The default dump contains DROP TABLE IF EXISTS before every table. A restore replaces those tables. Tables that exist in the target but not in the dump are left untouched, which produces a confusing half-and-half database.
"--single-transaction makes any dump consistent." Only for InnoDB tables, and only if nobody runs ALTER, DROP, RENAME, or TRUNCATE during the dump. MyISAM tables get no protection at all.
"--all-databases means I can rebuild the whole server." It gets you very close, but not the configuration files, the SSL certificates, or the binary logs. And restoring the mysql system database across different server versions causes its own problems.
"The dump contains everything in the database." Stored procedures, functions, and events are excluded unless you ask for them.
"A dump that worked last night will work tonight." Not if anyone removed a database account in between. Views, triggers, routines, and events store the account that created them, and a single dangling DEFINER makes mysqldump fail for the whole database.
"A replica is a backup." A replica copies your mistakes faithfully and within seconds. It protects against hardware failure, not against DELETE FROM.
"mysqldump is legacy; I should use something modern." Its official replacement, mysqlpump, was deprecated in MySQL 8.0.34 and removed in 8.4, while mysqldump remains the recommended general-purpose tool.

9.2 Traps to Avoid

  • Confusing -p and -P. Lower case is the password, capital is the port. And there is no space after -p: -p secret is read as "prompt for the password, then dump the database called secret".
  • Forgetting the database name in --ignore-table. It must be --ignore-table=db.table. The short form is accepted without complaint and does nothing.
  • Dropping a session or cache table entirely. Use --ignore-table plus a second pass with --no-data, or the restored site crashes on a missing table.
  • Using --compact for a real backup. It removes the DROP TABLE statements and the charset lines. It is for reading dumps, not for keeping them.
  • Leaving --set-gtid-purged at its default when restoring to staging. On a GTID-enabled server the dump carries a SET @@GLOBAL.GTID_PURGED that will break the target. Use --set-gtid-purged=OFF for ordinary backups.
  • Expecting --tab to work over the network. The server writes those files, so it only works when the dump runs on the database machine and the server can write to the directory.
  • Assuming the character set takes care of itself. It usually does, because SET NAMES and --default-character-set=utf8mb4 are defaults. When you meet a legacy database declared as latin1 but actually holding UTF-8 bytes, they do not, and the corruption only becomes visible after the restore.
  • Running the dump while an update runs. Schema changes during a --single-transaction dump silently produce an inconsistent file. Do not overlap the backup window with your maintenance window.
  • Storing the dump in the web root. An unprotected backup.sql under the document root hands over every password hash and every customer record to anyone who guesses the filename.
  • Forgetting -T or -i when restoring into a container. docker compose exec tells you "the input device is not a TTY"; plain docker exec says nothing at all and restores nothing.
  • Checking only that the dump file is not empty. Exit code 3 means the dump stopped part-way through a table, so the file is truncated rather than empty. Check the exit code as well as the size.
  • Deleting the "temporary table structure for view" blocks. They look like junk left behind by a bug. They are placeholders that let a view restore before the tables it reads from exist, and the dump needs them.
  • Never testing the restore. Every other item on this list is discovered during a restore. Choose whether that happens on a quiet Tuesday or during an outage.
Back to top

10. Summary

mysqldump looks like a one-line utility and behaves like one, but almost everything that goes wrong with database backups goes wrong in the space around it: the shell, the pipe, the schedule, and the restore nobody tried.

  • It makes a logical backup: plain SQL text that rebuilds your database anywhere, on any version, readable and editable. That portability is why it is still the default tool after thirty years.
  • It writes to standard output. The shell makes the file, which is why redirection, pipes, and compression work so naturally - and why a failed dump still leaves a file behind.
  • There is no restore command. You feed the dump back with mysql < file.sql, into a database that must already exist.
  • --single-transaction is the flag that matters on a live site: an InnoDB snapshot instead of a table lock, so the backup does not take the site down with it.
  • Defaults do most of the work through --opt, but --routines and --events are not among them, and their absence is silent.
  • Cut the dump down with --where, --ignore-table, --no-data, and the two-pass pattern that keeps a session table's structure without its rows.
  • Objects remember who made them. A DEFINER pointing at an account that no longer exists lets the restore finish silently and fails when the object is used - and stops the source database dumping at all.
  • The exit code is honest but the pipeline is not. Without set -o pipefail, a completely failed backup reports success. Check the status, check the size, and rename into place only on success.
  • On MariaDB the program is mariadb-dump, the mysqldump symlink disappeared in 11.4, and dumps start with a sandbox line that protects you from a hostile dump file.
  • Know where it stops: xtrabackup or mariabackup for large databases, MySQL Shell for parallel logical dumps, mysqlbinlog for point-in-time recovery, snapshots for whole-server rollback.
  • When in doubt, type man mysqldump.

This is the quick reference worth keeping:

mysqldump -u U -p DB > db.sql            dump one database to a file
mysql -u U -p DB < db.sql                restore it (database must exist)
mysqldump --single-transaction ...       consistent dump without locking (InnoDB)
mysqldump ... | gzip > db.sql.gz         compress in the pipe
gunzip < db.sql.gz | mysql -u U -p DB    restore a compressed dump
mysqldump -u U -p DB tbl1 tbl2           only these tables
mysqldump -u U -p --databases DB1 DB2    several databases, with CREATE DATABASE
mysqldump -u U -p --all-databases        the whole server
mysqldump --no-data DB > schema.sql      structure only (-d)
mysqldump --no-create-info DB > data.sql rows only (-t)
mysqldump --where="state=1" DB tbl       only matching rows (-w)
mysqldump --ignore-table=DB.tbl DB       skip a table (needs DB.tbl)
mysqldump --routines --events DB         include procedures and events
mysqldump --hex-blob DB                  safe binary columns
mysqldump --compact --no-data DB         readable schema, for diffing only
mysqldump --skip-column-statistics ...   8.0 client against a 5.7 server
mysqldump --set-gtid-purged=OFF ...      backup on a GTID server
mysqldump --defaults-extra-file=f.cnf    credentials from a file (must be first)
mysqldump --max-allowed-packet=512M ...  tables holding large TEXT or BLOB values
docker compose exec -T db mysqldump ...  dump from a container (-T for restores)
docker exec -i CONTAINER mysql db < f    restore into a plain docker container

set -o pipefail                          or a failed dump reports success
exit 0 ok | 2 connect or definer | 3 truncated | 6 no such table

A database backup is the cheapest insurance a website has, and it is almost always set up once and never looked at again. The uncomfortable part is that a backup routine and a broken backup routine look exactly the same from the outside, right up to the day you need one. If you want the database behind your site backed up, tested, and restorable by someone who has done the restore before, that is exactly the kind of quiet work I enjoy helping with.

Back to top
Linux command: mysqldump
Peter Martin
Peter Martin
Joomla Specialist

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.