diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..670ddaf
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+.git
+.gitignore
+README.md
+DEPLOY.md
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..5853810
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM nginx:1.27-alpine
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+WORKDIR /usr/share/nginx/html
+COPY index.html standalone.html styles.css data.js projects-data.js components.jsx tweaks-panel.jsx llms.txt ./
+COPY images ./images
+
+EXPOSE 43036
+
+HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
+ CMD wget -q --spider http://localhost:43036/ || exit 1
diff --git a/README.md b/README.md
index f844d00..5b4b878 100644
--- a/README.md
+++ b/README.md
@@ -20,15 +20,21 @@ standalone.html — fully self-contained single-file build (all images base6
## Serve
-Any static file server works:
+**Docker (production / persistent):**
```
-python3 -m http.server 8080
-# or
-npx serve .
+docker compose up -d --build
```
-Then open .
+Container `wilddragon-site` exposes nginx on port `43036`. Nginx Proxy Manager fronts it for `wilddragon.net`.
+
+**Local quick check:**
+
+```
+python3 -m http.server 8081
+```
+
+Then open .
## Single-file build
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c0618ad
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,13 @@
+services:
+ wilddragon:
+ build: .
+ image: wilddragon-site:latest
+ container_name: wilddragon-site
+ restart: unless-stopped
+ ports:
+ - "43036:43036"
+ healthcheck:
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:43036/"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..1152a99
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,38 @@
+server {
+ listen 43036 default_server;
+ listen [::]:43036 default_server;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ charset utf-8;
+
+ gzip on;
+ gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
+ gzip_min_length 1024;
+
+ # Long cache for static assets — they're content-addressed by path,
+ # bust via filename when they change.
+ location ~* \.(png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf)$ {
+ expires 30d;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # Short cache for the HTML/JS that drive the SPA so edits show up fast.
+ location ~* \.(html|css|js|jsx)$ {
+ expires 5m;
+ add_header Cache-Control "public, must-revalidate";
+ }
+
+ # Single-page app — unknown paths fall back to index.html, which
+ # handles routing via location.hash (#/projects/).
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # llms.txt — let crawlers pull it without rewrites.
+ location = /llms.txt {
+ default_type text/plain;
+ }
+}