Troubleshooting
PostgreSQL & databases

Fix MySQL "Access denied for user" in Django

Fix Django OperationalError 1045 Access denied for user when connecting to MySQL or MariaDB: match the user host part, use TCP instead of the socket, fix authentication plugins, and grant privileges on the right database.

Problem statement

Django refuses to start or migrate against MySQL or MariaDB:

django.db.utils.OperationalError: (1045, "Access denied for user 'myapp'@'localhost' (using password: YES)")

or (1044, "Access denied for user 'myapp'@'%' to database 'myapp'"). MySQL identifies an account by user and host together: 'myapp'@'localhost' and 'myapp'@'%' are different accounts with separate passwords and privileges. Most "but the password is right" cases are a host mismatch, a socket-versus-TCP difference, or an authentication plugin the client cannot use.

How MySQL matches an accountClient connectsuser + source hostPicks accountmost specific hostPassword checkedfor that accountPrivileges checkedon the database1045 means the account or password failed; 1044 means the account lacks rights on the database.
  1. Read the error: 1045 is login (account or password), 1044 or 1142 is privileges.
  2. List accounts: SELECT user, host, plugin FROM mysql.user WHERE user = 'myapp';.
  3. Create the account for the host Django connects from: 'myapp'@'localhost' for socket, 'myapp'@'127.0.0.1' or '%' for TCP.
  4. Grant privileges: GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';.
  5. Check for anonymous ''@'localhost' accounts that shadow yours, and remove them.

Step-by-step solution

1. See which accounts exist

SELECT user, host, plugin FROM mysql.user ORDER BY user, host;

Compare with the error. The host in the error message is how the server saw your client: localhost means a Unix socket connection, 127.0.0.1 means TCP from the same machine, and an IP or hostname means a remote client (including another Docker container).

2. Match the host part

MySQL treats HOST: 'localhost' in Django as "use the Unix socket", and the server sees the client as localhost. HOST: '127.0.0.1' forces TCP, and the server sees 127.0.0.1, which does not match an account defined for localhost. Create the account that matches how Django connects:

CREATE USER 'myapp'@'localhost' IDENTIFIED BY 'change-me';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';

For an app server on another machine or in a container:

CREATE USER 'myapp'@'10.0.0.%' IDENTIFIED BY 'change-me';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'10.0.0.%';

Avoid '%' on servers reachable from the internet; restrict to your private network.

user@host is one accountServer sees client asAccount to createHOST = localhostlocalhost (socket)✓'myapp'@'localhost'HOST = 127.0.0.1127.0.0.1 (TCP)✓'myapp'@'127.0.0.1'app in another containercontainer IP✓'myapp'@'172.18.%'app on another serverprivate IP✓'myapp'@'10.0.0.%'An account for localhost does not cover TCP connections to 127.0.0.1django-deployment.com
Create the account for the host the server actually sees, shown in the error message.

3. Remove anonymous accounts

Older installations ship anonymous accounts such as ''@'localhost'. Because MySQL picks the most specific host first and then the most specific user, an anonymous account on localhost can match before 'myapp'@'%', and your password is checked against the wrong account:

SELECT user, host FROM mysql.user WHERE user = '';
DROP USER ''@'localhost';

mysql_secure_installation removes these.

4. Fix authentication plugin mismatches

MySQL 8 defaults to caching_sha2_password. Old client libraries, some connectors, and certain proxies cannot use it and report access denied or "authentication plugin cannot be loaded". Update mysqlclient and the system libmysqlclient, or as a fallback switch the account:

ALTER USER 'myapp'@'localhost' IDENTIFIED WITH mysql_native_password BY 'change-me';

Note that mysql_native_password is deprecated and disabled by default in MySQL 8.4+, so upgrading the client is the better long-term fix. MariaDB uses mysql_native_password or ed25519.

5. Grant on the right database

Error 1044 means login worked but the account has no rights on that database:

SHOW GRANTS FOR 'myapp'@'localhost';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';

Django's test runner creates test_myapp, so for CI also grant on test_myapp.* or CREATE globally on a dedicated test server.

error code to fixErrorCause and fix1045 for 'myapp'@'localhost'no localhost account orwrong password; create it1045 for 'myapp'@'127.0.0.1'TCP needs its ownaccount; match the host1045 though password is rightanonymous accountshadows yours; drop itplugin caching_sha2 cannot loadold client library;upgrade mysqlclient1044 to database myapplogin fine, no grants;GRANT ON myapp.*The user@host in the message is the account MySQL trieddjango-deployment.com
Read the code and the user@host pair before changing passwords.

Verification check:

mysql -h 127.0.0.1 -u myapp -p myapp -e "SELECT CURRENT_USER(), DATABASE();"
python manage.py migrate --check

CURRENT_USER() shows the account MySQL matched, which may differ from the user name you typed if an anonymous or wildcard account won.

Explanation

MySQL's account model is unusual: identity is the pair of user name and client host, and the server sorts candidate accounts by host specificity before user specificity. That design lets one name have different passwords or privileges per network location, but it means the same user name can fail from one place and succeed from another. Django adds a second subtlety by mapping localhost to a socket connection, so changing HOST between localhost and 127.0.0.1 changes which account applies.

Edge cases / notes

  • Managed MySQL (RDS, Cloud SQL) usually requires TLS and network allow-listing; access denied can also mean the IP is not authorised.
  • Docker clients appear with their network IP; MYSQL_USER in the official image creates 'user'@'%' only on first initialisation of the volume.
  • Special characters in passwords must be URL-encoded in a DATABASE_URL.
  • skip-name-resolve makes the server use IPs only, so accounts defined with hostnames stop matching.

Next steps

FAQ

Why does it work from the mysql CLI but not Django?

The CLI may connect via the socket (localhost) while Django uses TCP (127.0.0.1), or the other way round, matching different accounts.

Is user@'%' the same as user@'localhost'?

No. They are separate accounts, and localhost socket connections do not match '%' on many setups.

Should I use mysql_native_password?

Only as a temporary workaround. Upgrade the client library to support caching_sha2_password.

What privileges does Django need?

On its own database: SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP, and REFERENCES for migrations.

Keep going