Add Docker + nginx deployment for homelab

- Multi-stage Dockerfile: Node builds static assets, nginx serves them
- nginx proxies /api/* to football-data.org and injects X-Auth-Token
  server-side via FOOTBALL_API_KEY env var (key never in browser bundle)
- docker-compose.yml exposes the app on port 3000
- Extract apiFetch() helper: sends key in dev (Vite proxy), skips in prod
- .env.example updated with both dev and production key vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 07:40:20 +01:00
parent d8fba41ea5
commit 5db020132e
8 changed files with 70 additions and 24 deletions

View File

@@ -2,10 +2,9 @@ import { useState, useEffect, useCallback } from 'react'
import StandingsTable from './components/StandingsTable'
import TeamMatchesModal from './components/TeamMatchesModal'
import CompetitionPicker from './components/CompetitionPicker'
import { apiFetch } from './api'
import styles from './App.module.css'
const API_KEY = import.meta.env.VITE_FOOTBALL_API_KEY
export default function App() {
const [selectedCode, setSelectedCode] = useState('DED')
const [standings, setStandings] = useState(null)
@@ -20,24 +19,12 @@ export default function App() {
const closeModal = useCallback(() => setSelectedTeam(null), [])
useEffect(() => {
if (!API_KEY) {
setError('No API key found. Create a .env file with VITE_FOOTBALL_API_KEY=your_key')
setLoading(false)
return
}
setLoading(true)
setError(null)
setStandings(null)
setSelectedTeam(null)
fetch(`/api/competitions/${selectedCode}/standings`, {
headers: { 'X-Auth-Token': API_KEY },
})
.then((res) => {
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`)
return res.json()
})
apiFetch(`/competitions/${selectedCode}/standings`)
.then((data) => {
const total = data.standings?.find((s) => s.type === 'TOTAL')
setStandings(total?.table ?? [])

11
src/api.js Normal file
View File

@@ -0,0 +1,11 @@
// In dev, Vite proxies /api/* but doesn't inject the key, so we send it
// from the browser. In production the nginx proxy injects it server-side.
const DEV_KEY = import.meta.env.VITE_FOOTBALL_API_KEY
export function apiFetch(path) {
const headers = DEV_KEY ? { 'X-Auth-Token': DEV_KEY } : {}
return fetch(`/api${path}`, { headers }).then((res) => {
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`)
return res.json()
})
}

View File

@@ -1,8 +1,7 @@
import { useState, useEffect } from 'react'
import { apiFetch } from '../api'
import styles from './TeamMatchesModal.module.css'
const API_KEY = import.meta.env.VITE_FOOTBALL_API_KEY
export default function TeamMatchesModal({ team, season, competitionCode, onClose }) {
const [matches, setMatches] = useState(null)
const [loading, setLoading] = useState(true)
@@ -11,13 +10,7 @@ export default function TeamMatchesModal({ team, season, competitionCode, onClos
useEffect(() => {
const seasonYear = new Date(season.startDate).getFullYear()
fetch(`/api/teams/${team.id}/matches?competitions=${competitionCode}&season=${seasonYear}`, {
headers: { 'X-Auth-Token': API_KEY },
})
.then((res) => {
if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`)
return res.json()
})
apiFetch(`/teams/${team.id}/matches?competitions=${competitionCode}&season=${seasonYear}`)
.then((data) => {
setMatches(data.matches ?? [])
setLoading(false)