Skip to content

Commit ef33dca

Browse files
authored
Merge pull request #4 from TheLeggett/feature/docker
Add Docker support with configurable SD card mounting
2 parents 1736910 + 3a5321a commit ef33dca

11 files changed

Lines changed: 406 additions & 25 deletions

File tree

‎.dockerignore‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build output (rebuilt in container)
5+
dist/
6+
7+
# Local data and config
8+
.local/
9+
.env
10+
.env.local
11+
*.log
12+
13+
# Git
14+
.git/
15+
.gitignore
16+
17+
# IDE
18+
.vscode/
19+
.idea/
20+
*.swp
21+
*.swo
22+
23+
# OS files
24+
.DS_Store
25+
Thumbs.db
26+
27+
# Test files
28+
*.test.ts
29+
*.spec.ts
30+
__tests__/
31+
coverage/
32+
33+
# Docker files (avoid recursive)
34+
Dockerfile
35+
docker-compose*.yml
36+
.dockerignore
37+
38+
# Documentation (not needed at runtime)
39+
*.md
40+
LICENSE

‎.env.example‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Path to your Analogue 3D SD card
2+
# Default: /Volumes/ANALOGUE 3D (standard macOS mount point)
3+
# Linux: /media/$USER/ANALOGUE 3D or /run/media/$USER/ANALOGUE 3D
4+
# Custom SD card name: /Volumes/YOUR_SD_CARD_NAME
5+
SD_VOLUMES_PATH=/Volumes/ANALOGUE 3D
6+
17
# Read label artwork directly from SD card instead of local labels.db
28
# This is very slow, but keeping here for test purposes.
39
# Useful for testing SD card read performance during browsing.

‎Dockerfile‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Build stage
2+
FROM node:20-slim AS builder
3+
4+
WORKDIR /app
5+
6+
# Copy package files
7+
COPY package*.json ./
8+
9+
# Install all dependencies (including devDependencies for build)
10+
RUN npm ci
11+
12+
# Copy source files
13+
COPY . .
14+
15+
# Build the application
16+
RUN npm run build
17+
18+
# Production stage
19+
FROM node:20-slim AS production
20+
21+
WORKDIR /app
22+
23+
# Install dumb-init for proper signal handling
24+
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init && rm -rf /var/lib/apt/lists/*
25+
26+
# Create non-root user for security
27+
RUN groupadd --gid 1001 nodejs && \
28+
useradd --uid 1001 --gid nodejs --shell /bin/bash --create-home nodejs
29+
30+
# Copy package files
31+
COPY package*.json ./
32+
33+
# Install production dependencies only
34+
# Sharp requires additional setup for prebuilt binaries
35+
RUN npm ci --omit=dev
36+
37+
# Copy built frontend from builder stage
38+
COPY --chown=nodejs:nodejs --from=builder /app/dist ./dist
39+
40+
# Copy server source (tsx runs TypeScript directly)
41+
COPY --chown=nodejs:nodejs --from=builder /app/server ./server
42+
43+
# Copy tsconfig files for tsx
44+
COPY --chown=nodejs:nodejs --from=builder /app/tsconfig.json ./
45+
COPY --chown=nodejs:nodejs --from=builder /app/tsconfig.server.json ./
46+
47+
# Copy data files (cart name database)
48+
COPY --chown=nodejs:nodejs --from=builder /app/data ./data
49+
50+
# Create local data directory with correct permissions
51+
RUN mkdir -p .local/Library/N64/Games .local/Library/N64/Images && \
52+
chown -R nodejs:nodejs .local
53+
54+
# Switch to non-root user
55+
USER nodejs
56+
57+
# Expose port
58+
EXPOSE 3001
59+
60+
# Environment variables
61+
ENV NODE_ENV=production
62+
ENV PORT=3001
63+
64+
# Health check
65+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
66+
CMD node -e "fetch('http://localhost:3001/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"
67+
68+
# Use dumb-init to handle signals properly
69+
ENTRYPOINT ["dumb-init", "--"]
70+
71+
# Start the server
72+
CMD ["npm", "start"]

‎README.md‎

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ Comprehensive N64 cartridge database:
134134
- Node.js 18 or higher
135135
- An Analogue 3D with an SD card
136136

137-
### Installation
137+
### Installation (Recommended)
138+
139+
Native installation is recommended for the best experience, especially if you frequently plug/unplug your SD card.
138140

139141
```bash
140142
# Clone the repository
@@ -148,7 +150,93 @@ npm install
148150
npm run dev
149151
```
150152

151-
The app will open at `http://localhost:5173` with the backend API running on port 3001.
153+
The app will open at `http://localhost:5173` with the backend API running on port 3001. Your SD card will be detected automatically and you can eject/re-insert it freely while the app runs.
154+
155+
### Docker Installation
156+
157+
A3D Manager can also run in Docker, though with some limitations around SD card handling:
158+
159+
```bash
160+
# Using Docker Compose (recommended)
161+
docker compose up -d
162+
163+
# Or build and run manually
164+
docker build -t a3d-manager .
165+
docker run -d \
166+
--name a3d-manager \
167+
-p 3001:3001 \
168+
-v a3d-data:/app/.local \
169+
a3d-manager
170+
```
171+
172+
The app will be available at `http://localhost:3001`.
173+
174+
#### SD Card Access
175+
176+
The Docker container mounts your Analogue 3D SD card directly. By default, it expects the standard mount path `/Volumes/ANALOGUE 3D`.
177+
178+
> **Important:** Your SD card must be connected and mounted **before** running `docker compose up -d`. If the SD card isn't mounted, Docker will fail with a "permission denied" error.
179+
180+
**Connecting your SD Card:**
181+
182+
You can either use an external SD card reader, or connect directly to your Analogue 3D via USB-C:
183+
184+
1. Fully power off your Analogue 3D and disconnect all controllers and cables
185+
2. Leave the SD card inserted in the Analogue 3D
186+
3. Connect the Analogue 3D's power port to your computer using USB-C and wait 5 seconds
187+
4. Press and hold the reset button, then while holding reset, press and hold the power switch
188+
5. Hold both buttons until the Power LED turns green, then release
189+
6. Your SD card should now appear as `ANALOGUE 3D` on your computer
190+
191+
See [Analogue's guide](https://www.analogue.co/support/3d/guide/getting-started#updating-3dos) for more details.
192+
193+
**Initial Setup (macOS):**
194+
195+
1. Connect your SD card (see above)
196+
2. Run `docker compose up -d`
197+
198+
**Initial Setup (Linux):**
199+
200+
1. Create a `.env` file with your SD card path:
201+
```bash
202+
cp .env.example .env
203+
# Edit SD_VOLUMES_PATH, e.g.:
204+
SD_VOLUMES_PATH=/media/$USER/ANALOGUE 3D
205+
```
206+
2. Run `docker compose up -d`
207+
208+
**If your SD card has a different name**, create a `.env` file:
209+
```bash
210+
SD_VOLUMES_PATH=/Volumes/YOUR_SD_CARD_NAME
211+
```
212+
213+
#### Ejecting the SD Card (Important)
214+
215+
Due to how Docker Desktop works on macOS, you must **quit Docker Desktop entirely** before ejecting your SD card. The Docker VM keeps file shares active even when containers are stopped.
216+
217+
**Workflow for ejecting:**
218+
219+
1. Stop the container: `docker compose down`
220+
2. Quit Docker Desktop (click Docker icon in menu bar → Quit Docker Desktop)
221+
3. Eject your SD card normally
222+
223+
**When ready to use again:**
224+
225+
1. Connect your SD card first (it must be mounted before starting Docker)
226+
2. Start Docker Desktop
227+
3. Run `docker compose up -d`
228+
229+
> **Note:** If you frequently need to eject your SD card, consider using the [native installation](#installation-recommended) instead, which has no restrictions on SD card ejection.
230+
231+
#### Docker Configuration
232+
233+
| Variable | Default | Description |
234+
|----------|---------|-------------|
235+
| `PORT` | `3001` | Server port |
236+
| `SD_VOLUMES_PATH` | `/Volumes/ANALOGUE 3D` | Path to your Analogue 3D SD card |
237+
| `TRANSFER_CHUNK_SIZE` | `2097152` | File transfer chunk size (bytes) |
238+
| `TRANSFER_FSYNC_PER_CHUNK` | `true` | Sync after each chunk for accurate progress |
239+
| `READ_LABELS_FROM_SD` | `false` | Read labels directly from SD card (slower) |
152240

153241
### Quick Start
154242

‎docker-compose.yml‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
services:
2+
a3d-manager:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
image: a3d-manager:latest
7+
container_name: a3d-manager
8+
ports:
9+
- "3001:3001"
10+
volumes:
11+
# Persist local library data (cartridge art, games metadata)
12+
- a3d-data:/app/.local
13+
# Mount SD card for detection and sync
14+
# Configure via SD_VOLUMES_PATH in .env file
15+
# Default: /Volumes/ANALOGUE 3D (standard Analogue 3D SD card name)
16+
- "${SD_VOLUMES_PATH:-/Volumes/ANALOGUE 3D}:${SD_VOLUMES_PATH:-/Volumes/ANALOGUE 3D}:rw"
17+
environment:
18+
- NODE_ENV=production
19+
- PORT=3001
20+
- SD_VOLUMES_PATH=${SD_VOLUMES_PATH:-/Volumes/ANALOGUE 3D}
21+
# File transfer settings
22+
- TRANSFER_CHUNK_SIZE=2097152
23+
- TRANSFER_FSYNC_PER_CHUNK=true
24+
# Set to 'true' to read labels directly from SD card (slower)
25+
- READ_LABELS_FROM_SD=false
26+
restart: unless-stopped
27+
healthcheck:
28+
test: ["CMD", "node", "-e", "fetch('http://localhost:3001/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"]
29+
interval: 30s
30+
timeout: 10s
31+
retries: 3
32+
start_period: 5s
33+
34+
volumes:
35+
a3d-data:
36+
name: a3d-manager-data

‎docs/TESTING.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ tests/
3131
├── cartridge-data/
3232
│ ├── tests.ts # Cartridge data tests
3333
│ └── output/ # Generated output (gitignored)
34+
├── sd-card/
35+
│ └── tests.ts # SD card configuration tests
3436
└── game-data/
3537
└── fixtures/ # Shared fixtures (settings.json, controller_pak.img)
3638
@@ -122,6 +124,16 @@ Tests for cartridge ownership tracking, settings parsing, and game pak operation
122124

123125
---
124126

127+
## SD Card Configuration Tests (4 tests)
128+
129+
Tests for SD card detection and Docker volume path configuration.
130+
131+
| Category | Tests | Description |
132+
|----------|-------|-------------|
133+
| Volumes Path | 4 | SD_VOLUMES_PATH env var, default /Volumes, Linux/macOS paths |
134+
135+
---
136+
125137
## Interactive Benchmarks (Settings Page)
126138

127139
The Settings page (`/settings`) includes interactive benchmarks for testing SD card performance with a connected SD card.

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dev:client": "vite",
1212
"dev:server": "tsx watch server/index.ts",
1313
"build": "tsc -b && vite build",
14+
"start": "NODE_ENV=production node --import tsx server/index.ts",
1415
"lint": "eslint .",
1516
"preview": "vite preview"
1617
},
@@ -25,7 +26,8 @@
2526
"react": "^19.2.0",
2627
"react-dom": "^19.2.0",
2728
"react-router-dom": "^7.11.0",
28-
"sharp": "^0.33.5"
29+
"sharp": "^0.33.5",
30+
"tsx": "^4.19.4"
2931
},
3032
"devDependencies": {
3133
"@eslint/js": "^9.39.1",
@@ -43,7 +45,6 @@
4345
"eslint-plugin-react-hooks": "^7.0.1",
4446
"eslint-plugin-react-refresh": "^0.4.24",
4547
"globals": "^16.5.0",
46-
"tsx": "^4.19.4",
4748
"typescript": "~5.9.3",
4849
"typescript-eslint": "^8.46.4",
4950
"vite": "^7.2.4"

‎server/index.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ app.get('/api/health', (_req, res) => {
3636
res.json({ status: 'ok', timestamp: new Date().toISOString() });
3737
});
3838

39+
// Serve static files in production
40+
if (process.env.NODE_ENV === 'production') {
41+
const distPath = path.join(process.cwd(), 'dist');
42+
app.use(express.static(distPath));
43+
44+
// SPA fallback - serve index.html for all non-API routes
45+
app.get('*', (_req, res) => {
46+
res.sendFile(path.join(distPath, 'index.html'));
47+
});
48+
}
49+
3950
// Start server
4051
async function start() {
4152
await ensureLocalDirs();

0 commit comments

Comments
 (0)