Skip to content

Feature: Docker Support - Dockerfile, Compose, and Container Registry #99

Description

@melvincarvalho

Summary

Add first-class Docker support to JavaScriptSolidServer, enabling users to deploy with a single docker run command. This is a P0 priority item as Docker is the expected deployment method for modern self-hosted software.

Difficulty: 30/100
Estimated Effort: 2-3 days
Dependencies: None


Current State

  • ❌ No Dockerfile
  • ❌ No docker-compose.yml
  • ❌ No .dockerignore
  • ❌ No container registry images
  • ❌ No health check endpoint
  • ❌ No Docker documentation

The only Docker reference is for running CTH tests (external container).


Proposed Implementation

1. Dockerfile (Multi-Stage Build)

# ============================================
# Stage 1: Builder
# ============================================
FROM node:22-alpine AS builder

WORKDIR /app

# Install dependencies first (cache optimization)
COPY package*.json ./
RUN npm ci

# Copy source
COPY . .

# ============================================
# Stage 2: Production
# ============================================
FROM node:22-alpine AS production

# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init

# Create non-root user
RUN addgroup -g 1001 -S jss && \
    adduser -S -u 1001 -G jss jss

WORKDIR /app

# Copy only production dependencies and source
COPY --from=builder --chown=jss:jss /app/node_modules ./node_modules
COPY --from=builder --chown=jss:jss /app/package.json ./
COPY --from=builder --chown=jss:jss /app/bin ./bin
COPY --from=builder --chown=jss:jss /app/src ./src

# Create data directory
RUN mkdir -p /data && chown jss:jss /data

# Switch to non-root user
USER jss

# Environment defaults
ENV NODE_ENV=production
ENV JSS_PORT=3000
ENV JSS_HOST=0.0.0.0
ENV JSS_ROOT=/data

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/.well-known/solid || exit 1

# Use dumb-init as PID 1
ENTRYPOINT ["dumb-init", "--"]

# Start server
CMD ["node", "bin/jss.js", "start"]

Key Dockerfile Features

Feature Benefit
Multi-stage build ~70% smaller image (node_modules dev deps excluded)
Alpine base Minimal attack surface, ~5x smaller than Debian
Non-root user Security best practice, prevents privilege escalation
dumb-init Proper signal handling, zombie process reaping
Layer caching package.json first = faster rebuilds
Health check Container orchestration support

2. Docker Compose Examples

Basic (docker-compose.yml)

services:
  jss:
    image: ghcr.io/javascriptsolidserver/jss:latest
    ports:
      - "3000:3000"
    volumes:
      - jss-data:/data
    environment:
      - JSS_MULTIUSER=true
      - JSS_IDP=true
    restart: unless-stopped

volumes:
  jss-data:

Production with SSL (docker-compose.prod.yml)

services:
  jss:
    image: ghcr.io/javascriptsolidserver/jss:latest
    ports:
      - "443:3000"
    volumes:
      - ./data:/data
      - ./ssl:/ssl:ro
    environment:
      - JSS_PORT=3000
      - JSS_SSL_KEY=/ssl/key.pem
      - JSS_SSL_CERT=/ssl/cert.pem
      - JSS_MULTIUSER=true
      - JSS_IDP=true
      - JSS_CONNEG=true
      - JSS_NOTIFICATIONS=true
      - JSS_DEFAULT_QUOTA=100MB
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/.well-known/solid"]
      interval: 30s
      timeout: 10s
      retries: 3

Full Stack with Reverse Proxy (docker-compose.full.yml)

services:
  jss:
    image: ghcr.io/javascriptsolidserver/jss:latest
    volumes:
      - jss-data:/data
    environment:
      - JSS_MULTIUSER=true
      - JSS_IDP=true
      - JSS_CONNEG=true
      - JSS_NOTIFICATIONS=true
      - JSS_ACTIVITYPUB=true
      - JSS_NOSTR=true
    restart: unless-stopped
    networks:
      - internal

  caddy:
    image: caddy:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - jss
    restart: unless-stopped
    networks:
      - internal

volumes:
  jss-data:
  caddy-data:
  caddy-config:

networks:
  internal:

Single-User Personal Server

services:
  jss:
    image: ghcr.io/javascriptsolidserver/jss:latest
    ports:
      - "3000:3000"
    volumes:
      - ./data:/data
    environment:
      - JSS_SINGLE_USER=true
      - JSS_SINGLE_USER_NAME=me
      - JSS_IDP=true
      - JSS_CONNEG=true
      - JSS_MASHLIB=true
    restart: unless-stopped

3. .dockerignore

# Dependencies
node_modules

# Git
.git
.gitignore

# IDE
.vscode
.idea
*.swp
*.swo

# Testing
test
tests
*.test.js
coverage
.nyc_output

# Documentation
*.md
!README.md
docs

# CI/CD
.github
.gitlab-ci.yml
.travis.yml

# Development
.env
.env.*
docker-compose*.yml
Dockerfile*

# Data (should be mounted as volume)
data
*.db

# Misc
.DS_Store
Thumbs.db
*.log

4. GitHub Actions Workflow

.github/workflows/docker.yml

name: Docker Build & Push

on:
  push:
    branches: [main, gh-pages]
    tags: ['v*']
  pull_request:
    branches: [main, gh-pages]

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

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

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

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

      - name: Log in to Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha,prefix=
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/gh-pages' }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

      - name: Generate SBOM
        if: github.event_name != 'pull_request'
        uses: anchore/sbom-action@v0
        with:
          image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}

Workflow Features

Feature Benefit
Multi-platform linux/amd64 + linux/arm64 (Raspberry Pi, M1/M2 Macs)
Semantic versioning Tags like v1.0.0 create 1.0.0, 1.0, 1 tags
Cache GitHub Actions cache for faster builds
SBOM Software Bill of Materials for security scanning
PR builds Build but don't push on PRs (test Dockerfile)

5. Health Check Endpoint

Add /.well-known/solid or /health endpoint:

// src/server.js - add health check route

fastify.get('/health', async (request, reply) => {
  return {
    status: 'healthy',
    version: process.env.npm_package_version || 'unknown',
    uptime: process.uptime(),
    timestamp: new Date().toISOString()
  };
});

// Or use existing /.well-known/solid which returns server metadata

6. Documentation Updates

README.md Docker Section

## Docker

### Quick Start

```bash
docker run -d \
  --name jss \
  -p 3000:3000 \
  -v jss-data:/data \
  ghcr.io/javascriptsolidserver/jss:latest

With Configuration

docker run -d \
  --name jss \
  -p 3000:3000 \
  -v ./data:/data \
  -e JSS_MULTIUSER=true \
  -e JSS_IDP=true \
  -e JSS_CONNEG=true \
  ghcr.io/javascriptsolidserver/jss:latest

Docker Compose

curl -O https://raw.githubusercontent.com/JavaScriptSolidServer/JavaScriptSolidServer/main/docker-compose.yml
docker compose up -d

Available Tags

Tag Description
latest Latest stable release
x.y.z Specific version
x.y Latest patch of minor version
x Latest minor of major version
sha-abc123 Specific commit

Environment Variables

All JSS_* environment variables are supported. See Configuration for full list.


---

## Industry Comparison

| Software | Docker Support |
|----------|----------------|
| **Community Solid Server** | ✅ Official image `solidproject/community-server` |
| **Nextcloud** | ✅ Official image, multiple variants (fpm, apache) |
| **Mastodon** | ✅ Official image, comprehensive compose files |
| **Gitea/Forgejo** | ✅ Official images, rootless variants |
| **Vaultwarden** | ✅ Official image `vaultwarden/server` |
| **PocketBase** | ✅ Official image, single binary friendly |
| **Matrix Synapse** | ✅ Official image `matrixdotorg/synapse` |
| **JSS (current)** | ❌ No Docker support |

All major self-hosted software provides official Docker images. This is table stakes.

---

## Image Size Estimates

| Stage | Estimated Size |
|-------|----------------|
| Base `node:22` | ~1.1 GB |
| Base `node:22-alpine` | ~180 MB |
| With dependencies | ~250 MB |
| Multi-stage final | ~80-100 MB |

Comparison:
- Community Solid Server: ~300 MB
- PocketBase: ~50 MB (Go binary)
- Nextcloud: ~800 MB (PHP + Apache)

---

## Implementation Checklist

- [ ] Create `Dockerfile` with multi-stage build
- [ ] Create `.dockerignore`
- [ ] Create `docker-compose.yml` (basic)
- [ ] Create `docker-compose.prod.yml` (with SSL)
- [ ] Create `docker-compose.full.yml` (with Caddy)
- [ ] Add health check endpoint (`/health`)
- [ ] Create GitHub Actions workflow
- [ ] Test on `linux/amd64`
- [ ] Test on `linux/arm64`
- [ ] Update README with Docker section
- [ ] Add example Caddyfile
- [ ] First release to ghcr.io

---

## Difficulty Breakdown

| Component | Difficulty |
|-----------|------------|
| Dockerfile | 20/100 |
| .dockerignore | 5/100 |
| docker-compose files | 15/100 |
| GitHub Actions workflow | 25/100 |
| Health endpoint | 10/100 |
| Documentation | 10/100 |
| Multi-arch testing | 20/100 |
| **Total** | 30/100 |

---

## Security Considerations

1. **Non-root user** - Container runs as UID 1001, not root
2. **Read-only root filesystem** - Can add `--read-only` flag
3. **No shell** - Consider `FROM scratch` or distroless for minimal attack surface
4. **SBOM generation** - Track dependencies for vulnerability scanning
5. **Signed images** - Consider sigstore/cosign for image signing
6. **Secrets handling** - Document proper secrets management (Docker secrets, env files)

---

## Future Enhancements

1. **Rootless variant** - For environments requiring rootless containers
2. **Distroless variant** - Minimal image without shell
3. **Kubernetes Helm chart** - For K8s deployments
4. **Docker Hub mirror** - Alternative registry
5. **Watchtower compatibility** - Auto-update support

---

## Related Issues

- #97 - Presets (Docker compose can use preset env var)
- #98 - CLI wizard (`jss generate docker` command)
- #95 - Web onboarding (works same in Docker)
- #96 - Admin panel (accessible via Docker)

---

## References

- [Node.js Docker Best Practices](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md)
- [Docker Multi-Stage Builds](https://docs.docker.com/build/building/multi-stage/)
- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)
- [Docker Build Push Action](https://github.com/docker/build-push-action)
- [Community Solid Server Docker](https://hub.docker.com/r/solidproject/community-server)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions