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.
- Read the error:
1045is login (account or password),1044or1142is privileges. - List accounts:
SELECT user, host, plugin FROM mysql.user WHERE user = 'myapp';. - Create the account for the host Django connects from:
'myapp'@'localhost'for socket,'myapp'@'127.0.0.1'or'%'for TCP. - Grant privileges:
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp'@'localhost';. - 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.
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.
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_USERin 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-resolvemakes the server use IPs only, so accounts defined with hostnames stop matching.
Next steps
- Configure the backend fully with Configure Django With MySQL in Production.
- If the driver itself will not install, see Fix mysqlclient Install Errors.
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.