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.
- Symptom: MySQL logs "Got an error reading communication packets" and aborts connections from an app container.
-
Root cause: The app connects during MySQL's initialization window — port 3306 is bound before the server can authenticate. Idle connections exceeding
wait_timeoutare a separate, slower-burning cause. -
Fix: Add a healthcheck / readiness probe. Only touch
wait_timeoutifSHOW VARIABLESproves it was lowered below its 28800-second default. -
Verification: Run
mysqladmin pingfrom the app container; the error disappears from MySQL logs.
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:
[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:
# 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.
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:
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:
// 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:
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:
DATABASE_URL="mysql://user:password@mysql:3306/mydb?connection_limit=5&pool_timeout=30"
-
connection_limit=5caps 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:
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:
[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:
docker exec app_container mysql -h mysql -u root -psecret -e "SELECT 1;"
Expected output:
+---+
| 1 |
+---+
| 1 |
+---+
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:
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:
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:
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.
Related
- Prisma: Can't reach database server at database:5432 on M1 — similar connectivity race with Prisma and PostgreSQL on Apple Silicon.
- Docker Tutorial 2026: Dev Environment with Compose — how to set up a reproducible dev environment with healthchecks and proper startup ordering.
- Extract date (yyyy/mm/dd) from a timestamp in PostgreSQL — useful when you need to parse timestamps in logs while debugging connection issues.
Originally published at https://www.iloveblogs.blog
Top comments (0)