version update

This commit is contained in:
NinjaPug
2025-07-18 20:56:49 -04:00
parent 41f3df8289
commit 1446c2a553
18 changed files with 712 additions and 810 deletions

View File

@@ -1,24 +0,0 @@
# Docker Registry Browser Environment Configuration
# Copy this file to .env and update with your values
# Registry host and port (without protocol)
REGISTRY_HOST=your-registry.example.com:5000
# Protocol to use for registry connection
REGISTRY_PROTOCOL=https
# Optional: Registry authentication
REGISTRY_USERNAME=your-username
REGISTRY_PASSWORD=your-password
# Development Examples:
# For local registry: REGISTRY_HOST=localhost:5000
# For Harbor: REGISTRY_HOST=harbor.example.com
# For Docker Hub: REGISTRY_HOST=registry-1.docker.io (not typically needed)
# For AWS ECR: REGISTRY_HOST=123456789012.dkr.ecr.us-west-2.amazonaws.com
# Development Usage:
# 1. Copy this file: cp .env.example .env
# 2. Edit .env with your registry details
# 3. Start development server: npm start
# 4. Or use convenience scripts: ./start-dev.sh

View File

@@ -1,4 +1,4 @@
# Multi-stage build for optimized production image
# Multi-stage build for Docker Registry Browser with Node.js proxy
FROM node:18-alpine AS build
# Set working directory
@@ -14,41 +14,43 @@ COPY . .
# Build the Angular application
RUN npm run build -- --configuration production
# Production stage
FROM nginx:1.25-alpine
# Production stage with Node.js proxy server
FROM node:18-alpine
# Install curl for health checks
RUN apk add --no-cache curl
# Set working directory
WORKDIR /app
# Copy package files and install production dependencies
COPY package*.json ./
RUN npm ci --only=production --silent
# Copy built application
COPY --from=build /app/dist/docker-registry-browser /usr/share/nginx/html
COPY --from=build /app/dist ./dist
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy entrypoint script
# Copy server and entrypoint
COPY server.js ./
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Create assets directory for env.js and set permissions
RUN mkdir -p /usr/share/nginx/html/assets && \
chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d && \
chmod -R 755 /usr/share/nginx/html
# Create non-root user
RUN addgroup -g 1001 -S appuser && \
adduser -S -D -H -u 1001 -h /app -s /sbin/nologin -G appuser appuser && \
chown -R appuser:appuser /app
# Add labels for better container management
LABEL maintainer="Your Name <your.email@example.com>"
LABEL description="Docker Registry Browser - A web interface for browsing Docker registries"
LABEL version="1.0.0"
LABEL description="Docker Registry Browser with Node.js proxy - A web interface for browsing Docker registries"
LABEL version="1.1.0"
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:80/ || exit 1
CMD curl -f http://localhost:80/health || exit 1
# Expose port
EXPOSE 80
# Use custom entrypoint to generate environment config (run as root for file creation)
# Use Node.js proxy entrypoint
ENTRYPOINT ["/docker-entrypoint.sh"]

316
README.md
View File

@@ -1,252 +1,186 @@
# Docker Registry Browser - Production Deployment
# Docker Registry Browser
A modern, responsive web interface for browsing Docker registries with support for both Docker v2 and OCI manifest formats.
A simple, reliable web interface for browsing Docker registries with a Node.js proxy to handle CORS issues. Perfect for Unraid users who want to easily view and manage their Docker registry.
## Features
- Browse repositories and tags
- View detailed image information (layers, environment, labels, etc.)
- Copy docker pull commands
- Push command generator with examples
- Dark/Light mode toggle
- Fully responsive design
- Support for OCI and Docker v2 manifests
- Multi-platform image support
## Prerequisites
- Docker installed and running
- Access to a Docker registry (local or remote)
- Network connectivity to the registry
- Browse repositories and tags in your Docker registry
- View detailed image information (size, layers, creation date, etc.)
- Copy docker pull commands with one click
- Clean, responsive web interface
- **CORS-free** - Uses Node.js proxy to avoid browser restrictions
- Works with any Docker Registry v2 compatible registry
## Quick Start
### Option 1: Docker Run
### Build and Run
```bash
docker run -d \
--name docker-registry-browser \
-p 8080:80 \
--add-host=host.docker.internal:host-gateway \
-e REGISTRY_HOST=localhost:5000 \
-e REGISTRY_PROTOCOL=http \
programmingpug/docker-registry-browser:latest
```
### Option 2: Docker Compose
```bash
git clone https://github.com/programmingPug/docker-registry-browser.git
cd docker-registry-browser
docker-compose up -d
```
### Option 3: Build from Source
```bash
git clone https://github.com/programmingPug/docker-registry-browser.git
cd docker-registry-browser
# Build the image
docker build -t docker-registry-browser .
docker run -d -p 8080:80 --add-host=host.docker.internal:host-gateway docker-registry-browser
# Run with your registry
docker run -d -p 8080:80 \
-e REGISTRY_HOST=your-registry:5000 \
-e REGISTRY_PROTOCOL=http \
docker-registry-browser
```
### Using Docker Compose
```bash
# Set your registry in .env or environment
export REGISTRY_HOST=192.168.1.100:5000
export REGISTRY_PROTOCOL=http
# Start with compose
docker-compose up -d
```
## Unraid Installation
### Method 1: Community Applications (Recommended)
### 1. Community Applications (Recommended)
- Go to **Apps** tab in Unraid
- Search for "Docker Registry Browser"
- Click **Install**
1. In Unraid, go to **Apps** tab
2. Search for "Docker Registry Browser"
3. Click **Install**
4. Configure the settings and click **Apply**
### 2. Manual Configuration
Use these settings in the Unraid template:
### Method 2: Manual Template
#### Basic Configuration
- **Registry Host**: `192.168.1.100:5000` (your Unraid IP and registry port)
- **Registry Protocol**: `http` (or `https` for secure registries)
1. In Unraid, go to **Docker** tab
2. Click **Add Container**
3. Set **Template** to the template URL or upload the XML template
4. Configure the required settings
5. Click **Apply**
#### For Authenticated Registries (Optional)
- **Username**: Your registry username
- **Password**: Your registry password
### Method 3: Docker Compose (Unraid 6.12+)
### 3. Common Unraid Examples
1. Install the "Compose Manager" plugin
2. Create a new compose stack with the provided `docker-compose.yml`
3. Deploy the stack
#### Local Registry on Unraid Host
```
Registry Host: 192.168.1.100:5000
Registry Protocol: http
```
## Supported Registries
#### Registry Container on Unraid
```
Registry Host: registry:5000
Registry Protocol: http
```
This browser works with any Docker Registry v2 compatible registry, including:
- **Local Docker Registry** - Self-hosted registry containers
- **Harbor** - Open source cloud native registry
- **AWS ECR** - Amazon Elastic Container Registry
- **Azure Container Registry** - Microsoft's container registry
- **Google Container Registry** - Google Cloud's container registry
- **GitLab Container Registry** - GitLab's integrated registry
- **Nexus Repository** - Sonatype's repository manager
- **Artifactory** - JFrog's universal repository manager
- **Docker Hub** - (limited support for browsing)
### Registry Requirements
- Docker Registry API v2 support
- CORS headers configured (for web access)
- Network accessibility from the browser container
#### Remote Secure Registry
```
Registry Host: my-registry.com:443
Registry Protocol: https
Username: myuser
Password: mypass
```
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `REGISTRY_HOST` | `localhost:5000` | Docker registry hostname and port |
| `REGISTRY_PROTOCOL` | `http` | Protocol (http/https) |
| `REGISTRY_USERNAME` | - | Registry username (optional) |
| `REGISTRY_PASSWORD` | - | Registry password (optional) |
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `REGISTRY_HOST` | Yes | `localhost:5000` | Registry hostname and port |
| `REGISTRY_PROTOCOL` | Yes | `http` | `http` or `https` |
| `REGISTRY_USERNAME` | No | - | Username for authenticated registries |
| `REGISTRY_PASSWORD` | No | - | Password for authenticated registries |
### Unraid Configuration
## How It Works
| Setting | Default | Description |
|---------|---------|-------------|
| **WebUI Port** | `8080` | Port for web interface |
| **Registry Host** | `localhost:5000` | Your registry address |
| **Registry Protocol** | `http` | http or https |
| **Registry Username** | - | Optional authentication |
| **Registry Password** | - | Optional authentication |
The application uses a **Node.js Express server** with `http-proxy-middleware` to proxy all registry API calls. This solves CORS issues that would otherwise prevent browsers from accessing Docker registries directly.
## Usage
### Request Flow
```
Browser → /api/v2/_catalog → Node.js Proxy → Registry
Browser ← Response with CORS headers ← Node.js Proxy ← Registry
```
1. Access the web interface at `http://your-server:8080`
2. Browse repositories on the left panel
3. Select a repository to view its tags
4. Click the info button to view detailed image information
5. Use the menu for push commands and settings
6. Toggle dark/light mode using the theme button in the toolbar
### Why This Works
- **No CORS Issues**: Proxy adds proper CORS headers automatically
- **Authentication**: Basic auth handled transparently by proxy
- **Any Registry**: Works with any Docker Registry v2 compatible API
- **Production Ready**: Uses proven `http-proxy-middleware` library
## Features Guide
## Supported Registries
### Dark Mode
- Toggle between light and dark themes using the moon/sun icon in the toolbar
- Theme preference is saved locally and persists between sessions
- All UI components are properly themed for optimal visibility in both modes
### Browsing Images
- Repository list shows all available repositories
- Click a repository to load its tags
- Search repositories using the search field
- View tag details by clicking the info button
### Push Commands
- Access via the menu (three dots) in the toolbar
- Get step-by-step instructions for pushing images
- Copy commands to clipboard
- Includes multi-architecture build instructions
### Image Details
- View comprehensive image information
- See layer details, environment variables, labels
- Check image size, architecture, and creation date
- Inspect exposed ports and volumes
Works with any Docker Registry v2 compatible registry:
- **Docker Registry** (open source)
- **Harbor**
- **AWS ECR**
- **Azure Container Registry**
- **Google Container Registry**
- **GitLab Container Registry**
- **Nexus Repository**
- **Artifactory**
## Troubleshooting
### Registry Connection Issues
### "Cannot connect to registry proxy"
- Ensure you're accessing `http://your-server:8080`, not a dev server
- Check container logs: `docker logs container-name`
- Verify `REGISTRY_HOST` and `REGISTRY_PROTOCOL` are correct
**Problem**: Cannot connect to registry
**Solutions**:
- Verify `REGISTRY_HOST` is correct (hostname:port format)
- Check if registry is accessible from container
- For local registries, ensure `--add-host=host.docker.internal:host-gateway` is set
### "Registry server error (502)"
- Registry is not accessible from the container
- Check if registry is running: `docker ps | grep registry`
- Test registry connectivity: `curl http://your-registry:5000/v2/`
- For Unraid: Use server IP, not `localhost`
### CORS Issues
**Problem**: API requests blocked by CORS
**Solutions**:
- The nginx configuration includes CORS headers
- Ensure your registry allows cross-origin requests
- For development, use the included proxy configuration
### Authentication Issues
**Problem**: 401 Unauthorized errors
**Solutions**:
- Set `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` environment variables
- Verify credentials are correct for your registry
- Check if registry requires authentication
### Manifest Issues
**Problem**: "OCI index found" or manifest parsing errors
**Solutions**:
- Current version supports OCI manifests
- Ensure registry supports Docker Registry API v2
- Check if image manifests are properly formatted
### Port Conflicts
**Problem**: Port 8080 already in use
**Solutions**:
- Change the host port: `-p 8081:80` instead of `-p 8080:80`
- Stop conflicting services or use different ports
- Check what's using the port: `netstat -tlnp | grep 8080`
### "No repositories found"
- Registry is accessible but empty (no images pushed yet)
- Authentication may be required but not configured
- Check registry has repositories: `curl http://your-registry:5000/v2/_catalog`
## Development
### Building
### Build from Source
```bash
git clone https://github.com/your-username/docker-registry-browser.git
cd docker-registry-browser
docker build -t docker-registry-browser .
```
### Local Development
```bash
# Install dependencies
npm install
# Development server
# Start Angular dev server (for frontend development)
npm start
# Build for production
npm run build
# Build Docker image
docker build -t docker-registry-browser .
# Start Node.js proxy server (for backend testing)
npm run server
```
### Contributing
## Health Monitoring
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## Health Check
The container includes a health check endpoint at `/health` that returns:
- `200 OK` with "healthy" response when running properly
- Checks every 30 seconds with 3 retries
The container provides health check endpoints:
- **`/health`** - Application health status
- **`/proxy-status`** - Proxy configuration status
## Security
- Runs as non-root user (nginx:nginx)
- Includes security headers
- Runs as non-root user
- No sensitive data stored in container
- Registry credentials passed via environment variables
- Registry credentials only used for API authentication
- All communication proxied through secure server
## License
MIT License - see LICENSE file for details.
## Changelog
## Support
### v1.1.0
- Updated Angular to v17
- Improved error handling
- Enhanced UI/UX
- Better multi-platform support
- Optimized build process
- **GitHub Issues**: Report bugs and request features
- **Unraid Forums**: Community support for Unraid-specific questions
- **Documentation**: Check this README for common solutions
### v1.0.0
- Initial release
- Support for Docker v2 and OCI manifests
- Responsive design
- Push command generation
- Dark/Light mode
- Multi-platform image support
---
## Technical Details
- **Frontend**: Angular 17 with Material Design
- **Backend**: Node.js Express with http-proxy-middleware
- **Container**: Alpine Linux for minimal size
- **Architecture**: Multi-stage Docker build for optimized production image

View File

@@ -1,96 +0,0 @@
@echo off
REM Docker Registry Browser - Build and Deploy Script for Windows
REM Usage: build.bat [tag] [registry]
setlocal enabledelayedexpansion
REM Configuration
set IMAGE_NAME=docker-registry-browser
set DEFAULT_TAG=latest
set DEFAULT_REGISTRY=
REM Parse arguments
if "%1"=="" (
set TAG=%DEFAULT_TAG%
) else (
set TAG=%1
)
if "%2"=="" (
set REGISTRY=%DEFAULT_REGISTRY%
) else (
set REGISTRY=%2
)
REM Build the full image name
if "%REGISTRY%"=="" (
set FULL_IMAGE_NAME=%IMAGE_NAME%:%TAG%
) else (
set FULL_IMAGE_NAME=%REGISTRY%/%IMAGE_NAME%:%TAG%
)
echo Building Docker Registry Browser...
echo Image: %FULL_IMAGE_NAME%
echo.
REM Build the Docker image
echo Building Docker image...
docker build -t "%FULL_IMAGE_NAME%" .
if %ERRORLEVEL% neq 0 (
echo Build failed!
pause
exit /b 1
)
echo.
echo Build completed successfully!
echo.
echo To run the container with environment variables:
echo docker run -d --name docker-registry-browser ^
echo -p 8080:80 ^
echo --add-host=host.docker.internal:host-gateway ^
echo -e REGISTRY_HOST=your-registry.com:5000 ^
echo -e REGISTRY_PROTOCOL=https ^
echo %FULL_IMAGE_NAME%
echo.
echo Or use docker-compose with .env file:
echo copy .env.example .env
echo # Edit .env with your values
echo docker-compose up -d
echo.
echo To push to registry (if configured):
if "%REGISTRY%"=="" (
echo Please specify a registry: build.bat %TAG% your-registry.com
) else (
echo docker push %FULL_IMAGE_NAME%
)
echo.
REM Optional: Run the container immediately
set /p REPLY="Do you want to run the container now? (y/N): "
if /i "%REPLY%"=="y" (
echo Starting container...
REM Check if .env file exists
if exist ".env" (
echo Using .env file for configuration...
docker run -d --name docker-registry-browser -p 8080:80 --add-host=host.docker.internal:host-gateway --env-file .env "%FULL_IMAGE_NAME%"
) else (
echo No .env file found, using default values...
echo Copy .env.example to .env and edit it for your registry configuration.
docker run -d --name docker-registry-browser -p 8080:80 --add-host=host.docker.internal:host-gateway -e REGISTRY_HOST=localhost:5000 -e REGISTRY_PROTOCOL=http "%FULL_IMAGE_NAME%"
)
if %ERRORLEVEL% equ 0 (
echo.
echo Container started successfully!
echo Access the application at: http://localhost:8080
echo View container logs: docker logs docker-registry-browser
echo Stop container: docker stop docker-registry-browser
) else (
echo Failed to start container!
)
)
pause

View File

@@ -1,88 +0,0 @@
#!/bin/bash
# Docker Registry Browser - Build and Deploy Script
# Usage: ./build.sh [tag] [registry]
set -e
# Configuration
IMAGE_NAME="docker-registry-browser"
DEFAULT_TAG="latest"
DEFAULT_REGISTRY=""
# Parse arguments
TAG=${1:-$DEFAULT_TAG}
REGISTRY=${2:-$DEFAULT_REGISTRY}
# Build the full image name
if [ -n "$REGISTRY" ]; then
FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG"
else
FULL_IMAGE_NAME="$IMAGE_NAME:$TAG"
fi
echo "Building Docker Registry Browser..."
echo "Image: $FULL_IMAGE_NAME"
echo ""
# Build the Docker image
echo "Building Docker image..."
docker build -t "$FULL_IMAGE_NAME" .
echo ""
echo "Build completed successfully!"
echo ""
echo "To run the container with environment variables:"
echo "docker run -d --name docker-registry-browser \\"
echo " -p 8080:80 \\"
echo " --add-host=host.docker.internal:host-gateway \\"
echo " -e REGISTRY_HOST=your-registry.com:5000 \\"
echo " -e REGISTRY_PROTOCOL=https \\"
echo " $FULL_IMAGE_NAME"
echo ""
echo "Or use docker-compose with .env file:"
echo "cp .env.example .env"
echo "# Edit .env with your values"
echo "docker-compose up -d"
echo ""
echo "To push to registry (if configured):"
if [ -n "$REGISTRY" ]; then
echo "docker push $FULL_IMAGE_NAME"
else
echo "Please specify a registry: ./build.sh $TAG your-registry.com"
fi
echo ""
# Optional: Run the container immediately
read -p "Do you want to run the container now? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Starting container..."
# Check if .env file exists
if [ -f ".env" ]; then
echo "Using .env file for configuration..."
docker run -d \
--name docker-registry-browser \
-p 8080:80 \
--add-host=host.docker.internal:host-gateway \
--env-file .env \
"$FULL_IMAGE_NAME"
else
echo "No .env file found, using default values..."
echo "Copy .env.example to .env and edit it for your registry configuration."
docker run -d \
--name docker-registry-browser \
-p 8080:80 \
--add-host=host.docker.internal:host-gateway \
-e REGISTRY_HOST=localhost:5000 \
-e REGISTRY_PROTOCOL=http \
"$FULL_IMAGE_NAME"
fi
echo ""
echo "Container started successfully!"
echo "Access the application at: http://localhost:8080"
echo "View container logs: docker logs docker-registry-browser"
echo "Stop container: docker stop docker-registry-browser"
fi

View File

@@ -14,6 +14,8 @@ services:
# Optional: Basic auth if your registry requires it
- REGISTRY_USERNAME=${REGISTRY_USERNAME:-}
- REGISTRY_PASSWORD=${REGISTRY_PASSWORD:-}
# Node.js configuration
- NODE_ENV=production
extra_hosts:
# For accessing host services (like local registry)
- "host.docker.internal:host-gateway"

View File

@@ -1,20 +1,87 @@
#!/bin/sh
# Generate environment configuration for Docker Registry Browser
# This script is run at container startup to inject environment variables
# Docker Registry Browser with Node.js Express Proxy
# Reliable CORS-handling solution
# Create env.js with environment variables
cat <<EOF > /usr/share/nginx/html/assets/env.js
# Set default values
REGISTRY_HOST=${REGISTRY_HOST:-localhost:5000}
REGISTRY_PROTOCOL=${REGISTRY_PROTOCOL:-http}
REGISTRY_USERNAME=${REGISTRY_USERNAME:-}
REGISTRY_PASSWORD=${REGISTRY_PASSWORD:-}
echo "=== Docker Registry Browser with Node.js Proxy ==="
echo "Registry Host: ${REGISTRY_HOST}"
echo "Registry Protocol: ${REGISTRY_PROTOCOL}"
echo "Registry URL: ${REGISTRY_PROTOCOL}://${REGISTRY_HOST}"
# Validate configuration
if [ -z "$REGISTRY_HOST" ]; then
echo "ERROR: REGISTRY_HOST is required"
exit 1
fi
# Test registry connectivity if possible
echo "Testing registry connectivity..."
REGISTRY_URL="${REGISTRY_PROTOCOL}://${REGISTRY_HOST}"
if command -v curl >/dev/null 2>&1; then
echo "Testing connection to: ${REGISTRY_URL}/v2/"
CURL_CMD="curl -f -s --connect-timeout 10"
if [ -n "$REGISTRY_USERNAME" ] && [ -n "$REGISTRY_PASSWORD" ]; then
CURL_CMD="$CURL_CMD -u ${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}"
fi
if $CURL_CMD "${REGISTRY_URL}/v2/" > /dev/null; then
echo "SUCCESS: Registry is accessible"
else
echo "WARNING: Registry connection test failed"
echo "This might be normal - the Node.js proxy will still attempt to connect"
fi
else
echo "curl not available, skipping connectivity test"
fi
# Create environment configuration for the frontend
echo "Creating frontend environment configuration..."
mkdir -p /app/dist/docker-registry-browser/assets
cat > /app/dist/docker-registry-browser/assets/env.js <<EOF
// Node.js Express Proxy Configuration
window.env = {
REGISTRY_HOST: '${REGISTRY_HOST:-localhost:5000}',
REGISTRY_PROTOCOL: '${REGISTRY_PROTOCOL:-http}',
REGISTRY_USERNAME: '${REGISTRY_USERNAME:-}',
REGISTRY_PASSWORD: '${REGISTRY_PASSWORD:-}'
REGISTRY_HOST: '${REGISTRY_HOST}',
REGISTRY_PROTOCOL: '${REGISTRY_PROTOCOL}',
REGISTRY_USERNAME: '${REGISTRY_USERNAME}',
REGISTRY_PASSWORD: '${REGISTRY_PASSWORD}',
USE_PROXY: true
};
console.log('Docker Registry Browser with Node.js Express Proxy');
console.log('Registry Target:', '${REGISTRY_PROTOCOL}://${REGISTRY_HOST}');
console.log('Proxy Endpoint: /api/*');
console.log('Proxy Method: http-proxy-middleware');
EOF
echo "Environment configuration generated:"
cat /usr/share/nginx/html/assets/env.js
# Set proper permissions
chown -R appuser:appuser /app
chmod 644 /app/dist/docker-registry-browser/assets/env.js
# Start nginx
exec nginx -g 'daemon off;'
echo "=== Starting Node.js Express Proxy Server ==="
echo "Proxy: /api/* -> ${REGISTRY_PROTOCOL}://${REGISTRY_HOST}/*"
echo "Web interface: http://localhost:80"
echo "Health check: http://localhost:80/health"
echo "Proxy status: http://localhost:80/proxy-status"
if [ -n "$REGISTRY_USERNAME" ]; then
echo "Using basic authentication"
fi
# Export environment variables for Node.js
export REGISTRY_HOST
export REGISTRY_PROTOCOL
export REGISTRY_USERNAME
export REGISTRY_PASSWORD
export NODE_ENV=production
# Switch to non-root user and start Node.js server
echo "Switching to non-root user and starting server..."
su appuser -s /bin/sh -c "cd /app && node server.js"

View File

@@ -1,43 +0,0 @@
const fs = require('fs');
const path = require('path');
// Get registry configuration from environment variables
const registryHost = process.env.REGISTRY_HOST || 'localhost:5000';
const registryProtocol = process.env.REGISTRY_PROTOCOL || 'http';
const registryUrl = `${registryProtocol}://${registryHost}`;
console.log(`Configuring proxy for registry: ${registryUrl}`);
// Create dynamic proxy configuration
const proxyConfig = {
"/api/*": {
"target": registryUrl,
"secure": registryProtocol === 'https',
"changeOrigin": true,
"logLevel": "debug",
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
},
"pathRewrite": {
"^/api": ""
}
}
};
// Add basic auth if credentials are provided
if (process.env.REGISTRY_USERNAME && process.env.REGISTRY_PASSWORD) {
const auth = Buffer.from(`${process.env.REGISTRY_USERNAME}:${process.env.REGISTRY_PASSWORD}`).toString('base64');
proxyConfig["/api/*"].headers.Authorization = `Basic ${auth}`;
console.log('Added basic authentication to proxy');
}
// Write the configuration file
const configPath = path.join(__dirname, 'proxy.conf.json');
fs.writeFileSync(configPath, JSON.stringify(proxyConfig, null, 2));
console.log('Proxy configuration updated successfully');
console.log('Configuration:', JSON.stringify(proxyConfig, null, 2));
module.exports = proxyConfig;

View File

@@ -1,132 +0,0 @@
# nginx.conf for production deployment
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100m;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Security Headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security: Hide nginx version
server_tokens off;
# Main application
location / {
try_files $uri $uri/ /index.html;
# Cache control for HTML files (no cache)
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
}
# Static assets with long-term caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
# Proxy for Docker Registry API (configurable via environment)
location /api/ {
# Remove /api prefix and forward to registry
rewrite ^/api/(.*)$ /$1 break;
# Default to localhost:5000, but can be overridden
proxy_pass http://host.docker.internal:5000;
# Proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS headers for registry API
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization";
add_header Access-Control-Max-Age 1728000;
add_header Content-Type "text/plain charset=UTF-8";
add_header Content-Length 0;
return 204;
}
# Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Health check endpoint
location /health {
access_log off;
add_header Content-Type text/plain;
return 200 "healthy\n";
}
# 404 error handling
error_page 404 /index.html;
# 50x error handling
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

View File

@@ -7,7 +7,8 @@
"start:dev": "npm run start",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"serve": "ng serve --host 0.0.0.0 --port 4200"
"serve": "ng serve --host 0.0.0.0 --port 4200",
"server": "node server.js"
},
"private": true,
"dependencies": {
@@ -21,6 +22,8 @@
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"

View File

@@ -1,16 +0,0 @@
{
"/api/*": {
"target": "http://localhost:5000",
"secure": false,
"changeOrigin": true,
"logLevel": "debug",
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
},
"pathRewrite": {
"^/api": ""
}
}
}

160
server.js Normal file
View File

@@ -0,0 +1,160 @@
// server.js - Node.js Express proxy server for Docker Registry Browser
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const path = require('path');
const app = express();
// Environment variables
const REGISTRY_HOST = process.env.REGISTRY_HOST || 'localhost:5000';
const REGISTRY_PROTOCOL = process.env.REGISTRY_PROTOCOL || 'http';
const REGISTRY_USERNAME = process.env.REGISTRY_USERNAME || '';
const REGISTRY_PASSWORD = process.env.REGISTRY_PASSWORD || '';
const registryUrl = `${REGISTRY_PROTOCOL}://${REGISTRY_HOST}`;
console.log('=== Docker Registry Browser Node.js Proxy ===');
console.log('Target Registry:', registryUrl);
console.log('Has Authentication:', !!(REGISTRY_USERNAME && REGISTRY_PASSWORD));
// Serve static files from the Angular build
app.use(express.static(path.join(__dirname, 'dist/docker-registry-browser')));
// Create proxy middleware with comprehensive options
const proxyOptions = {
target: registryUrl,
changeOrigin: true,
secure: false, // Allow self-signed certificates
pathRewrite: {
'^/api': '', // Remove /api prefix
},
timeout: 30000, // 30 second timeout
proxyTimeout: 30000,
onProxyReq: (proxyReq, req, res) => {
// Add auth header if configured
if (REGISTRY_USERNAME && REGISTRY_PASSWORD) {
const auth = Buffer.from(`${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}`).toString('base64');
proxyReq.setHeader('Authorization', `Basic ${auth}`);
}
// Log requests for debugging
console.log(`[PROXY] ${req.method} ${req.url} -> ${registryUrl}${req.url.replace('/api', '')}`);
// Ensure proper headers
proxyReq.setHeader('User-Agent', 'Docker-Registry-Browser/1.0');
},
onProxyRes: (proxyRes, req, res) => {
// Add comprehensive CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Pragma');
res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Docker-Content-Digest, Docker-Distribution-Api-Version, X-Registry-Auth, WWW-Authenticate');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
// Log successful responses
console.log(`[PROXY] Response: ${proxyRes.statusCode} ${proxyRes.statusMessage}`);
},
onError: (err, req, res) => {
console.error(`[PROXY ERROR] ${req.method} ${req.url}:`, err.message);
// Send detailed error response
res.status(502).json({
error: 'Registry proxy error',
message: err.message,
code: err.code,
target: registryUrl,
url: req.url,
timestamp: new Date().toISOString()
});
}
};
// Registry API proxy - this handles all /api/* requests
app.use('/api', createProxyMiddleware(proxyOptions));
// Handle CORS preflight requests explicitly
app.options('/api/*', (req, res) => {
console.log('[CORS] Handling OPTIONS preflight request');
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Pragma');
res.header('Access-Control-Max-Age', '86400');
res.sendStatus(204);
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
registry: registryUrl,
hasAuth: !!(REGISTRY_USERNAME && REGISTRY_PASSWORD),
timestamp: new Date().toISOString(),
version: '1.1.0'
});
});
// Proxy status endpoint for debugging
app.get('/proxy-status', (req, res) => {
res.json({
proxy_target: registryUrl,
status: 'configured',
has_auth: !!(REGISTRY_USERNAME && REGISTRY_PASSWORD),
proxy_middleware: 'http-proxy-middleware',
node_version: process.version,
timestamp: new Date().toISOString()
});
});
// Test registry connection endpoint
app.get('/test-registry', (req, res) => {
// Simple test endpoint - actual testing is done by the proxy
res.json({
message: 'Registry connection testing is handled by the proxy',
registry: registryUrl,
hasAuth: !!(REGISTRY_USERNAME && REGISTRY_PASSWORD),
timestamp: new Date().toISOString()
});
});
// Catch-all handler: send back Angular's index.html file for any non-API routes
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist/docker-registry-browser/index.html'));
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error('[SERVER ERROR]:', err);
res.status(500).json({
error: 'Internal server error',
message: err.message,
timestamp: new Date().toISOString()
});
});
// Start the server
const PORT = process.env.PORT || 80;
app.listen(PORT, '0.0.0.0', () => {
console.log(`=== Server Started ===`);
console.log(`Port: ${PORT}`);
console.log(`Registry proxy: /api/* -> ${registryUrl}/*`);
console.log(`Health check: http://localhost:${PORT}/health`);
console.log(`Proxy status: http://localhost:${PORT}/proxy-status`);
console.log('======================');
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
process.exit(0);
});
module.exports = app;

View File

@@ -11,7 +11,6 @@ import { PushCommandsDialogComponent } from './components/push-commands-dialog.c
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
registryUrl = '/api'; // Using proxy for CORS
repositories: Repository[] = [];
selectedRepo: Repository | null = null;
tags: Tag[] = [];
@@ -22,6 +21,7 @@ export class AppComponent implements OnInit {
copyMessage = '';
selectedTag: Tag | null = null;
showingDetails = false;
connectionStatus: { success: boolean; message: string } | null = null;
constructor(
private registryService: DockerRegistryService,
@@ -30,13 +30,117 @@ export class AppComponent implements OnInit {
) {}
ngOnInit() {
this.loadRepositories();
this.testConnectionAndLoad();
}
get registryInfo() {
return this.environmentService.getRegistryInfo();
}
get registryHost(): string {
return this.environmentService.displayHost;
}
async testConnectionAndLoad() {
this.loading = true;
this.error = '';
try {
// Test connection first
console.log('Testing registry connection...');
this.connectionStatus = await this.registryService.testConnection();
if (this.connectionStatus.success) {
console.log('Connection successful, loading repositories...');
await this.loadRepositories();
} else {
this.error = `Registry connection failed: ${this.connectionStatus.message}`;
console.error('Connection failed:', this.connectionStatus.message);
}
} catch (err: any) {
this.error = `Failed to connect to registry: ${err.message}`;
console.error('Connection error:', err);
}
this.loading = false;
}
async loadRepositories() {
this.loading = true;
this.error = '';
try {
console.log('Loading repositories...');
this.repositories = await this.registryService.getRepositories();
console.log(`Loaded ${this.repositories.length} repositories`);
if (this.repositories.length === 0) {
this.error = 'No repositories found in the registry. Make sure your registry contains some images.';
}
} catch (err: any) {
this.error = err.message;
this.repositories = [];
console.error('Failed to load repositories:', err);
}
this.loading = false;
}
async loadTags(repo: Repository) {
this.loading = true;
this.error = '';
this.selectedRepo = repo;
this.selectedTag = null;
this.showingDetails = false;
try {
console.log(`Loading tags for repository: ${repo.name}`);
this.tags = await this.registryService.getTags(repo.name);
console.log(`Loaded ${this.tags.length} tags`);
if (this.tags.length === 0) {
this.error = `No tags found for repository ${repo.name}`;
}
} catch (err: any) {
this.error = err.message;
this.tags = [];
console.error('Failed to load tags:', err);
}
this.loading = false;
}
async loadImageDetails(tag: Tag) {
if (!this.selectedRepo) return;
this.loadingDetails = true;
this.selectedTag = tag;
this.showingDetails = true;
this.error = '';
try {
if (!tag.details) {
console.log(`Loading details for: ${this.selectedRepo.name}:${tag.name}`);
tag.details = await this.registryService.getImageDetails(
this.selectedRepo.name,
tag.name
);
console.log('Image details loaded:', tag.details);
}
} catch (err: any) {
this.error = err.message;
console.error('Failed to load image details:', err);
}
this.loadingDetails = false;
}
closeDetails() {
this.selectedTag = null;
this.showingDetails = false;
this.error = '';
}
openPushCommandsDialog() {
this.dialog.open(PushCommandsDialogComponent, {
width: '800px',
@@ -49,69 +153,22 @@ export class AppComponent implements OnInit {
});
}
async loadRepositories() {
this.loading = true;
this.error = '';
try {
this.repositories = await this.registryService.getRepositories(this.registryUrl);
} catch (err: any) {
this.error = `Failed to fetch repositories: ${err.message}`;
this.repositories = [];
}
this.loading = false;
}
async loadTags(repo: Repository) {
this.loading = true;
this.error = '';
this.selectedRepo = repo;
this.selectedTag = null;
this.showingDetails = false;
try {
this.tags = await this.registryService.getTags(this.registryUrl, repo.name);
} catch (err: any) {
this.error = `Failed to fetch tags: ${err.message}`;
this.tags = [];
}
this.loading = false;
}
async loadImageDetails(tag: Tag) {
if (!this.selectedRepo) return;
this.loadingDetails = true;
this.selectedTag = tag;
this.showingDetails = true;
try {
if (!tag.details) {
tag.details = await this.registryService.getImageDetails(
this.registryUrl,
this.selectedRepo.name,
tag.name
);
}
} catch (err: any) {
this.error = `Failed to fetch image details: ${err.message}`;
}
this.loadingDetails = false;
}
closeDetails() {
this.selectedTag = null;
this.showingDetails = false;
}
getDockerPullCommand(repoName: string, tagName: string): string {
return `docker pull ${this.registryHost}/${repoName}:${tagName}`;
}
copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
this.copyMessage = 'Copied to clipboard!';
setTimeout(() => {
this.copyMessage = '';
}, 2000);
navigator.clipboard.writeText(text).then(() => {
this.copyMessage = 'Copied to clipboard!';
setTimeout(() => {
this.copyMessage = '';
}, 2000);
}).catch(() => {
this.copyMessage = 'Failed to copy';
setTimeout(() => {
this.copyMessage = '';
}, 2000);
});
}
formatBytes = (bytes: number): string => {
@@ -139,4 +196,14 @@ export class AppComponent implements OnInit {
repo.name.toLowerCase().includes(this.searchTerm.toLowerCase())
);
}
// Retry connection
async retryConnection() {
await this.testConnectionAndLoad();
}
// Manual refresh
async refresh() {
await this.loadRepositories();
}
}

View File

@@ -1,38 +1,120 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { firstValueFrom, timeout } from 'rxjs';
import { Repository, Tag, RegistryResponse, TagsResponse, ImageDetails, ManifestResponse, ConfigResponse } from '../models/registry.model';
import { EnvironmentService } from './environment.service';
@Injectable({
providedIn: 'root'
})
export class DockerRegistryService {
constructor(private http: HttpClient) {}
private readonly requestTimeout = 30000; // 30 seconds
async getRepositories(registryUrl: string): Promise<Repository[]> {
constructor(
private http: HttpClient,
private environmentService: EnvironmentService
) {}
private get baseUrl(): string {
// ALWAYS use proxy - this solves CORS issues
console.log('Using proxy endpoint /api to avoid CORS issues');
console.log('Proxy target:', this.environmentService.fullRegistryUrl);
return '/api';
}
private getRequestOptions(): { headers: HttpHeaders } {
// When using proxy, don't add auth headers - the server handles it
const headers = new HttpHeaders({
'Content-Type': 'application/json',
});
return { headers };
}
private handleError(error: any, operation: string): never {
console.error(`${operation} failed:`, error);
let errorMessage = `${operation} failed`;
if (error instanceof HttpErrorResponse) {
switch (error.status) {
case 0:
errorMessage = `Cannot connect to registry proxy. Please check if the Docker Registry Browser server is running properly. Target registry: ${this.environmentService.fullRegistryUrl}`;
break;
case 401:
errorMessage = 'Registry authentication failed. Please check your username and password in the container environment.';
break;
case 404:
errorMessage = `Registry endpoint not found. Please verify your registry host and protocol. Target: ${this.environmentService.fullRegistryUrl}`;
break;
case 502:
errorMessage = `Proxy cannot reach the registry at ${this.environmentService.fullRegistryUrl}. Please check your REGISTRY_HOST and REGISTRY_PROTOCOL settings.`;
break;
case 500:
case 503:
case 504:
errorMessage = `Registry server error. Please check if your registry at ${this.environmentService.fullRegistryUrl} is running correctly.`;
break;
default:
errorMessage = `Registry error (${error.status}): ${error.message || 'Unknown error'}`;
}
// Add helpful debugging info
if (error.status === 0) {
errorMessage += '\n\nTroubleshooting:\n1. Make sure you\'re accessing the app through the Docker container, not the dev server\n2. Check that REGISTRY_HOST and REGISTRY_PROTOCOL are correct\n3. Verify your registry is accessible';
}
} else if (error.message) {
errorMessage = error.message;
}
throw new Error(errorMessage);
}
async getRepositories(): Promise<Repository[]> {
try {
const url = `${this.baseUrl}/v2/_catalog`;
console.log('Fetching repositories from:', url);
console.log('Registry target (via proxy):', this.environmentService.fullRegistryUrl);
const response = await firstValueFrom(
this.http.get<RegistryResponse>(`${registryUrl}/v2/_catalog`)
this.http.get<RegistryResponse>(url, this.getRequestOptions())
.pipe(timeout(this.requestTimeout))
);
return (response?.repositories || []).map(name => ({ name }));
console.log('Registry response:', response);
const repositories = (response?.repositories || []).map(name => ({ name }));
console.log(`Found ${repositories.length} repositories`);
return repositories;
} catch (error) {
throw new Error(`Failed to connect to registry: ${error}`);
this.handleError(error, 'Fetching repositories');
}
}
async getTags(registryUrl: string, repositoryName: string): Promise<Tag[]> {
async getTags(repositoryName: string): Promise<Tag[]> {
try {
const url = `${this.baseUrl}/v2/${repositoryName}/tags/list`;
console.log(`Fetching tags for repository: ${repositoryName} from: ${url}`);
const response = await firstValueFrom(
this.http.get<TagsResponse>(`${registryUrl}/v2/${repositoryName}/tags/list`)
this.http.get<TagsResponse>(url, this.getRequestOptions())
.pipe(timeout(this.requestTimeout))
);
return (response?.tags || []).map(name => ({ name }));
const tags = (response?.tags || []).map(name => ({ name }));
console.log(`Found ${tags.length} tags for ${repositoryName}`);
return tags;
} catch (error) {
throw new Error(`Failed to fetch tags for ${repositoryName}: ${error}`);
this.handleError(error, `Fetching tags for ${repositoryName}`);
}
}
async getImageDetails(registryUrl: string, repositoryName: string, tag: string): Promise<ImageDetails> {
async getImageDetails(repositoryName: string, tag: string): Promise<ImageDetails> {
try {
console.log(`Fetching image details for: ${repositoryName}:${tag}`);
// Enhanced Accept header to support both Docker v2 and OCI manifest formats
const manifestHeaders = new HttpHeaders({
'Accept': [
@@ -42,29 +124,27 @@ export class DockerRegistryService {
'application/vnd.oci.image.index.v1+json'
].join(', ')
});
const url = `${this.baseUrl}/v2/${repositoryName}/manifests/${tag}`;
console.log('Fetching manifest from:', url);
const manifest = await firstValueFrom(
this.http.get<any>(
`${registryUrl}/v2/${repositoryName}/manifests/${tag}`,
{ headers: manifestHeaders }
)
this.http.get<any>(url, { headers: manifestHeaders })
.pipe(timeout(this.requestTimeout))
);
console.log('Manifest response:', manifest);
// Handle different manifest types
if (this.isManifestList(manifest)) {
// Handle manifest lists (multi-platform images)
return await this.handleManifestList(registryUrl, repositoryName, tag, manifest);
return await this.handleManifestList(repositoryName, tag, manifest);
} else if (this.isImageManifest(manifest)) {
// Handle single platform manifests
return await this.handleImageManifest(registryUrl, repositoryName, manifest);
return await this.handleImageManifest(repositoryName, manifest);
} else {
throw new Error(`Unsupported manifest type: ${manifest.mediaType}`);
}
} catch (error) {
console.error('Error fetching image details:', error);
throw new Error(`Failed to fetch image details for ${repositoryName}:${tag}: ${error}`);
this.handleError(error, `Fetching image details for ${repositoryName}:${tag}`);
}
}
@@ -78,16 +158,15 @@ export class DockerRegistryService {
manifest.mediaType === 'application/vnd.oci.image.manifest.v1+json';
}
private async handleManifestList(registryUrl: string, repositoryName: string, tag: string, manifestList: any): Promise<ImageDetails> {
// For manifest lists, we'll use the first manifest (usually linux/amd64)
// You could enhance this to let users choose the platform
private async handleManifestList(repositoryName: string, tag: string, manifestList: any): Promise<ImageDetails> {
// For manifest lists, use the first manifest (usually linux/amd64)
const firstManifest = manifestList.manifests?.[0];
if (!firstManifest) {
throw new Error('No manifests found in manifest list');
}
console.log('Using manifest from list:', firstManifest);
console.log('Using first manifest from list:', firstManifest);
// Fetch the actual manifest using its digest
const manifestHeaders = new HttpHeaders({
@@ -97,17 +176,18 @@ export class DockerRegistryService {
].join(', ')
});
const url = `${this.baseUrl}/v2/${repositoryName}/manifests/${firstManifest.digest}`;
console.log('Fetching actual manifest from:', url);
const actualManifest = await firstValueFrom(
this.http.get<any>(
`${registryUrl}/v2/${repositoryName}/manifests/${firstManifest.digest}`,
{ headers: manifestHeaders }
)
this.http.get<any>(url, { headers: manifestHeaders })
.pipe(timeout(this.requestTimeout))
);
return await this.handleImageManifest(registryUrl, repositoryName, actualManifest, firstManifest);
return await this.handleImageManifest(repositoryName, actualManifest, firstManifest);
}
private async handleImageManifest(registryUrl: string, repositoryName: string, manifest: any, platformInfo?: any): Promise<ImageDetails> {
private async handleImageManifest(repositoryName: string, manifest: any, platformInfo?: any): Promise<ImageDetails> {
try {
// Get the config blob
const configDigest = manifest.config?.digest;
@@ -115,10 +195,12 @@ export class DockerRegistryService {
throw new Error('No config digest found in manifest');
}
const url = `${this.baseUrl}/v2/${repositoryName}/blobs/${configDigest}`;
console.log('Fetching config from:', url);
const config = await firstValueFrom(
this.http.get<ConfigResponse>(
`${registryUrl}/v2/${repositoryName}/blobs/${configDigest}`
)
this.http.get<ConfigResponse>(url, this.getRequestOptions())
.pipe(timeout(this.requestTimeout))
);
console.log('Config response:', config);
@@ -165,35 +247,33 @@ export class DockerRegistryService {
}
}
// Helper method to get available platforms for a manifest list
async getAvailablePlatforms(registryUrl: string, repositoryName: string, tag: string): Promise<any[]> {
// Test registry connectivity via proxy
async testConnection(): Promise<{ success: boolean; message: string }> {
try {
const manifestHeaders = new HttpHeaders({
'Accept': [
'application/vnd.docker.distribution.manifest.list.v2+json',
'application/vnd.oci.image.index.v1+json'
].join(', ')
});
const url = `${this.baseUrl}/v2/`;
console.log('Testing connection to:', url);
console.log('Target registry (via proxy):', this.environmentService.fullRegistryUrl);
const manifest = await firstValueFrom(
this.http.get<any>(
`${registryUrl}/v2/${repositoryName}/manifests/${tag}`,
{ headers: manifestHeaders }
)
await firstValueFrom(
this.http.get(url, this.getRequestOptions())
.pipe(timeout(10000)) // 10 second timeout for connectivity test
);
if (this.isManifestList(manifest)) {
return manifest.manifests?.map((m: any) => ({
digest: m.digest,
platform: m.platform,
size: m.size
})) || [];
}
return [];
return { success: true, message: 'Registry connection successful via proxy' };
} catch (error) {
console.error('Error getting platforms:', error);
return [];
let errorMsg = 'Connection failed: Unable to reach registry via proxy';
if (error instanceof HttpErrorResponse) {
if (error.status === 0) {
errorMsg = 'Cannot connect to proxy server. Make sure you\'re accessing the app through the Docker container.';
} else if (error.status === 502) {
errorMsg = `Proxy cannot reach registry at ${this.environmentService.fullRegistryUrl}. Check REGISTRY_HOST and REGISTRY_PROTOCOL.`;
} else {
errorMsg = `Connection failed (${error.status}): ${error.message || 'Unknown error'}`;
}
}
console.error('Connection test failed:', error);
return { success: false, message: errorMsg };
}
}
}

View File

@@ -2,11 +2,13 @@ import { Injectable } from '@angular/core';
declare global {
interface Window {
env: {
REGISTRY_HOST: string;
REGISTRY_PROTOCOL: string;
REGISTRY_USERNAME: string;
REGISTRY_PASSWORD: string;
env?: {
REGISTRY_HOST?: string;
REGISTRY_PROTOCOL?: string;
REGISTRY_USERNAME?: string;
REGISTRY_PASSWORD?: string;
REGISTRY_DISPLAY_URL?: string;
USE_PROXY?: boolean;
};
}
}
@@ -20,10 +22,21 @@ export class EnvironmentService {
constructor() {
// Load configuration from window object (set by env.js)
this.config = window.env || {};
// Log configuration for debugging (without sensitive data)
console.log('Registry Configuration:', {
host: this.registryHost,
protocol: this.registryProtocol,
hasUsername: !!this.registryUsername,
hasPassword: !!this.registryPassword,
useProxy: this.config.USE_PROXY || false
});
}
get registryHost(): string {
return this.config.REGISTRY_HOST || 'localhost:5000';
const host = this.config.REGISTRY_HOST || 'localhost:5000';
// Ensure we don't have protocol in the host
return host.replace(/^https?:\/\//, '');
}
get registryProtocol(): string {
@@ -38,12 +51,33 @@ export class EnvironmentService {
return this.config.REGISTRY_PASSWORD || '';
}
get useProxy(): boolean {
return this.config.USE_PROXY || false;
}
// Get the full registry URL (used for proxy target and direct connection)
get fullRegistryUrl(): string {
return `${this.registryProtocol}://${this.registryHost}`;
}
// Get display host (without protocol) for commands
// Get display host for docker commands (without protocol)
get displayHost(): string {
return this.registryHost;
}
// Check if authentication is configured
get hasAuthentication(): boolean {
return !!(this.registryUsername && this.registryPassword);
}
// Get registry info for display
getRegistryInfo(): { host: string; protocol: string; secure: boolean; hasAuth: boolean; useProxy: boolean } {
return {
host: this.registryHost,
protocol: this.registryProtocol,
secure: this.registryProtocol === 'https',
hasAuth: this.hasAuthentication,
useProxy: this.useProxy
};
}
}

View File

@@ -1,29 +0,0 @@
@echo off
REM Development start script for Docker Registry Browser
REM Usage: start-dev.bat [registry_host] [protocol]
REM Set default values
set REGISTRY_HOST=%1
set REGISTRY_PROTOCOL=%2
if "%REGISTRY_HOST%"=="" set REGISTRY_HOST=localhost:5000
if "%REGISTRY_PROTOCOL%"=="" set REGISTRY_PROTOCOL=http
echo Starting Docker Registry Browser development server...
echo Registry: %REGISTRY_PROTOCOL%://%REGISTRY_HOST%
echo.
REM Check if .env file exists and load it
if exist ".env" (
echo Loading environment from .env file...
for /f "delims== tokens=1,2" %%a in (.env) do (
if not "%%a"=="" if not "%%b"=="" (
set %%a=%%b
)
)
)
REM Generate proxy configuration and start server
npm run start
pause

View File

@@ -1,25 +0,0 @@
#!/bin/bash
# Development start script for Docker Registry Browser
# Usage: ./start-dev.sh [registry_host] [protocol]
# Set default values
REGISTRY_HOST=${1:-"localhost:5000"}
REGISTRY_PROTOCOL=${2:-"http"}
echo "Starting Docker Registry Browser development server..."
echo "Registry: $REGISTRY_PROTOCOL://$REGISTRY_HOST"
echo ""
# Export environment variables
export REGISTRY_HOST="$REGISTRY_HOST"
export REGISTRY_PROTOCOL="$REGISTRY_PROTOCOL"
# Check if .env file exists and source it
if [ -f ".env" ]; then
echo "Loading environment from .env file..."
export $(cat .env | grep -v '^#' | xargs)
fi
# Generate proxy configuration and start server
npm run start

View File

@@ -9,22 +9,28 @@
<Privileged>false</Privileged>
<Support>https://github.com/programmingPug/docker-registry-browser</Support>
<Project>https://github.com/programmingPug/docker-registry-browser</Project>
<Overview>Docker Registry Browser - A modern web interface for browsing and managing Docker registries. Features include repository browsing, tag listing, image details inspection, and push command generation.</Overview>
<Overview>Docker Registry Browser - A simple web interface for browsing Docker registries. Browse repositories, view tags, and get pull commands. Perfect for managing your local Docker registry.</Overview>
<Category>Tools:</Category>
<WebUI>http://[IP]:[PORT:8080]/</WebUI>
<TemplateURL>https://raw.githubusercontent.com/programmingPug/docker-registry-browser/main/unraid-template.xml</TemplateURL>
<Icon>https://raw.githubusercontent.com/docker/docs/main/assets/images/docker-icon.png</Icon>
<ExtraParams>--add-host=host.docker.internal:host-gateway</ExtraParams>
<ExtraParams></ExtraParams>
<PostArgs/>
<CPUset/>
<DateInstalled/>
<DonateText/>
<DonateLink/>
<Requires/>
<!-- Simplified configuration - only the essentials -->
<Config Name="WebUI Port" Target="80" Default="8080" Mode="tcp" Description="Port for accessing the web interface" Type="Port" Display="always" Required="true" Mask="false">8080</Config>
<Config Name="Registry Host" Target="REGISTRY_HOST" Default="localhost:5000" Mode="" Description="Docker registry host and port (e.g., localhost:5000, registry.example.com:5000)" Type="Variable" Display="always" Required="true" Mask="false">localhost:5000</Config>
<Config Name="Registry Protocol" Target="REGISTRY_PROTOCOL" Default="http" Mode="" Description="Protocol to use for registry connection (http or https)" Type="Variable" Display="always" Required="true" Mask="false">http</Config>
<Config Name="Registry Username" Target="REGISTRY_USERNAME" Default="" Mode="" Description="Username for registry authentication (leave empty for no auth)" Type="Variable" Display="always" Required="false" Mask="false"></Config>
<Config Name="Registry Password" Target="REGISTRY_PASSWORD" Default="" Mode="" Description="Password for registry authentication (leave empty for no auth)" Type="Variable" Display="always" Required="false" Mask="true"></Config>
<Config Name="App Data" Target="/app/data" Default="/mnt/user/appdata/docker-registry-browser" Mode="rw" Description="Application data directory" Type="Path" Display="advanced" Required="false" Mask="false">/mnt/user/appdata/docker-registry-browser</Config>
<Config Name="Registry Host" Target="REGISTRY_HOST" Default="" Mode="" Description="Your Docker registry host and port. Examples: 192.168.1.100:5000 (for Unraid host), registry:5000 (for container), hub.example.com (for remote)" Type="Variable" Display="always" Required="true" Mask="false"></Config>
<Config Name="Registry Protocol" Target="REGISTRY_PROTOCOL" Default="http" Mode="" Description="http for local/insecure registries, https for secure registries" Type="Variable" Display="always" Required="true" Mask="false">http</Config>
<!-- Optional authentication - most users won't need this -->
<Config Name="Username (Optional)" Target="REGISTRY_USERNAME" Default="" Mode="" Description="Leave empty if your registry doesn't require authentication" Type="Variable" Display="advanced" Required="false" Mask="false"></Config>
<Config Name="Password (Optional)" Target="REGISTRY_PASSWORD" Default="" Mode="" Description="Leave empty if your registry doesn't require authentication" Type="Variable" Display="advanced" Required="false" Mask="true"></Config>
</Container>