Access denied means MySQL matched you to a user@host account and rejected the credentials — or no such account exists for your host. Get the host part right and it's usually solved.
'app'@'localhost' and 'app'@'10.0.0.%' are different accounts with different passwords/privileges. SELECT user, host FROM mysql.user; shows what exists.
Empty MYSQL_PASSWORD/DB_PASSWORD env, missing --password flag, or ~/.my.cnf not readable → MySQL never got a password to check.
ALTER USER 'app'@'%' IDENTIFIED BY ... but the client matches a more specific host entry ('app'@'localhost') with an old password — most-specific host rule wins.
sudo mysql -e "SELECT user, host, plugin FROM mysql.user WHERE user LIKE 'app%';"
ALTER USER 'app'@'%' IDENTIFIED BY 'strong-pass'; FLUSH PRIVILEGES;
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'%'; # '%' = any host — scope tighter on real networks
mysql -h <host> -u app -p -e 'SELECT current_user(), user();' # current_user() = the matched account; user() = who you claimed to be
current_user() vs user() in MySQL: if they differ, a broader host account (or anonymous ''@'localhost') matched first — that's the root of many phantom denials. caching_sha2_password (default in MySQL 8) breaks older clients/drivers; create the user with mysql_native_password or upgrade the connector.
MySQL rewrites your identity to the matched account. The shown host is your actual source IP — the account pattern that matched may be a different, more specific one than you intended. Compare current_user() after connecting.
Not for ALTER/GRANT via SQL — those take effect immediately. FLUSH matters only when you edit grant tables directly with INSERT/UPDATE.
Our most-documented failures, packaged as ready-to-ship starter kits: Docker, Kubernetes, and Terraform.
Browse the template store →One-time. Yours to modify. Instant download from the NinjaOps template store.