Menu
Login / Register

API & INTEGRATIONS

Connecting a Flask App on Your Host to MySQL Running Inside an LXD Container

By John Simiyu • jonysimiyu75@gmail.com

A common setup: MySQL runs inside an LXD container alongside a legacy PHP application, while a newer Flask service runs directly on the host. Both need to talk to the same database. This sounds straightforward until you hit the classic error:

mysql.connector.errors.ProgrammingError: 1045 (28000): Access denied for user 'appuser'@'localhost' (using password: YES)

The root cause is almost always the same: localhost means something different depending on where you are standing. Inside the container, localhost refers to the container itself. From the host, connecting to the container's MySQL instance is a real network connection, not a loopback connection, even if it feels local to you.

Step 1: Confirm the actual network path

Find the container's LXD bridge IP and the host's IP as seen from inside the container:

# Container's own IP on the LXD bridge
lxc list

# Host's IP as seen from inside the container (usually the bridge gateway)
lxc exec my-container -- ip route | grep default

Step 2: Grant MySQL access scoped to the host's bridge IP

Avoid a wide-open 'user'@'%' grant. Instead, scope it specifically to the host's address on the LXD bridge:

CREATE USER IF NOT EXISTS 'appuser'@'10.x.x.1' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON your_database.* TO 'appuser'@'10.x.x.1';
FLUSH PRIVILEGES;

Keep the original 'appuser'@'localhost' grant in place if another service inside the container still needs it. The two grants coexist independently.

Step 3: Check MySQL's bind-address

Even with the correct grant, MySQL may still refuse the connection if it is only listening on loopback. Check:

grep bind-address /etc/mysql/mysql.conf.d/mysqld.cnf

If it is set to 127.0.0.1, MySQL is only accepting connections from inside the container itself, regardless of any grants you have added. Change it to the container's own LXD bridge IP, deliberately not 0.0.0.0, to avoid exposing MySQL more broadly than necessary:

bind-address = 10.x.x.x

Watch out: if you are editing this with a script or sed, double-check it does not also modify unrelated settings like mysqlx-bind-address if your pattern matches too broadly.

Step 4: Restart the correct service

Do not assume the service is named mariadb. On many Ubuntu installs running MySQL (not MariaDB), the actual systemd unit is mysql.service:

systemctl restart mysql
systemctl status mysql

Step 5: Update your Flask app's DB config

Point your Flask app's database host setting at the container's bridge IP rather than localhost:

DB_CONFIG = {
    'host': '10.x.x.x',
    'user': 'appuser',
    'password': 'your_password',
    'database': 'your_database'
}

Step 6: Verify

Restart your Flask app and test a query-dependent endpoint. A clean JSON error response (rather than a 500 with a connector traceback) confirms the connection itself is working, even before your application logic is fully correct.

Summary checklist

  1. Identify the real network path between host and container (bridge IPs).
  2. Add a MySQL grant scoped to the host's bridge IP, not a wildcard.
  3. Confirm and fix bind-address if it is restricted to loopback.
  4. Confirm the actual systemd service name before restarting.
  5. Update your application's DB host config to match.

This pattern applies any time two services are split across a host/container boundary and both happen to think of themselves as localhost. It is worth checking all four of these points together rather than assuming a single fix, like the grant alone, will resolve it.