fix(docker): Compose v5 pids_limit + copy public assets (#327)

* fix(docker): move pids to service-level pids_limit and copy public assets

- Move `pids: 256` from `deploy.resources.limits` to service-level
  `pids_limit` for Docker Compose v5+ compatibility (#322)
- Add `COPY --from=build /app/public ./public` to Dockerfile runtime
  stage so static assets (logos, icons) are available at runtime (#323)

Closes #322, closes #323

* test: add Docker config validation tests

Verifies pids_limit is at service level (Compose v5+), public dir
is copied in Dockerfile, and all required COPY instructions exist.
This commit is contained in:
nyk 2026-03-14 14:17:57 +07:00 committed by GitHub
parent 8d455de39e
commit 5208a76b76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 50 additions and 1 deletions

View File

@ -33,6 +33,7 @@ ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
# Copy schema.sql needed by migration 001_init at runtime
COPY --from=build /app/src/lib/schema.sql ./src/lib/schema.sql
# Create data directory with correct ownership for SQLite

View File

@ -21,12 +21,12 @@ services:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
pids_limit: 256
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
pids: 256
networks:
- mc-net
restart: unless-stopped

View File

@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import { readFileSync } from 'fs'
import { resolve } from 'path'
/**
* Tests that docker-compose.yml and Dockerfile contain the expected
* configuration for Compose v5+ compatibility and complete runtime assets.
*/
const ROOT = resolve(__dirname, '../../..')
describe('docker-compose.yml schema', () => {
const content = readFileSync(resolve(ROOT, 'docker-compose.yml'), 'utf-8')
it('uses service-level pids_limit instead of deploy.resources.limits.pids', () => {
// pids_limit should be at service level (not nested inside deploy)
expect(content).toContain('pids_limit:')
// Should NOT have pids inside deploy.resources.limits
const deployBlock = content.match(/deploy:[\s\S]*?(?=\n\s{4}\w|\nvolumes:|\nnetworks:)/)?.[0] ?? ''
expect(deployBlock).not.toContain('pids:')
})
it('still has memory and cpus in deploy.resources.limits', () => {
expect(content).toContain('memory:')
expect(content).toContain('cpus:')
})
})
describe('Dockerfile runtime stage', () => {
const content = readFileSync(resolve(ROOT, 'Dockerfile'), 'utf-8')
it('copies public directory to runtime stage', () => {
expect(content).toContain('COPY --from=build /app/public ./public')
})
it('copies standalone output', () => {
expect(content).toContain('COPY --from=build /app/.next/standalone ./')
})
it('copies static assets', () => {
expect(content).toContain('COPY --from=build /app/.next/static ./.next/static')
})
it('copies schema.sql for migrations', () => {
expect(content).toContain('schema.sql')
})
})