How NeuronIP is packaged for production
- Overview
- Docker Containerization
- Image Build Strategy
- CI/CD Pipeline
- Release Process
- Image Registry
- Build Optimizations
NeuronIP is packaged as two Docker containers for production deployment:
-
Backend API Container (
neuronip/api)- Go application compiled to static binary
- Minimal Alpine Linux runtime
- Image size: ~10-20MB
-
Frontend Container (
neuronip/frontend)- Next.js production build
- Node.js Alpine runtime
- Image size: ~150-200MB
Important: External services (PostgreSQL, NeuronDB, NeuronMCP, NeuronAgent) are not packaged with NeuronIP and must be deployed separately.
Location: api/Dockerfile
Multi-stage build pattern:
Complete Dockerfile:
FROM golang:1.24-alpine AS builder
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/neuronip-api ./cmd/serverLine-by-line explanation:
-
FROM golang:1.24-alpine AS builder- Base image: Official Go 1.24 Alpine Linux
- Alpine: Minimal Linux distribution (~5MB base)
AS builder: Names this stage for multi-stage build- Includes Go compiler, build tools, and standard library
-
WORKDIR /app- Sets working directory inside container
- All subsequent commands run from
/app - Creates directory if it doesn't exist
-
COPY go.mod go.sum ./- Copies dependency manifest files first
- Critical optimization: Separates dependency layer from source code
- Docker caches this layer separately
- Only rebuilds if dependencies change
-
RUN go mod download- Downloads all Go module dependencies
- Creates cache layer for dependencies
- Subsequent builds skip this if
go.mod/go.sumunchanged - Downloads to
$GOPATH/pkg/mod(cached in layer)
-
COPY . .- Copies all source code into container
- Excludes files in
.dockerignore - This layer invalidates on any source change
- Dependencies already cached, so rebuild is fast
-
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/neuronip-api ./cmd/serverCGO_ENABLED=0: Disables CGO (C bindings)- Creates fully static binary
- No external C library dependencies
- Binary runs on any Linux system
GOOS=linux: Targets Linux operating system-o /app/neuronip-api: Output binary name and path./cmd/server: Source package to build- Compiles Go code to machine code
Build output:
- Binary size: ~15-25MB (depends on dependencies)
- Architecture: Linux amd64 (or specified target)
- Type: Static ELF executable
Complete Dockerfile:
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/neuronip-api .
EXPOSE 8082
CMD ["./neuronip-api"]Line-by-line explanation:
-
FROM alpine:latest- Minimal Alpine Linux base image (~5MB)
- No build tools, no compilers, no package managers (except apk)
- Minimal attack surface
- Only essential system libraries
-
RUN apk --no-cache add ca-certificatesapk: Alpine package manager--no-cache: Doesn't store package index (saves space)ca-certificates: Root CA certificates bundle- Required for HTTPS/TLS connections to external services
- Enables SSL certificate validation
-
WORKDIR /root/- Sets working directory to root user's home
- Binary will execute from this directory
- Standard location for single-binary applications
-
COPY --from=builder /app/neuronip-api .--from=builder: Copies from named build stage- Copies only the compiled binary
- No source code, no build tools, no dependencies
- Final image contains only: Alpine base + CA certs + binary
-
EXPOSE 8082- Documents that container listens on port 8082
- Doesn't actually open the port (that's done at runtime)
- Documentation for Docker and orchestrators
- Used by
docker run -Pto map ports
-
CMD ["./neuronip-api"]- Default command to run when container starts
- Executes the binary directly
[]: Exec form (recommended, no shell wrapper)- Process becomes PID 1 in container
Final image composition:
- Base: Alpine Linux (~5MB)
- CA certificates: ~200KB
- Binary: ~15-25MB
- Total: ~20-30MB (vs ~300MB+ with full golang image)
Security benefits:
- No shell access (unless explicitly added)
- No package manager in runtime (can't install malware)
- Minimal base reduces CVE surface area
- Static binary reduces dependency vulnerabilities
Optimizations:
- ✅ Layer caching: Dependencies cached separately
- ✅ Static binary: No runtime dependencies
- ✅ Minimal base: Alpine reduces attack surface
- ✅ No build tools: Only binary in final image
Location: frontend/Dockerfile
Multi-stage build pattern:
Complete Dockerfile:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run buildLine-by-line explanation:
-
FROM node:20-alpine AS builder- Base image: Official Node.js 20 Alpine Linux
- Includes Node.js runtime, npm, and build tools
- Alpine variant: Smaller than standard Node image
AS builder: Names this stage for multi-stage build
-
WORKDIR /app- Sets working directory to
/app - All commands execute from this directory
- Creates directory structure
- Sets working directory to
-
COPY package*.json ./- Copies
package.jsonandpackage-lock.json - Critical optimization: Separates dependency layer
- Docker caches this layer independently
- Only rebuilds dependencies if package files change
- Copies
-
RUN npm cinpm ci: Clean install (vsnpm install)- Deterministic: Installs exact versions from
package-lock.json - Faster: Skips dependency resolution
- Reliable: Fails if
package-lock.jsonis out of sync - Removes
node_modulesfirst, then installs fresh - Production dependencies only (if
--productionflag used)
-
COPY . .- Copies all source code into container
- Includes TypeScript files, React components, configs
- Excludes files in
.dockerignore - This layer invalidates on source changes
-
RUN npm run build- Executes Next.js production build
- Process:
- TypeScript compilation (
tsc) - React component bundling
- Code minification and optimization
- Static page generation (SSG)
- Creates
.nextdirectory with optimized output
- TypeScript compilation (
- Output:
.next/: Optimized production build- Static assets: CSS, JS, images
- Server-side code: API routes, middleware
- Build manifest: For runtime optimization
Build output:
.next/: Production build directory (~50-200MB)- Optimized JavaScript bundles
- Minified CSS
- Static HTML pages (if SSG used)
- Source maps (optional, for debugging)
Complete Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "start"]Line-by-line explanation:
-
FROM node:20-alpine- Fresh Alpine Node.js image
- No build artifacts from builder stage
- Only Node.js runtime (no build tools needed)
-
WORKDIR /app- Sets working directory
- Matches builder stage structure
-
COPY --from=builder /app/.next ./.next- Copies Next.js production build
- Contains optimized server and client code
- Required for
npm startto work
-
COPY --from=builder /app/public ./public- Copies static public assets
- Images, fonts, favicons, robots.txt
- Served directly by Next.js
- Not processed by build system
-
COPY --from=builder /app/package*.json ./- Copies package files
- Required for
npm startcommand - Contains scripts and metadata
-
COPY --from=builder /app/node_modules ./node_modules- Copies production dependencies
- Only runtime dependencies (not devDependencies)
- Required for Next.js to run
- Note: Could be optimized with Next.js standalone mode
-
EXPOSE 3000- Documents container listens on port 3000
- Next.js default port
- Can be changed via
PORTenvironment variable
-
CMD ["npm", "start"]- Runs
npm startscript - Executes:
next start(production server) - Serves optimized build from
.next/ - Handles SSR, API routes, and static files
- Runs
Final image composition:
- Base: Node.js Alpine (~40MB)
- Production dependencies: ~100-150MB
.nextbuild: ~50-200MB- Public assets: ~5-50MB
- Total: ~200-450MB (depends on dependencies and build size)
Optimization opportunities:
- Next.js Standalone Mode: Reduces
node_modulessize- Only includes necessary dependencies
- Can reduce image by 50-100MB
- Configure in
next.config.js:output: 'standalone'
Optimizations:
- ✅ Dependency layer caching
- ✅ Production-only artifacts
- ✅ No source code in final image
Potential improvement:
- Next.js standalone output mode (commented in
next.config.js)- Would reduce image size further
- Only includes necessary dependencies
Both containers use .dockerignore files to exclude unnecessary files:
Backend exclusions:
- Test files:
*_test.go,testdata/ - Build artifacts:
*.exe,*.dll,*.so - IDE files:
.vscode/,.idea/ - Documentation:
*.md,docs/ - CI/CD:
.github/,.circleci/ - Environment:
.env,.env.local
Frontend exclusions:
- Dependencies:
node_modules/(rebuilt in container) - Test files:
*.test.ts,*.test.tsx - Build outputs:
.next/,out/,dist/ - IDE files:
.vscode/,.idea/ - Documentation:
*.md,docs/
-
Smaller Images
- Go backend: ~15MB vs ~300MB+ (if using full golang image)
- Node.js frontend: Only production artifacts
-
Faster Builds
- Layer caching speeds up subsequent builds
- Only changed layers are rebuilt
- Dependencies cached separately from source
-
Security
- Minimal base images reduce attack surface
- No build tools in runtime images
- Only necessary files included
-
Reproducible Builds
- Deterministic dependency installation
- Consistent build environment
- Same source code produces identical images
Location: .github/workflows/build.yml
Complete Workflow:
name: Build and Push Docker Images
on:
push:
branches: [ main ]
tags:
- 'v*'
jobs:
build:
name: Build and Push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
neuronip/api
neuronip/frontend
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push API image
uses: docker/build-push-action@v5
with:
context: ./api
push: ${{ github.event_name != 'pull_request' }}
tags: neuronip/api:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
- name: Build and push Frontend image
uses: docker/build-push-action@v5
with:
context: ./frontend
push: ${{ github.event_name != 'pull_request' }}
tags: neuronip/frontend:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxDetailed Step-by-Step Explanation:
-
Trigger Configuration
on: push: branches: [ main ] tags: - 'v*'
- Triggers on:
- Push to
mainbranch (continuous integration) - Push of version tags matching
v*(e.g.,v1.2.3)
- Push to
- Does NOT trigger on:
- Pull requests (handled by separate CI workflow)
- Other branches (unless explicitly configured)
- Triggers on:
-
Checkout Code
- uses: actions/checkout@v4
- Checks out repository code
- Makes source available to workflow
- Version: v4 (latest stable)
-
Docker Buildx Setup
- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3
- Buildx: Advanced Docker build features
- Features enabled:
- Multi-platform builds (amd64, arm64)
- Build cache management
- Parallel builds
- Advanced caching strategies
-
Docker Hub Authentication
- name: Login to Docker Hub if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }}
- Conditional: Only runs if not a pull request
- Secrets: Stored in GitHub repository settings
- Purpose: Authenticate to push images
- Security: Secrets are masked in logs
-
Metadata Extraction
- name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: | neuronip/api neuronip/frontend tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix={{branch}}-
- Purpose: Generate image tags automatically
- Tag strategies:
type=ref,event=branch: Tags with branch nametype=ref,event=pr: Tags with PR numbertype=semver: Tags with semantic versiontype=sha: Tags with commit SHA
- Example tags for
v1.2.3onmain:neuronip/api:v1.2.3neuronip/api:v1.2neuronip/api:main-abc123neuronip/api:latest(if configured)
-
Build and Push API Image
- name: Build and push API image uses: docker/build-push-action@v5 with: context: ./api push: ${{ github.event_name != 'pull_request' }} tags: neuronip/api:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | VERSION=${{ github.sha }} BUILD_DATE=${{ github.event.head_commit.timestamp }}
- Context:
./apidirectory (build context) - Push: Only if not a pull request
- Tags: Single tag with commit SHA
- Cache:
cache-from: type=gha: Restores from GitHub Actions cachecache-to: type=gha,mode=max: Saves all layers to cache- Benefits: Faster subsequent builds
- Build args: Pass metadata to Dockerfile
VERSION: Commit SHABUILD_DATE: Timestamp of commit
- Context:
-
Build and Push Frontend Image
- Same process as API image
- Context:
./frontenddirectory - Tags:
neuronip/frontend:${{ github.sha }}
Image Tagging Strategy:
| Event | Tag Format | Example |
|---|---|---|
| Push to main | neuronip/api:main-abc123 |
Commit SHA |
| Version tag | neuronip/api:v1.2.3 |
Semantic version |
| Version tag | neuronip/api:v1.2 |
Major.minor |
| PR #123 | neuronip/api:pr-123 |
PR number |
Caching Strategy:
- GitHub Actions Cache (
type=gha):- Stores Docker layer cache
- Persists between workflow runs
- Mode
max: Caches all layers (not just final) - Benefits:
- Faster builds (reuses layers)
- Reduced bandwidth (doesn't re-download)
- Cost savings (faster CI runs)
Build Performance:
- First build: ~5-10 minutes (no cache)
- Cached build: ~2-5 minutes (with cache)
- Cache hit rate: ~70-90% (if dependencies unchanged)
Location: .github/workflows/release.yml
Complete Workflow:
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Get version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Generate changelog
id: changelog
run: |
VERSION="${{ steps.get_version.outputs.version }}"
echo "changelog=See CHANGELOG.md" >> $GITHUB_OUTPUT
- name: Build API
working-directory: ./api
run: |
go build -ldflags "-X main.version=${{ steps.get_version.outputs.version }} -X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ) -X main.gitCommit=${GITHUB_SHA}" -o neuronip-api cmd/server/main.go
- name: Build Frontend
working-directory: ./frontend
run: |
npm ci
npm run build
- name: Build Docker images
uses: docker/build-push-action@v5
with:
context: ./api
push: true
tags: |
neuronip/api:${{ steps.get_version.outputs.version }}
neuronip/api:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build Frontend Docker image
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: |
neuronip/frontend:${{ steps.get_version.outputs.version }}
neuronip/frontend:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ steps.get_version.outputs.version }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
- name: Upload release artifacts
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./api/neuronip-api
asset_name: neuronip-api-${{ steps.get_version.outputs.version }}
asset_content_type: application/octet-streamDetailed Step-by-Step Explanation:
-
Trigger Configuration
on: push: tags: - 'v*.*.*'
- Triggers on: Version tags matching semantic versioning
- Examples:
v1.0.0,v2.3.1,v10.5.2 - Does NOT trigger on:
v1,v1.0,beta-1.0.0
-
Permissions
permissions: contents: write
- Required to create GitHub Releases
- Required to upload release artifacts
- Minimal permissions (security best practice)
-
Checkout with Full History
- uses: actions/checkout@v4 with: fetch-depth: 0
fetch-depth: 0: Fetches full git history- Required for changelog generation
- Required for version comparison
-
Version Extraction
VERSION=${GITHUB_REF#refs/tags/v}- Input:
refs/tags/v1.2.3 - Process: Removes
refs/tags/vprefix - Output:
1.2.3 - Stored in:
steps.get_version.outputs.version
- Input:
-
Go Binary Build with Metadata
go build -ldflags "-X main.version=$VERSION -X main.buildDate=$DATE -X main.gitCommit=$SHA" \ -o neuronip-api cmd/server/main.go-ldflags: Linker flags for variable injection-X main.version: Injects version string-X main.buildDate: Injects build timestamp (UTC)-X main.gitCommit: Injects commit SHA- Runtime access: Binary can display version info
- Example usage in Go:
var version = "unknown" var buildDate = "unknown" var gitCommit = "unknown" func main() { fmt.Printf("Version: %s\nBuild Date: %s\nCommit: %s\n", version, buildDate, gitCommit) }
-
Docker Image Tagging
tags: | neuronip/api:${{ steps.get_version.outputs.version }} neuronip/api:latest
- Version tag:
neuronip/api:v1.2.3(specific version) - Latest tag:
neuronip/api:latest(most recent) - Both tags point to same image
- Latest tag updates on each release
- Version tag:
-
GitHub Release Creation
- name: Create GitHub Release uses: actions/create-release@v1 with: tag_name: ${{ github.ref }} release_name: Release ${{ steps.get_version.outputs.version }} body: ${{ steps.changelog.outputs.changelog }} draft: false prerelease: false
- Creates: GitHub Release for the tag
- Includes: Release notes from changelog
- Status: Published (not draft, not prerelease)
- URL:
https://github.com/owner/repo/releases/tag/v1.2.3
-
Binary Artifact Upload
- name: Upload release artifacts uses: actions/upload-release-asset@v1 with: asset_path: ./api/neuronip-api asset_name: neuronip-api-${{ steps.get_version.outputs.version }}
- Uploads: Compiled Go binary
- Purpose: Direct binary download (no Docker required)
- Name:
neuronip-api-v1.2.3 - Location: Attached to GitHub Release
Release Artifacts:
-
Docker Images:
neuronip/api:v1.2.3neuronip/api:latestneuronip/frontend:v1.2.3neuronip/frontend:latest
-
GitHub Release:
- Release notes
- Source code archive
- Binary artifact (
neuronip-api-v1.2.3)
Version Metadata in Go Binary:
The binary contains embedded metadata accessible at runtime:
# View version information
./neuronip-api --version
# Output:
# Version: 1.2.3
# Build Date: 2024-01-15T10:30:00Z
# Git Commit: abc123def456Image Tags Created:
| Tag | Purpose | Example |
|---|---|---|
v1.2.3 |
Specific version | neuronip/api:v1.2.3 |
latest |
Most recent release | neuronip/api:latest |
| Both | Point to same image | Immutable version, mutable latest |
Purpose: Validate builds without pushing
Actions:
- Runs tests for backend and frontend
- Builds Docker images (without pushing)
- Validates build process
- Uses GitHub Actions cache
Script: scripts/release.sh
Usage:
./scripts/release.sh patch # v1.2.3 → v1.2.4
./scripts/release.sh minor # v1.2.3 → v1.3.0
./scripts/release.sh major # v1.2.3 → v2.0.0Process:
- Accepts version type (major/minor/patch)
- Calculates new version from current git tag
- Updates CHANGELOG.md (placeholder)
- Creates git tag with version
- Pushes tag to trigger release workflow
Versioning: Semantic versioning (vMAJOR.MINOR.PATCH)
When a version tag is pushed:
- GitHub Actions release workflow triggers
- Docker images are built and pushed
- GitHub Release is created
- Binary artifacts are uploaded
Example:
# Create and push tag
git tag -a v1.2.3 -m "Release v1.2.3"
git push origin v1.2.3
# GitHub Actions automatically:
# - Builds images
# - Tags as neuronip/api:v1.2.3 and neuronip/api:latest
# - Creates GitHub ReleaseRegistry: docker.io
Images:
neuronip/apineuronip/frontend
Latest Release:
neuronip/api:latestneuronip/frontend:latest
Version Tags:
neuronip/api:v1.2.3neuronip/frontend:v1.2.3
Branch Tags:
neuronip/api:main-abc123(branch-commit SHA)
PR Tags:
neuronip/api:pr-123
Docker Compose:
services:
neuronip-api:
image: neuronip/api:v1.2.3
# or
image: neuronip/api:latestKubernetes:
containers:
- name: api
image: neuronip/api:v1.2.3
imagePullPolicy: IfNotPresentHelm:
image:
api:
repository: neuronip/api
tag: v1.2.3
pullPolicy: IfNotPresent-
Multi-stage Builds
- Separate build and runtime stages
- Minimal final images
-
Layer Caching
- Dependencies in separate layers
- GitHub Actions cache for faster builds
-
Static Binary (Backend)
- No runtime dependencies
- Single executable file
-
Production Builds (Frontend)
- Only production artifacts
- Optimized Next.js build
-
Next.js Standalone Mode
// next.config.js output: 'standalone'
- Reduces frontend image size
- Only includes necessary dependencies
-
BuildKit Cache Mounts
RUN --mount=type=cache,target=/go/pkg/mod \ go mod download- Faster dependency downloads
- Better cache utilization
-
Multi-architecture Builds
- Support ARM64 (Apple Silicon, ARM servers)
- Use Docker Buildx for multi-platform builds
-
Image Compression
- Use
docker-squashor similar tools - Reduce image layers
- Smaller image sizes
- Use
-
Distroless Images
- Consider Google's distroless images
- Even smaller attack surface
- No shell, no package manager
Basic build:
cd api
docker build -t neuronip-api:local .Build with specific platform:
docker build --platform linux/amd64 -t neuronip-api:local .
docker build --platform linux/arm64 -t neuronip-api:local-arm64 .Build with build arguments:
docker build \
--build-arg VERSION=1.2.3 \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-t neuronip-api:local \
.Build with cache from registry:
docker build \
--cache-from neuronip/api:latest \
-t neuronip-api:local \
.Build with progress output:
docker build --progress=plain -t neuronip-api:local .Basic build:
cd frontend
docker build -t neuronip-frontend:local .Build with Node.js version override:
docker build \
--build-arg NODE_VERSION=20 \
-t neuronip-frontend:local \
.Build with npm registry override:
docker build \
--build-arg NPM_REGISTRY=https://registry.npmjs.org/ \
-t neuronip-frontend:local \
.Build all services:
docker compose buildBuild specific service:
docker compose build neuronip-api
docker compose build neuronip-frontendBuild without cache:
docker compose build --no-cacheBuild with progress:
docker compose build --progress=plainBuild and start:
docker compose up -d --buildParallel build (Docker BuildKit):
DOCKER_BUILDKIT=1 docker compose buildBackend:
# Build with version tag
docker build -t neuronip/api:v1.2.3 ./api
# Tag as latest
docker tag neuronip/api:v1.2.3 neuronip/api:latest
# Push both tags
docker push neuronip/api:v1.2.3
docker push neuronip/api:latestFrontend:
# Build with version tag
docker build -t neuronip/frontend:v1.2.3 ./frontend
# Tag as latest
docker tag neuronip/frontend:v1.2.3 neuronip/frontend:latest
# Push both tags
docker push neuronip/frontend:v1.2.3
docker push neuronip/frontend:latestUsing Docker Buildx:
# Create buildx builder
docker buildx create --name multiarch --use
# Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t neuronip/api:v1.2.3 \
-t neuronip/api:latest \
--push \
./api
# Build frontend for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t neuronip/frontend:v1.2.3 \
-t neuronip/frontend:latest \
--push \
./frontendBenefits:
- Single image supports multiple architectures
- Automatic platform selection
- Works on Intel, AMD, and ARM systems
Using Docker BuildKit secrets:
# Build with secret file
docker build \
--secret id=npm_token,src=./.npmrc \
-t neuronip/frontend:local \
./frontendIn Dockerfile:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npm_token \
npm cidocker build -f Dockerfile.prod -t neuronip-api:prod ./api# Build only builder stage
docker build --target builder -t neuronip-api:builder ./api
# Build only runtime stage (default)
docker build --target runtime -t neuronip-api:runtime ./apidocker build \
--memory=4g \
--cpuset-cpus=0-3 \
-t neuronip-api:local \
./api# Build with host network (for private registries)
docker build --network=host -t neuronip-api:local ./apiEnable BuildKit:
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1Use BuildKit cache mounts:
# In Dockerfile
RUN --mount=type=cache,target=/go/pkg/mod \
go mod downloadParallel builds:
# Build multiple images in parallel
docker compose build --parallelBuild time comparison:
- Without cache: ~10-15 minutes
- With cache: ~2-5 minutes
- With BuildKit cache mounts: ~1-3 minutes
Basic inspection:
docker inspect neuronip/api:v1.2.3Inspect specific fields:
# Get image size
docker inspect -f '{{.Size}}' neuronip/api:v1.2.3
# Get creation date
docker inspect -f '{{.Created}}' neuronip/api:v1.2.3
# Get architecture
docker inspect -f '{{.Architecture}}' neuronip/api:v1.2.3
# Get environment variables
docker inspect -f '{{.Config.Env}}' neuronip/api:v1.2.3
# Get exposed ports
docker inspect -f '{{.Config.ExposedPorts}}' neuronip/api:v1.2.3List all images:
docker images neuronip/api
docker images neuronip/frontendDetailed size information:
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" \
| grep neuronipCompare image sizes:
# Compare different tags
docker images neuronip/api --format "{{.Tag}}: {{.Size}}"
# Show size breakdown
docker system df -vView layer history:
docker history neuronip/api:v1.2.3Detailed layer information:
docker history --human --format "table {{.ID}}\t{{.CreatedBy}}\t{{.Size}}" \
neuronip/api:v1.2.3Analyze layer sizes:
# Show layer sizes
docker history --no-trunc neuronip/api:v1.2.3 | head -20Export and analyze:
# Export image to tar
docker save neuronip/api:v1.2.3 -o neuronip-api.tar
# Analyze tar contents
tar -tf neuronip-api.tar | head -20List files in image:
docker run --rm neuronip/api:v1.2.3 ls -la /Check binary:
docker run --rm neuronip/api:v1.2.3 file /root/neuronip-api
docker run --rm neuronip/api:v1.2.3 ldd /root/neuronip-api 2>&1 || echo "Static binary"Check frontend build:
docker run --rm neuronip/frontend:v1.2.3 ls -la /app/.nextBasic run:
docker run -p 8082:8082 \
-e DB_HOST=postgres \
-e DB_USER=neuronip \
-e DB_PASSWORD=password \
-e DB_NAME=neuronip \
neuronip/api:v1.2.3Run with volume mounts:
docker run -p 8082:8082 \
-v $(pwd)/config:/config \
-e DB_HOST=postgres \
neuronip/api:v1.2.3Run with network:
docker run -p 8082:8082 \
--network neurondb-network \
-e DB_HOST=postgres \
neuronip/api:v1.2.3Run in background:
docker run -d --name neuronip-api \
-p 8082:8082 \
-e DB_HOST=postgres \
neuronip/api:v1.2.3Check logs:
docker logs neuronip-api
docker logs -f neuronip-api # Follow logsBasic run:
docker run -p 3000:3000 \
-e NEXT_PUBLIC_API_URL=http://localhost:8082/api/v1 \
neuronip/frontend:v1.2.3Run with custom port:
docker run -p 3001:3000 \
-e PORT=3000 \
-e NEXT_PUBLIC_API_URL=http://localhost:8082/api/v1 \
neuronip/frontend:v1.2.3Test API health endpoint:
# From host
curl http://localhost:8082/health
# From container
docker exec neuronip-api wget -qO- http://localhost:8082/healthTest frontend:
# Check if frontend is serving
curl http://localhost:3000
# Check API connectivity from frontend
docker exec neuronip-frontend wget -qO- http://neuronip-api:8082/healthScan for vulnerabilities:
# Using Trivy
trivy image neuronip/api:v1.2.3
# Using Docker Scout
docker scout cves neuronip/api:v1.2.3
# Using Snyk
snyk test --docker neuronip/api:v1.2.3Check for secrets:
# Using TruffleHog
trufflehog filesystem --directory=./api
# Using GitGuardian
ggshield secret scan docker neuronip/api:v1.2.3Measure container startup time:
time docker run --rm neuronip/api:v1.2.3 --versionCheck resource usage:
docker stats neuronip-api --no-streamProfile container:
docker run --rm \
--cap-add=SYS_PTRACE \
-p 8082:8082 \
neuronip/api:v1.2.3Problem: Go module dependencies not found
Solutions:
# Verify go.mod exists
ls -la api/go.mod
# Download dependencies locally
cd api && go mod download
# Verify go.sum is up to date
go mod tidy
# Rebuild
docker build -t neuronip-api:local ./apiProblem: npm install fails
Solutions:
# Clear npm cache
docker build --no-cache -t neuronip-frontend:local ./frontend
# Check package.json syntax
cd frontend && npm install --dry-run
# Verify package-lock.json
npm ci --dry-runProblem: Builds are slow
Solutions:
# Enable BuildKit
export DOCKER_BUILDKIT=1
# Use cache from registry
docker build --cache-from neuronip/api:latest -t neuronip-api:local ./api
# Build in parallel
docker compose build --parallel
# Use BuildKit cache mounts (in Dockerfile)
RUN --mount=type=cache,target=/go/pkg/mod go mod downloadProblem: Final image is larger than expected
Solutions:
# Analyze image layers
docker history neuronip/api:v1.2.3
# Check for unnecessary files
docker run --rm neuronip/api:v1.2.3 du -sh /*
# Use multi-stage build (already implemented)
# Remove unnecessary COPY commands
# Use .dockerignore effectivelyProblem: Container starts then exits
Solutions:
# Check logs
docker logs <container-id>
# Run interactively
docker run -it neuronip/api:v1.2.3 sh
# Check if binary exists
docker run --rm neuronip/api:v1.2.3 ls -la /root/
# Verify binary is executable
docker run --rm neuronip/api:v1.2.3 file /root/neuronip-apiProblem: Binary cannot execute
Solutions:
# In Dockerfile, ensure binary is executable
RUN chmod +x /root/neuronip-api
# Or build with proper permissions
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/neuronip-api ./cmd/server && \
chmod +x /app/neuronip-apiProblem: Cannot bind to port 8082 or 3000
Solutions:
# Find process using port
lsof -i :8082
netstat -tulpn | grep 8082
# Use different port
docker run -p 8083:8082 neuronip/api:v1.2.3
# Stop conflicting container
docker stop <container-id>Problem: Cannot connect to external database
Solutions:
# Test network connectivity
docker run --rm --network neurondb-network \
neuronip/api:v1.2.3 nc -zv postgres 5432
# Verify environment variables
docker run --rm neuronip/api:v1.2.3 env | grep DB_
# Check DNS resolution
docker run --rm --network neurondb-network \
neuronip/api:v1.2.3 nslookup postgresProblem: Workflow fails during build
Solutions:
- Check workflow logs in GitHub Actions
- Verify secrets are configured:
DOCKER_USERNAMEDOCKER_PASSWORD
- Check Docker Hub rate limits
- Verify Dockerfile syntax
- Check for syntax errors in workflow YAML
Problem: Build succeeds but images not in registry
Solutions:
# Verify push is enabled
push: ${{ github.event_name != 'pull_request' }}
# Check authentication
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Verify tags are correct
tags: neuronip/api:${{ github.sha }}Problem: Tag push doesn't trigger release
Solutions:
- Verify tag format:
v1.2.3(not1.2.3orv1.2) - Check workflow trigger:
tags: - 'v*.*.*' - Ensure tag is pushed:
git push origin v1.2.3 - Check workflow permissions in repository settings
Problem: Pulling images takes too long
Solutions:
# Use image registry closer to deployment
# Use CDN for image distribution
# Compress images (already using Alpine)
# Use multi-stage builds (already implemented)
# Pull specific platform
docker pull --platform linux/amd64 neuronip/api:v1.2.3Problem: Containers use too much memory
Solutions:
# Set memory limits
docker run -m 512m neuronip/api:v1.2.3
# Monitor memory usage
docker stats neuronip-api
# Optimize Go binary
# Use -ldflags="-s -w" to strip debug info
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-o /app/neuronip-api ./cmd/serverProblem: Containers take long to start
Solutions:
# Use smaller base images (already using Alpine)
# Minimize layers (combine RUN commands)
# Use health checks for faster readiness
# Pre-warm containersEnable in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
reactStrictMode: true,
}
module.exports = nextConfigUpdate Dockerfile:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
# Copy standalone build
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Benefits:
- Reduces
node_modulessize by 50-70% - Only includes necessary dependencies
- Faster container startup
- Smaller image size
For Go dependencies:
# syntax=docker/dockerfile:1
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/neuronip-api ./cmd/serverFor npm dependencies:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run buildBenefits:
- Faster builds (cached dependencies)
- Persistent cache across builds
- Reduced network usage
Combine RUN commands:
# Before (multiple layers)
RUN apk update
RUN apk add ca-certificates
RUN rm -rf /var/cache/apk/*
# After (single layer)
RUN apk --no-cache add ca-certificatesUse .dockerignore effectively:
# Exclude unnecessary files
node_modules/
.git/
*.md
.env*
.DS_Store
coverage/
.nyc_output/
Use non-root user:
FROM alpine:latest
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
RUN apk --no-cache add ca-certificates
WORKDIR /home/appuser
COPY --from=builder /app/neuronip-api .
RUN chown appuser:appuser /home/appuser/neuronip-api
USER appuser
CMD ["./neuronip-api"]Scan for vulnerabilities:
# Regular scanning
trivy image neuronip/api:v1.2.3
# In CI/CD
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: neuronip/api:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'Use distroless images (advanced):
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/neuronip-api /neuronip-api
EXPOSE 8082
CMD ["/neuronip-api"]Benefits:
- Even smaller attack surface
- No shell, no package manager
- Only application binary
- Docker Deployment - Docker setup and configuration
- Production Deployment - Production deployment guide
- Kubernetes Deployment - Kubernetes deployment guide
- CI/CD Reference - GitHub Actions workflows