feat: add single-port reverse proxy deployment support
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
APP_ENV=development
|
APP_ENV=development
|
||||||
SERVER_HOST=0.0.0.0
|
SERVER_HOST=0.0.0.0
|
||||||
SERVER_PORT=4040
|
SERVER_PORT=5050
|
||||||
DATABASE_PATH=./data/app.db
|
DATABASE_PATH=./data/app.db
|
||||||
APP_ENCRYPTION_KEY=change-me-for-production
|
APP_ENCRYPTION_KEY=change-me-for-production
|
||||||
ARTWORK_CACHE_DIR=./data/artwork
|
ARTWORK_CACHE_DIR=./data/artwork
|
||||||
|
|||||||
@@ -680,9 +680,9 @@ Responsibilities:
|
|||||||
- [x] Persistent DB volume
|
- [x] Persistent DB volume
|
||||||
- [x] Persistent cache volume
|
- [x] Persistent cache volume
|
||||||
- [x] Music folder mount strategy
|
- [x] Music folder mount strategy
|
||||||
- [ ] Single public HTTPS port for web UI and Subsonic clients
|
- [x] Single app port for web UI and Subsonic clients
|
||||||
- [ ] Reverse proxy example
|
- [x] Reverse proxy example
|
||||||
- [ ] HTTPS deployment notes
|
- [x] HTTP/reverse proxy deployment notes
|
||||||
- [ ] Backup/restore notes
|
- [ ] Backup/restore notes
|
||||||
|
|
||||||
## Nice-to-Have After MVP
|
## Nice-to-Have After MVP
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
|||||||
const username = useSessionStore((state) => state.username)
|
const username = useSessionStore((state) => state.username)
|
||||||
const clearSession = useSessionStore((state) => state.clearSession)
|
const clearSession = useSessionStore((state) => state.clearSession)
|
||||||
const fullPlayerOpen = usePlayerStore((state) => state.fullPlayerOpen)
|
const fullPlayerOpen = usePlayerStore((state) => state.fullPlayerOpen)
|
||||||
|
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||||
const [paletteOpen, setPaletteOpen] = useState(false)
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
||||||
@@ -87,7 +88,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
|||||||
<div className="absolute right-0 top-10 z-30 w-64 overflow-hidden rounded-xl border border-[#24314f] bg-[#0d1528] shadow-2xl">
|
<div className="absolute right-0 top-10 z-30 w-64 overflow-hidden rounded-xl border border-[#24314f] bg-[#0d1528] shadow-2xl">
|
||||||
<div className="border-b border-[#24314f] px-4 py-3">
|
<div className="border-b border-[#24314f] px-4 py-3">
|
||||||
<div className="text-xl font-semibold text-white">{username ?? 'demo'}</div>
|
<div className="text-xl font-semibold text-white">{username ?? 'demo'}</div>
|
||||||
<div className="mt-1 text-sm text-slate-400">https://music.daemonlord.ru</div>
|
<div className="mt-1 text-sm text-slate-400">{currentOrigin || 'https://music.example.com'}</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="flex w-full items-center justify-between px-4 py-3 text-left text-sm text-slate-100 hover:bg-[#18233a]" type="button">
|
<button className="flex w-full items-center justify-between px-4 py-3 text-left text-sm text-slate-100 hover:bg-[#18233a]" type="button">
|
||||||
Сочетания клавиш
|
Сочетания клавиш
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export type PlaylistDetail = PlaylistSummary & {
|
|||||||
tracks: Track[]
|
tracks: Track[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:4040'
|
const API_BASE = import.meta.env.VITE_API_BASE ?? ''
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const token = useSessionStore.getState().token
|
const token = useSessionStore.getState().token
|
||||||
|
|||||||
@@ -14,5 +14,19 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:5050',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/rest': {
|
||||||
|
target: 'http://127.0.0.1:5050',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/health': {
|
||||||
|
target: 'http://127.0.0.1:5050',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
15
deploy/Caddyfile
Normal file
15
deploy/Caddyfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
:80 {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy app:5050 {
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
header_up X-Forwarded-Host {host}
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,5 +19,5 @@ COPY --from=backend-build /out/temporserv /app/temporserv
|
|||||||
COPY --from=web-build /src/apps/web/dist /app/web
|
COPY --from=web-build /src/apps/web/dist /app/web
|
||||||
RUN mkdir -p /app/data /music && chown -R appuser:appuser /app /music
|
RUN mkdir -p /app/data /music && chown -R appuser:appuser /app /music
|
||||||
USER appuser
|
USER appuser
|
||||||
EXPOSE 4040
|
EXPOSE 5050
|
||||||
CMD ["/app/temporserv"]
|
CMD ["/app/temporserv"]
|
||||||
|
|||||||
77
deploy/REVERSE_PROXY.md
Normal file
77
deploy/REVERSE_PROXY.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Reverse Proxy Deployment
|
||||||
|
|
||||||
|
The application is designed to run as a single HTTP service on one port.
|
||||||
|
|
||||||
|
Default internal URL:
|
||||||
|
|
||||||
|
- `http://127.0.0.1:5050`
|
||||||
|
|
||||||
|
This same origin serves:
|
||||||
|
|
||||||
|
- web UI on `/`
|
||||||
|
- internal web API on `/api/*`
|
||||||
|
- Subsonic API on `/rest/*`
|
||||||
|
- cover art and streaming on the same host
|
||||||
|
|
||||||
|
That means mobile and TV Subsonic clients should use the same base URL as the browser.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- web UI: `http://your-host:5050/`
|
||||||
|
- Subsonic client server URL: `http://your-host:5050`
|
||||||
|
|
||||||
|
## Direct Docker Run
|
||||||
|
|
||||||
|
Use the root `docker-compose.yml`.
|
||||||
|
|
||||||
|
It publishes:
|
||||||
|
|
||||||
|
- `5050:5050`
|
||||||
|
|
||||||
|
After startup the app is available at:
|
||||||
|
|
||||||
|
- `http://localhost:5050`
|
||||||
|
|
||||||
|
## External Reverse Proxy
|
||||||
|
|
||||||
|
If you later publish the service through another reverse proxy, forward the entire host to the same upstream:
|
||||||
|
|
||||||
|
- upstream: `http://app-host:5050`
|
||||||
|
|
||||||
|
Do not split web and Subsonic traffic across different public ports.
|
||||||
|
|
||||||
|
Forward all of these paths to the same backend:
|
||||||
|
|
||||||
|
- `/`
|
||||||
|
- `/api/*`
|
||||||
|
- `/rest/*`
|
||||||
|
- `/health`
|
||||||
|
|
||||||
|
## Caddy Example
|
||||||
|
|
||||||
|
See [deploy/Caddyfile](C:\Users\benya\TemporServ\deploy\Caddyfile).
|
||||||
|
|
||||||
|
This example listens on plain HTTP and proxies all requests to `app:5050`.
|
||||||
|
|
||||||
|
## Nginx Example
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5050;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- In production the frontend uses relative URLs, so it works correctly behind the same origin without hardcoded API hosts.
|
||||||
|
- In local frontend development, Vite proxies `/api`, `/rest`, and `/health` to `http://127.0.0.1:5050`.
|
||||||
|
- If you later enable HTTPS on an external reverse proxy, clients should still connect to one public base URL only.
|
||||||
@@ -6,15 +6,18 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: deploy/Dockerfile
|
dockerfile: deploy/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "4040:4040"
|
- "5050:5050"
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: production
|
APP_ENV: production
|
||||||
SERVER_HOST: 0.0.0.0
|
SERVER_HOST: 0.0.0.0
|
||||||
SERVER_PORT: 4040
|
SERVER_PORT: 5050
|
||||||
DATABASE_PATH: /app/data/app.db
|
DATABASE_PATH: /app/data/app.db
|
||||||
|
APP_ENCRYPTION_KEY: change-me-for-production
|
||||||
|
ARTWORK_CACHE_DIR: /app/data/artwork
|
||||||
MEDIA_ROOT: /music
|
MEDIA_ROOT: /music
|
||||||
CORS_ORIGINS: http://localhost:4040
|
CORS_ORIGINS: http://localhost:5050
|
||||||
|
DEFAULT_ADMIN_USERNAME: demo
|
||||||
|
DEFAULT_ADMIN_PASSWORD: demo
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./media:/music
|
- ./media:/music
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func Load() Config {
|
|||||||
return Config{
|
return Config{
|
||||||
AppEnv: getenv("APP_ENV", "development"),
|
AppEnv: getenv("APP_ENV", "development"),
|
||||||
ServerHost: getenv("SERVER_HOST", "0.0.0.0"),
|
ServerHost: getenv("SERVER_HOST", "0.0.0.0"),
|
||||||
ServerPort: getenv("SERVER_PORT", "4040"),
|
ServerPort: getenv("SERVER_PORT", "5050"),
|
||||||
DatabasePath: getenv("DATABASE_PATH", "./data/app.db"),
|
DatabasePath: getenv("DATABASE_PATH", "./data/app.db"),
|
||||||
EncryptionKey: getenv("APP_ENCRYPTION_KEY", "temporserv-dev-insecure-key"),
|
EncryptionKey: getenv("APP_ENCRYPTION_KEY", "temporserv-dev-insecure-key"),
|
||||||
ArtworkCacheDir: getenv("ARTWORK_CACHE_DIR", "./data/artwork"),
|
ArtworkCacheDir: getenv("ARTWORK_CACHE_DIR", "./data/artwork"),
|
||||||
|
|||||||
Reference in New Issue
Block a user