Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
node_modules
npm-debug.log
dist
logs
.cache
.git
.github
.vscode
.husky
Dockerfile
docker-compose.yml
README.md
LICENSE
biome.json
commitlint.config.mjs
commitlint.config.mjs
sea-config.json
45 changes: 28 additions & 17 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -1,47 +1,58 @@
name: Docker Image CI

on:
push:
branches: ['**']
release:
types: [created]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
name: Publish Docker image
name: Build and push Docker image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Get package version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Generate Docker meta
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/nodelink
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ steps.package-version.outputs.current-version}}
type=raw,value=latest

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
type=ref,event=branch
type=sha,prefix=
type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') || github.event_name == 'release' }}

- name: Build and Push
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64, linux/arm64
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
48 changes: 16 additions & 32 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,46 +1,30 @@
# Stage 1: Builder - Install dependencies
# Stage 1: Builder - Install dependencies and bundle
FROM node:25-alpine AS builder

# Install git (required for npm to install dependencies from GitHub)
RUN apk add --no-cache git

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json (if available) to leverage Docker cache
# Use wildcards to ensure both package.json and package-lock.json (or yarn.lock/pnpm-lock.yaml) are copied
COPY package.json ./

# Install production dependencies
# This command automatically handles package-lock.json if it exists, otherwise it creates one.
# For Bun, you might use 'bun install --production'.
RUN npm install

# Stage 2: Runner - Copy application code and run
FROM node:25-alpine

# Set working directory
WORKDIR /app

# Copy production dependencies from the builder stage
COPY --from=builder /app/node_modules ./node_modules

# Copy the rest of the application source code
# This includes the 'src' directory, default config, and package files for runtime information.
COPY src/ ./src/
COPY scripts/ ./scripts/
COPY config.default.js ./config.default.js
COPY package.json ./package.json
COPY plugins/ ./plugins/

RUN npm install --no-save esbuild && BUNDLE_ONLY=1 node scripts/build.js

# Expose the port the application listens on (default is 3000 from config.default.js)
EXPOSE 3000
# Stage 2: Runner - Minimal image with bundled output
FROM node:25-alpine

WORKDIR /app

# Set environment variables for configuration
# These can be overridden via docker-compose.yml or 'docker run -e'
# Example: NODELINK_SERVER_PASSWORD=your_secure_password
ENV NODELINK_SERVER_PORT=3000 \
NODELINK_SERVER_HOST=0.0.0.0 \
NODELINK_CLUSTER_ENABLED=true
# Copy bundled application and native modules from builder
COPY --from=builder /app/src ./src/
COPY --from=builder /app/dist/ ./dist/
COPY --from=builder /app/config.default.js ./config.default.js
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules/

# Command to run the application
# It uses the 'start' script defined in package.json
CMD ["npm", "start"]
CMD ["node", "--dns-result-order=ipv4first", "--openssl-legacy-provider", "dist/main.mjs"]
15 changes: 15 additions & 0 deletions config.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,21 @@ export default {
maxLayersMix: 5,
autoCleanup: true
},
externalApi: {
enabled: false,
baseUrl: '',
authorization: '',
deezer: { // Endpoints: GET /api/deezer/arls (returns { arls: [{ arl: 'arl_value', expires_at: 'iso_ts', license: 'license_value', api_key: 'api_key_value' } ...] }) and POST /api/deezer/report (body { arl: 'arl_value' })
enabled: false,
failureThreshold: 3, // Remove ARL from rotation after this many consecutive failures
refreshIntervalMs: 60 * 60 * 1000, // Refetch ARLs from API interval
poolMinSize: 2 // Trigger early refetch when pool drops below this size
},
youtube: { // Endpoint: GET /api/youtube/tokens (returns { tokens: [{ access_token: 'token_value', refresh_token: 'refresh_token_value', token_type: 'Bearer', expires_at: 'iso_ts' }, ...] }) and POST /api/youtube/tokens (body { access_token: 'token_value' })
enabled: false,
refreshIntervalMs: 20 * 60 * 60 * 1000 // Refetch tokens interval
}
},
plugins: [
/* {
name: 'nodelink-sample-plugin',
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ services:
# NODELINK_MIX_AUTOCLEANUP: "true"

# volumes:
# - ./config.js:/app/config.js
# - ./local-music:/app/local-music
# - ./logs:/app/logs
# - ./.cache:/app/.cache
Expand Down
5 changes: 5 additions & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ if (fs.existsSync(mediaplexPkgDir)) {
}
}

if (process.env.BUNDLE_ONLY) {
console.log('Bundle-only mode: skipping SEA binary creation.')
process.exit(0)
}

const filesToEmbed = {}
function scanDir(dir, base = '') {
for (const file of fs.readdirSync(dir)) {
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import 'dotenv/config'
import { GatewayEvents } from './constants.js'
import DosProtectionManager from './managers/dosProtectionManager.js'
import ExternalApiManager from './managers/externalApiManager.js'
import PlayerManager from './managers/playerManager.js'
import PluginManager from './managers/pluginManager.js'
import RateLimitManager from './managers/rateLimitManager.js'
Expand Down Expand Up @@ -174,6 +175,7 @@ class NodelinkServer extends EventEmitter {
this.statsManager = new statsManager(this)
this.rateLimitManager = new RateLimitManager(this)
this.dosProtectionManager = new DosProtectionManager(this)
this.externalApiManager = new ExternalApiManager(this)
this.pluginManager = new PluginManager(this)
this.sourceWorkerManager =
isClusterPrimary && options.cluster?.specializedSourceWorker?.enabled
Expand Down Expand Up @@ -1527,6 +1529,7 @@ class NodelinkServer extends EventEmitter {
await this.credentialManager.load()
await this.trackCacheManager.load()
await this.statsManager.initialize()
await this.externalApiManager.initialize()

// Ensure sources are initialized before proceeding
if (this._sourceInitPromise) await this._sourceInitPromise
Expand Down
Loading