DevOps

MySQL 'Got an error reading communication packets' in Docker

When MySQL inside Docker aborts connections with 'Got an error reading communication packets', the root cause is usually a race between container startup and the first query. Here's how to fix it with a readiness check and timeout tuning.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

If you see [Note] Aborted connection 8 to db: 'unconnected' user: 'root' host: '164.20.10.2' (Got an error reading communication packets) in your MySQL container logs, the cause is almost always that your application connects before MySQL has finished initialising. Fix it with a readiness check (mysqladmin ping) before the first query. Timeouts are a second, much rarer cause — and note that wait_timeout already defaults to 28800 seconds, so "raising it to 28800" changes nothing.

If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.

What you'll see#

A developer on Stack Overflow reported this exact symptom after switching from docker-compose to programmatic container creation with the Docker API. The MySQL 5.7 container logs this line every time the app tries to connect:

plaintext
[Note] Aborted connection 8 to db: 'unconnected' user: 'root' host: '164.20.10.2' (Got an error reading communication packets)

The app container can telnet to port 3306 and even receives the MySQL handshake banner, yet every authenticated connection is immediately aborted. The same setup works perfectly when launched with docker-compose.

Why MySQL aborts connections in Docker#

The error "Got an error reading communication packets" means MySQL accepted a TCP connection but could not read a valid packet from the client. In a Docker environment, this almost always happens because the application connects before the MySQL server has finished its first‑time initialisation.

When you start a MySQL container for the first time, the entrypoint script runs mysql_install_db (or the equivalent in newer images) to create the system tables. The port 3306 is bound early — often before that work completes — so a telnet test succeeds. However, the server process is not yet ready to authenticate users. Any client that connects during this window will be aborted with the "communication packets" error.

The second trigger is the default wait_timeout. MySQL 5.7 sets wait_timeout to 28800 seconds (8 hours) for non‑interactive connections. If your application opens a connection and then sits idle — common with connection pools that hold connections open — the server will eventually kill it. The same error appears in the logs, though the timing is different (after hours, not seconds).

The Docker API does not wait for the container to be "healthy" before returning control, unlike docker-compose which can use depends_on with a healthcheck. That is why the programmatic approach fails while Compose succeeds.

The fix: wait for readiness, then check (don't blindly raise) the timeout#

The fix has two parts, and they are not equal: the readiness check is what resolves the startup race that causes this error in Docker. The timeout section exists to stop you from "fixing" a value that is already correct.

1. Add a healthcheck to the MySQL container#

If you are using docker run or docker-compose, add a healthcheck that uses mysqladmin ping:

yaml
# docker-compose.yml (healthcheck example)
services:
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: secret
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 5s
      retries: 10

When you create the container programmatically with the Docker API, you can implement the same logic by polling docker exec. Make sure both containers are on the same user-defined bridge network (not the default bridge) so that the hostname mysql resolves correctly.

ts
import Docker from 'dockerode';
 
// Wait for MySQL to be ready using dockerode
async function waitForMySQL(container: Docker.Container): Promise<void> {
  for (let i = 0; i < 30; i++) {
    const exec = await container.exec({
      Cmd: ["mysqladmin", "ping", "-h", "localhost"],
      AttachStdout: true,
      AttachStderr: true,
    });
    const stream = await exec.start({ hijack: true, Detach: false });
    const output = await new Promise<string>((resolve) => {
      let data = "";
      stream.on("data", (chunk: Buffer) => (data += chunk.toString()));
      stream.on("end", () => resolve(data));
    });
    if (output.includes("mysqld is alive")) return;
    await new Promise((r) => setTimeout(r, 2000));
  }
  throw new Error("MySQL did not become ready in time");
}

Call waitForMySQL(mysqlContainer) after mysql.start() and before creating your application containers.

2. Check wait_timeout before changing it#

Even after the initial connection succeeds, an idle connection can be killed. But check the live value first — on a stock image there is nothing to fix:

bash
docker exec <mysql-container> mysql -uroot -psecret   -e "SHOW VARIABLES LIKE 'wait_timeout';"

If it already reads 28800, this is not your problem and raising it to 28800 is a no-op. It is worth pinning only when a mounted my.cnf, a managed provider, or an image default has lowered it. To pin it explicitly, pass it to mysqld:

ts
// When creating the container with the Docker API
const mysql = await docker.container.create({
  Image: "mysql:5.7",
  // ... other options
  Args: [
    "mysqld",
    "--max-connections=1000",
    "--wait-timeout=28800",   // 8 hours; increase if needed
    // ... other flags
  ],
});

If you are using docker run:

bash
docker run -d --name mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:5.7 \
  --wait-timeout=28800

This is a guard, not an increase: 28800 is already the stock default, and pinning it only protects you from a config file that would otherwise lower it. If you genuinely hold connections idle for longer than eight hours — a background worker, a nightly job — then go above the default, e.g. --wait-timeout=86400 (24 hours).

3. Tune the Prisma connection pool (if you use Prisma)#

Prisma Client maintains a connection pool. If a pooled connection sits idle beyond MySQL's wait_timeout, the next query will fail with a similar error. Configure the pool to close idle connections before the timeout by adding parameters to your DATABASE_URL:

plaintext
DATABASE_URL="mysql://user:password@mysql:3306/mydb?connection_limit=5&pool_timeout=30"
  • connection_limit=5 caps the number of concurrent connections.
  • pool_timeout=30 (seconds) tells Prisma to close idle connections after 30 seconds, well before MySQL's 8‑hour timeout.

This prevents "communication packets" errors caused by stale connections. For more details, see the Prisma database connections guide.

Verify the fix#

After applying the readiness check and timeout changes, restart your stack and watch the MySQL logs:

bash
docker logs -f mysql_container_name

You should no longer see Aborted connection ... Got an error reading communication packets. Instead, successful connections will appear as:

plaintext
[Note] Access denied for user 'root'@'...' (using password: YES)   # if auth fails

or simply no aborted‑connection entries.

From the application container, run a quick query to confirm connectivity:

bash
docker exec app_container mysql -h mysql -u root -psecret -e "SELECT 1;"

Expected output:

plaintext
+---+
| 1 |
+---+
| 1 |
+---+

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Two patterns that still trip you up#

Pattern A — Using telnet as a readiness check#

A successful telnet mysql 3306 only proves the TCP port is open. MySQL binds the port early in its startup sequence, often before the data directory is fully initialised. Always use mysqladmin ping or a query‑based check (SELECT 1) to confirm the server is ready to accept authenticated connections.

Pattern B — Not setting wait_timeout for long‑running idle connections#

If your application uses a connection pool that holds connections open for hours, the default wait_timeout of 28800 seconds (8 hours) will eventually kill them. The error appears long after startup, making it hard to correlate. Explicitly set --wait-timeout to a value larger than your longest expected idle period, or configure your pool to close idle connections before the timeout (as shown in the Prisma example above).

Other configuration pitfalls#

max_allowed_packet too small#

MySQL rejects packets larger than max_allowed_packet (default 4 MB in 5.7, 16 MB in 8.0). If your application sends a query or data that exceeds this limit, the server aborts the connection with the same "Got an error reading communication packets" message. Increase the limit by passing --max-allowed-packet=256M (or a suitable value) to mysqld:

bash
docker run -d mysql:5.7 --max-allowed-packet=256M

net_read_timeout too short#

net_read_timeout (default 30 seconds) controls how long MySQL waits for the client to send data before aborting the connection. If your application experiences network latency or takes time to construct a large query, the server may kill the connection prematurely. Raise it with --net-read-timeout=60 (or higher) to give the client more time:

bash
docker run -d mysql:5.7 --net-read-timeout=60

FAQ#

Why does telnet to port 3306 succeed but MySQL still aborts connections?#

The port is open as soon as the MySQL process binds to it, but the server may still be running first‑time initialisation (creating system tables, etc.). During that window, MySQL accepts the TCP connection but cannot complete the authentication handshake, so it aborts the connection and logs "Got an error reading communication packets". A readiness check with mysqladmin ping avoids this race.

Should I increase wait_timeout to fix this error?#

Usually not. wait_timeout already defaults to 28800 seconds, so setting it to 28800 is a no-op — check the live value with SHOW VARIABLES LIKE 'wait_timeout' before touching it. Raise it only if a config file lowered it, or if you legitimately hold connections idle for more than eight hours. To pin or raise it:

bash
docker run -d mysql:5.7 --wait-timeout=28800

In Docker, though, this error is far more often the startup race described above than an idle timeout — fix the readiness check first.

Can I use the same approach with PostgreSQL?#

The underlying principle — waiting for the database to be ready before connecting — applies to any database. For PostgreSQL, you would use pg_isready instead of mysqladmin ping. If you're dealing with PostgreSQL connection issues in Docker, see Fix PostgreSQL Server Won't Start on Mac OS X (2026) for platform‑specific troubleshooting.

Further reading: Fix Prisma Query Engine Library Not Found, Fix: Sequelize authenticate() Hangs With No Success or Error.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote