A containerized APT mirror solution with a web interface. This project provides a complete local Ubuntu / Debian package repository with an admin panel, file hosting capabilities, optional npm package caching, and developer cheatsheets. Perfect for organizations that need to work in a totally offline environment while maintaining access to essential development resources.
- APT Mirror: Local Ubuntu package repository with automatic synchronization using apt-mirror2 (Python/asyncio version) from PyPI
- GPG Signing: Optional per-host signing keys generated via the admin panel — re-signs
Releasefiles so clients can verify the mirror with a real key instead of relying on[trusted=yes]. Sources.list snippets in the admin UI auto-include the correctsigned-by/Signed-Bydirective when a key is present. - NPM Proxy: Optional local npm package registry cache for faster npm installs and reduced bandwidth usage
- Cheatsheets: Offline, searchable markdown cheatsheets downloaded from GitHub repositories you choose (e.g. tldr-pages for console commands)
- Web Interface: web UI for all services
- Multi-Host Setup: Five distinct web services:
mirror.intra- DEB packages repositoryadmin.mirror.intra- Admin panel with authenticationfiles.mirror.intra- File hosting servicenpm.mirror.intra- NPM registry cache (optional)cheatsheets.mirror.intra- Public developer cheatsheets (no authentication)
- Advanced File Manager: File upload/download, directory management, built-in media player for video/audio files, and container image downloads from Docker Hub and GCR
- Multi-Architecture Support: Builds for both AMD64 and ARM64
- Easy Deployment: Simple scripts for building and deployment
- Configurable: Custom domains, sync frequency, admin passwords, and optional npm proxy
- arm64 or amd64 machine
- Docker and Docker Compose
- Linux system
- Complete Mirroring apt repos usually requires a lot of disk space (> 500G)
First, build the Docker images for your architecture:
./build.shThis will create:
dist/ui-apt-mirror-amd64.tar.gz(for x86_64 systems)dist/ui-apt-mirror-arm64.tar.gz(for ARM64 systems)
Run the setup script to deploy the container:
./setup.shThe script will:
- Detect your system architecture
- Ask for your custom domain (default:
mirror.intra) - Configure sync frequency
- Set admin password
- Ask if you need npm proxy functionality (default: Yes)
- Save these settings to
.env - Load the appropriate Docker image
- Start the container
Running ./setup.sh again on an existing install keeps your configuration. Use
./setup.sh --reconfigure to change settings (current values are the defaults)
and ./setup.sh --reset-admin-password to set a new admin password; other users
are kept.
- URL:
http://mirror.intra - Purpose: Browse and download mirrored packages
- Features:
- Package browsing with directory listing
- URL:
http://admin.mirror.intra - Authentication: Auth (username/password)
- Features:
- Mirror status monitoring
- Log viewing
- Documentation
- Files management
- Cheatsheet sources (add GitHub repositories, update, remove)
- User management and settings
Failed logins (web and npm login) are limited per client address and per
username. The container has no IPv6 address, so Docker's userland proxy
connects IPv6 clients and clients on the Docker host itself (127.0.0.1,
::1) from the network gateway, and they all reach the mirror with that one
address, so the mirror can't tell them apart. For that address the limits are 5
failures per username (so a client guessing at one user doesn't lock that
user out for the others), 50 failures in all (so no client can try a password
on every username), and the usual per-username limit, all per 15 minutes.
Any of those clients can still fill these limits, and then a user signing in
from there waits up to 15 minutes, as often as the attacker repeats it. A
browser that has signed in as the user before (in the last 30 days, and since
the last password change) is exempt: it has a limit of its own. npm login
from those clients has no such exemption. A reverse proxy in front of the
mirror has the same problem: its clients share its address, and the mirror
ignores X-Forwarded-For because any client can send one.
- URL:
http://files.mirror.intra - Purpose: File hosting and sharing
- Features:
- Built-in video/audio player supporting MP4, WebM, AVI, MKV, MP3, WAV, FLAC, and more
- Stream large files (optimized for 8GB+ videos) with HTTP range requests
- Horizontal media gallery for quick browsing of files in the same folder
- Autoplay when selecting media files
- URL:
http://cheatsheets.mirror.intra - Authentication: None (public access)
- Purpose: Read-only access to the downloaded cheatsheets
- Features:
- Full-text search across all sources, with snippets
- Browse by source and category
- Works fully offline once sources are downloaded
No cheatsheets are bundled. In the admin panel, open Cheatsheets and paste a public GitHub URL:
- a repository:
https://github.com/<owner>/<repo>(default branch) - or one folder of it:
https://github.com/<owner>/<repo>/tree/<branch>/<folder>, e.g.https://github.com/tldr-pages/tldr/tree/main/pagesfor tldr's console commands
Every .md file under that location becomes a page (README/LICENSE/CONTRIBUTING
files are skipped). The first # Heading is the page title. Sub-folders become
categories, unless the folder contains a categories.json mapping
{"Category": ["relative/path.md", ...]}. Pages are GitHub-flavoured markdown;
formulas written as TeX between double dollars ($$x_1 + x_2$$ inline, or $$
on lines of their own for a block) are rendered with KaTeX, offline. Single
dollars stay plain text. Downloading needs internet access;
Update re-downloads a source and keeps the previous copy if it fails.
Content is stored under data/data/cheatsheets/ and is not part of this repository.
- URL:
http://npm.mirror.intra - Purpose: Local npm package registry cache and private package hosting
- Features:
- Caches npm packages locally for faster installs
- Reduces bandwidth usage
- Transparent proxy to npmjs.org
- Private package publishing (requires authentication)
- Private packages stored separately and never forwarded to npmjs.org
To use the local repository on your Ubuntu systems, add the following to /etc/apt/sources.list:
# Replace mirror.intra with your custom domain
# For Ubuntu 24.04 (Noble)
Types: deb
URIs: http://mirror.intra/archive.ubuntu.com/ubuntu
Suites: noble noble-updates noble-security noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
# For Debian 12 (Bookworm)
deb http://mirror.intra/deb.debian.org/debian bookworm main contrib non-free non-free-firmware
deb http://mirror.intra/security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
deb http://mirror.intra/deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmwareThen update your package lists:
sudo apt updateAccess the admin panel at http://admin.mirror.intra to:
- Monitor sync status
- View logs and statistics
To use the local npm proxy (if enabled during setup), configure npm to use your local registry:
# Configure npm to use the local proxy
npm config set registry http://npm.mirror.intra
# Or for a specific project
npm install --registry http://npm.mirror.intra
# To revert back to the official registry
npm config set registry https://registry.npmjs.orgOther package managers:
# pnpm
pnpm config set registry http://npm.mirror.intra
# Yarn 1
yarn config set registry http://npm.mirror.intra
# Yarn 2+ (it refuses a registry over plain http unless the host is whitelisted)
yarn config set npmRegistryServer http://npm.mirror.intra
yarn config set unsafeHttpWhitelist --json '["npm.mirror.intra"]'To let Corepack download Yarn or pnpm itself through the proxy, also set
COREPACK_NPM_REGISTRY=http://npm.mirror.intra.
The npm proxy will:
- Cache packages locally on first download
- Serve cached packages for subsequent requests
- Automatically fetch from npmjs.org if not cached
- Point tarball URLs (and so lockfiles) at the proxy, so installs keep working when npmjs.org cannot be reached
The npm proxy supports publishing private packages using standard npm commands.
Method 1: Using npm login (recommended)
For npm 9.x+, use the --auth-type=legacy flag:
npm login --registry=http://npm.mirror.intra --auth-type=legacy
# Enter your username and password when prompted
npm whoami --registry=http://npm.mirror.intraMethod 2: Manual token configuration
If npm login doesn't work, you can manually obtain and configure the token:
TOKEN=$(curl -X PUT http://npm.mirror.intra/-/user/org.couchdb.user:admin \
-H "Content-Type: application/json" \
-d '{"name": "admin", "password": "your-password"}' \
| jq -r .token)
echo "//npm.mirror.intra/:_authToken=$TOKEN" >> ~/.npmrc
npm whoami --registry=http://npm.mirror.intraOnce authenticated, publish packages normally:
npm publishImportant Notes:
- All published packages are treated as private packages
- Private packages are stored in
data/data/npm/private/ - Public packages (cached from npmjs.org) are stored in
data/data/npm/public/ - The contents of published packages are NOT sent to npmjs.org; only the check of a new name is (below)
- Authentication tokens for npm are JWT-based and valid for 1 year;
npm logout(andpnpm logout) ends the token on the server. Yarn 2+'syarn npm logoutonly removes it from Yarn's own settings - A name that is a public package on npmjs.org cannot be published (403); publish under a scope of your
own, such as
@yourorg/tool. The first publish of a name is checked with npmjs.org: for a scoped name only the package list of its scope is asked for, so the name itself is never sent; an unscoped name is looked up as is. Later versions of a package already published here are not checked again - If npmjs.org gives no clear answer (an error, a rate limit), the publish is refused with 503; try again later. If npmjs.org cannot be reached at all, only a scoped name whose scope has no public package in the cache can be published; unscoped names wait until npmjs.org can be reached
Use the file repository at http://files.mirror.intra to:
- Upload files via web interface
- Browse uploaded files
To upgrade to the latest version:
./upgrade.shThe upgrade script will:
- Check connectivity to the official website
- Ask you to choose between current architecture or all architectures
- Back up your configuration, users, repository signing keys and cheatsheet
sources list to
backups/pre-upgrade-<date>.tar.gz - Download the latest version and install the new image and scripts
- Run
setup.sh --upgrade, which asks nothing and keeps your configuration - Clean up temporary files
| What | Where | On upgrade |
|---|---|---|
| Settings (domain, sync frequency, timezone, npm proxy) and your own lines | .env |
kept |
| Your own compose changes (ports, volumes, …) | docker-compose.override.yml |
kept |
| Repositories and package filters | data/conf/apt-mirror/mirror.list |
kept |
| Users and passwords | data/auth/.htpasswd |
kept |
| Login signing secret | data/auth/.jwt-secret |
kept |
| Nginx overrides | data/conf/nginx/custom/<site>.conf |
kept |
| Mirrored packages, GPG keys, files, private files, npm cache, cheatsheets | data/data/ |
kept |
docker-compose.yml |
stock file | replaced (holds no settings) |
| Nginx site configs | generated in the container from .env |
regenerated |
Do not edit docker-compose.yml: put changes in docker-compose.override.yml
instead (same format, merged on top by start.sh). To change an nginx site,
copy it from the running container (docker exec ui-apt-mirror cat /etc/nginx/sites-available/files.mirror.intra.conf) to
data/conf/nginx/custom/files.mirror.intra.conf and edit it there; delete the
file to go back to the stock config. An override that is the previous
release's stock config, unedited, is removed on upgrade and the current stock
config is used. Any other change, even only a different host name, makes it
your override, which is kept.
./upgrade.sh upgrades from the latest release only (3.0 to 3.1, then 3.1 to
3.2, and so on). Version 3 does not upgrade 2.x installs: its setup.sh,
upgrade.sh and start.sh recognise one (it has no .ui-apt-mirror-version
file) and stop before changing anything.
- Back up the 2.x install's settings, users, signing keys and files (in its
directory):
tar -czf ../ui-apt-mirror-2.x-backup.tar.gz .env docker-compose*.yml data/conf data/auth \ data/data/apt-mirror/gpg data/data/files data/data/files-private - Install version 3 in a new directory. It stops and replaces the 2.x
container, which has the same name and ports:
curl -fsSLO https://ui-apt-mirror.dbashkatov.com/downloads/install.sh && bash install.sh - Copy over what you want to keep with
cp -a, then rundocker restart ui-apt-mirror:data/data/filesanddata/data/files-private: public and private filesdata/data/apt-mirror: mirrored packages and the signing keys apt clients trustdata/conf/apt-mirror/mirror.list: repositories
- Set up users, npm packages and cheatsheet sources again in the admin panel.
ui-apt-mirror/
├── build.sh # Build script for Docker images
├── setup.sh # Deployment and configuration script
├── start.sh # Start the container
├── upgrade.sh # Upgrade script for latest version
├── README.md # This file
├── .env # Settings (written by setup.sh, kept on upgrade)
├── docker-compose.src.yml # Docker Compose template
├── docker-compose.yml # Stock copy of the template (replaced on upgrade)
├── docker-compose.override.yml # Optional: your compose changes (kept on upgrade)
├── backups/ # Configuration backups made by upgrade.sh
├── nginx/ # Nginx site templates baked into the image
├── Dockerfile # Multi-stage Docker build
├── entrypoint.sh # Container startup script
├── admin/ # Admin panel React application source
│ ├── app/ # React Router application
│ ├── build/ # Built admin panel assets
│ ├── package.json # Node.js dependencies
│ └── vite.config.ts # Vite build configuration
├── scripts/ # Service scripts
│ ├── health-check.sh # System health monitoring
│ ├── mirror-sync.sh # APT mirror synchronization
│ ├── resource-monitor.sh # Resource usage monitoring
│ ├── start-mirror.sh # Start mirror services
│ └── stop-mirror.sh # Stop mirror services
├── dist/ # Built Docker images
│ ├── ui-apt-mirror-amd64.tar.gz
│ └── ui-apt-mirror-arm64.tar.gz
└── data/ # Persistent data and configuration
├── auth/ # Users (.htpasswd) and login secret (.jwt-secret)
├── conf/ # Configuration files
│ ├── apt-mirror/ # APT mirror configuration (mirror.list)
│ └── nginx/custom/ # Optional nginx site overrides
├── data/ # Application data
│ ├── apt-mirror/ # APT mirror package data
│ ├── files/ # File hosting data
│ ├── files-private/ # Private files
│ ├── cheatsheets/ # Downloaded cheatsheet sources
│ └── npm/ # NPM cache data
└── logs/ # Log files
├── apt-mirror/ # APT mirror logs
└── nginx/ # Nginx logs
An upgrade recognises the previous release's stock nginx site configs among
the overrides in data/conf/nginx/custom/ and replaces them with the new
stock configs. After tagging a release, replace the list of those configs with
the new release's and commit it, so the next version recognises them:
git tag v3.0.0
nginx/sites-setup/update-released-sites.sh v3.0.0
git commit -m "List the stock nginx site configs of v3.0.0" nginx/sites-setup/released-sites.sha256nginx/sites-setup/test.sh checks that the list matches the tag it names.
This project is licensed under the MIT License.
- apt-mirror2 - The Python/asyncio APT mirroring tool from PyPI
- nginx - Web server
- skopeo - For container image management