Skip to content
View in the app

A better way to browse. Learn more.

Learn more

MySQL in Docker, with the socket shared so host apps think it's local

Featured Replies

This setup came out of a PHP app's web installer telling me No such file or directory when I pointed it at the database. We'd moved MySQL into a container because upgrades and rollbacks are much easier than with the distro package, but the app itself ran on the host under nginx and PHP-FPM. Apps like that usually expect MySQL on the local Unix socket at /var/run/mysqld/mysqld.sock, the same as a native install. So this is how we do it now, with TCP on loopback only and the socket shared to the host.

Why localhost gave a file error

We'd only published the TCP port, and the installer had the database host set to localhost. With mysqli, the word localhost doesn't mean TCP at all. It tries the default Unix socket (/var/run/mysqld/mysqld.sock on Debian builds), and that socket didn't exist because MySQL was only listening on TCP inside a container. The error sounds like a missing file in the app, which sent me looking in the wrong place. You can fix it by entering 127.0.0.1 as the host to force TCP, or by sharing the socket as below so localhost works the way the app expects. We went with the socket, so the app config looks exactly like a native install.

You'll need Docker Engine with the Compose plugin, and root or sudo. Make sure no MySQL or MariaDB server package is installed on the host, because it will fight over port 3306 and /run/mysqld. Client packages are fine.

Project directory and root password

mkdir -p /opt/mysql/data
cd /opt/mysql

Put the root password in /opt/mysql/.env and lock it down:

MYSQL_ROOT_PASSWORD=change-me-to-a-long-random-string
chmod 600 /opt/mysql/.env

A socket directory that survives reboots

/var/run is a symlink to /run, which is a tmpfs, so anything you create there is gone after a reboot. A tmpfiles.d entry recreates it at boot with the right owner, before Docker starts the container. Without it, Docker creates the missing bind-mount source itself as a root-owned directory, and depending on the image and timing, mysqld may not be able to create its socket there. I'd rather not leave that to luck.

First check the UID of the mysql user inside the image:

docker run --rm mysql:8.4 id mysql

In the official image that's UID and GID 999. Create /etc/tmpfiles.d/mysqld-docker.conf:

d /run/mysqld 0755 999 999 -

The 0755 matters. The socket file itself is world-writable, but the host user (www-data, for example) has to be able to enter the directory, or the app gets "Permission denied" on the socket. Apply it now without rebooting:

systemd-tmpfiles --create /etc/tmpfiles.d/mysqld-docker.conf
ls -ld /run/mysqld

The compose file

/opt/mysql/compose.yaml:

services:
  mysql:
    image: mysql:8.4
    container_name: mysql
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:3306:3306"
    volumes:
      - ./data:/var/lib/mysql
      - /var/run/mysqld:/var/run/mysqld

127.0.0.1:3306:3306 publishes the port on loopback only. Leave the IP prefix off and Docker publishes on all interfaces, and Docker's own firewall rules are applied ahead of most host firewall setups, so a ufw "deny 3306" won't save you.

Bind-mounting /var/run/mysqld puts the container's socket directory on the host. When mysqld creates mysqld.sock inside the container, it shows up on the host at the same path.

Start it and check the socket

docker compose up -d
docker compose logs -f mysql      # wait for "ready for connections", then Ctrl+C
ls -l /var/run/mysqld/

You should see mysqld.sock. Confirm the server agrees on the path:

docker exec -it mysql mysql -uroot -p -e "SHOW VARIABLES LIKE 'socket';"

If your image version reports the socket somewhere else, change the bind mount to match that directory, or set the socket path explicitly in a config file mounted into /etc/mysql/conf.d/.

The app's database and user

docker exec -it mysql mysql -uroot -p
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'another-long-random-string';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';

Socket users and TCP users aren't the same

MySQL sees connections over the Unix socket as coming from localhost. Connections through the published TCP port arrive from the Docker bridge network instead, so a 'appuser'@'localhost' account won't match them. If the app will use TCP, create the user for the bridge subnet (or %, since the port is only on loopback). This is the part I always forget.

Making sure it holds up

From the host, with only the client package installed:

apt install -y default-mysql-client
mysql -u appuser -p appdb -e "SELECT CURRENT_USER(), @@hostname;"
mysql -h 127.0.0.1 -u root -p -e "SELECT 1;"
ss -ltnp | grep 3306

The first mysql command uses the socket (no host given), the second uses TCP. ss should show 3306 bound to 127.0.0.1 only. Then reboot the host and check that /run/mysqld exists, the container is up and the socket is back.

Create an account or sign in to comment

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.