diff --git a/datasette/cli.py b/datasette/cli.py
index 12ee92c3..09aebcc8 100644
--- a/datasette/cli.py
+++ b/datasette/cli.py
@@ -333,6 +333,10 @@ def uninstall(packages, yes):
type=click.IntRange(0, 65535),
help="Port for server, defaults to 8001. Use -p 0 to automatically assign an available port.",
)
+@click.option(
+ "--uds",
+ help="Bind to a Unix domain socket",
+)
@click.option(
"--reload",
is_flag=True,
@@ -428,6 +432,7 @@ def serve(
immutable,
host,
port,
+ uds,
reload,
cors,
sqlite_extensions,
@@ -569,6 +574,8 @@ def serve(
uvicorn_kwargs = dict(
host=host, port=port, log_level="info", lifespan="on", workers=1
)
+ if uds:
+ uvicorn_kwargs["uds"] = uds
if ssl_keyfile:
uvicorn_kwargs["ssl_keyfile"] = ssl_keyfile
if ssl_certfile:
diff --git a/docs/datasette-serve-help.txt b/docs/datasette-serve-help.txt
index db51dd80..ec3f41a0 100644
--- a/docs/datasette-serve-help.txt
+++ b/docs/datasette-serve-help.txt
@@ -12,6 +12,7 @@ Options:
machines.
-p, --port INTEGER RANGE Port for server, defaults to 8001. Use -p 0 to automatically
assign an available port. [0<=x<=65535]
+ --uds TEXT Bind to a Unix domain socket
--reload Automatically reload if code or metadata change detected -
useful for development
--cors Enable CORS by serving Access-Control-Allow-Origin: *
diff --git a/docs/deploying.rst b/docs/deploying.rst
index 44ddd07b..f3680034 100644
--- a/docs/deploying.rst
+++ b/docs/deploying.rst
@@ -148,7 +148,6 @@ Here is an example of an `nginx `__ configuration file that
http {
server {
listen 80;
-
location /my-datasette {
proxy_pass http://127.0.0.1:8009/my-datasette;
proxy_set_header X-Real-IP $remote_addr;
@@ -157,6 +156,28 @@ Here is an example of an `nginx `__ configuration file that
}
}
+You can also use the ``--uds`` option to Datasette to listen on a Unix domain socket instead of a port, configuring the nginx upstream proxy like this::
+
+ daemon off;
+ events {
+ worker_connections 1024;
+ }
+ http {
+ server {
+ listen 80;
+ location / {
+ proxy_pass http://datasette;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+ }
+ upstream datasette {
+ server unix:/tmp/datasette.sock;
+ }
+ }
+
+Then run Datasette with ``datasette --uds /tmp/datasette.sock path/to/database.db``.
+
Apache proxy configuration
--------------------------
diff --git a/tests/conftest.py b/tests/conftest.py
index c6a3eee6..34a64efc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -131,7 +131,6 @@ def ds_localhost_https_server(tmp_path_factory):
for blob in server_cert.cert_chain_pems:
blob.write_to_path(path=certfile, append=True)
ca.cert_pem.write_to_path(path=client_cert)
-
ds_proc = subprocess.Popen(
[
"datasette",
@@ -154,3 +153,22 @@ def ds_localhost_https_server(tmp_path_factory):
yield ds_proc, client_cert
# Shut it down at the end of the pytest session
ds_proc.terminate()
+
+
+@pytest.fixture(scope="session")
+def ds_unix_domain_socket_server(tmp_path_factory):
+ socket_folder = tmp_path_factory.mktemp("uds")
+ uds = str(socket_folder / "datasette.sock")
+ ds_proc = subprocess.Popen(
+ ["datasette", "--memory", "--uds", uds],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ cwd=tempfile.gettempdir(),
+ )
+ # Give the server time to start
+ time.sleep(1.5)
+ # Check it started successfully
+ assert not ds_proc.poll(), ds_proc.stdout.read().decode("utf-8")
+ yield ds_proc, uds
+ # Shut it down at the end of the pytest session
+ ds_proc.terminate()
diff --git a/tests/test_cli.py b/tests/test_cli.py
index e094ccb6..e31a305e 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -132,6 +132,7 @@ def test_metadata_yaml():
immutable=[],
host="127.0.0.1",
port=8001,
+ uds=None,
reload=False,
cors=False,
sqlite_extensions=[],
diff --git a/tests/test_cli_serve_server.py b/tests/test_cli_serve_server.py
index 6f5366d1..73439125 100644
--- a/tests/test_cli_serve_server.py
+++ b/tests/test_cli_serve_server.py
@@ -1,5 +1,6 @@
import httpx
import pytest
+import socket
@pytest.mark.serial
@@ -21,3 +22,17 @@ def test_serve_localhost_https(ds_localhost_https_server):
"path": "/_memory",
"tables": [],
}.items() <= response.json().items()
+
+
+@pytest.mark.serial
+@pytest.mark.skipif(not hasattr(socket, "AF_UNIX"), reason="Requires socket.AF_UNIX support")
+def test_serve_unix_domain_socket(ds_unix_domain_socket_server):
+ _, uds = ds_unix_domain_socket_server
+ transport = httpx.HTTPTransport(uds=uds)
+ client = httpx.Client(transport=transport)
+ response = client.get("http://localhost/_memory.json")
+ assert {
+ "database": "_memory",
+ "path": "/_memory",
+ "tables": [],
+ }.items() <= response.json().items()