A small, Make-driven deployment for running a private HTTP Docker Registry with Docker Compose.
The project provides:
- Compose-based Registry lifecycle management
- Persistent Registry storage on the host
- Idempotent client configuration for Docker Engine
- Safe merging of
/etc/docker/daemon.json - Configuration validation, backup, and rollback
- A basic push smoke test
This deployment uses plain HTTP without authentication. It is intended only for trusted, firewall-restricted internal networks.
.
├── compose.yaml
├── Makefile
├── .env.example
├── README.md
└── scripts/
└── configure-client.sh
Registry blobs and manifests are stored under /var/lib/docker-registry by default. This keeps persistent application data under /var/lib rather than placing it in /etc, which should remain reserved for configuration.
Docker client configuration is managed through the standard Linux Docker Engine configuration file:
/etc/docker/daemon.json
- Linux
- Docker Engine
- Docker Compose v2 (
docker compose) - GNU Make
sudocurlformake status
- Linux Docker Engine
- GNU Make
- Python 3
sudosystemctlorservicefor restarting Docker
Create the local environment file:
cp .env.example .envReview and edit it as needed:
nano .envStart the Registry:
make upCheck the container and API status:
make ps
make statusThe default endpoint is:
http://SERVER_IP:5000
Start or update the Registry service:
make upStop and remove the Compose container and network:
make downPersistent Registry data is not removed by make down.
Restart the Registry container:
make restartFollow the Registry logs:
make logsShow the current service state:
make psRender the fully resolved Compose configuration:
make configPull the configured Registry image without starting the service:
make pullBecause this Registry uses plain HTTP, every Docker host that pushes to or pulls from it must list the Registry endpoint under Docker Engine's insecure-registries configuration.
Run the following command on each Docker client:
make connect REGISTRY=172.20.4.171:5000When the port is omitted, port 5000 is used automatically:
make connect REGISTRY=172.20.4.171Hostnames are also supported:
make connect REGISTRY=registry.internal:5000Bracketed IPv6 addresses are supported:
make connect REGISTRY='[2001:db8::10]:5000'Remove an endpoint from Docker's insecure-registries list:
make disconnect REGISTRY=172.20.4.171:5000Display the insecure registries currently reported by Docker Engine:
make client-configThe client configuration script performs the following operations:
- Reads the existing
/etc/docker/daemon.json, or starts with an empty JSON object if the file does not exist. - Preserves all unrelated Docker daemon settings.
- Adds or removes only the requested
insecure-registriesentry. - Removes duplicate entries while preserving their original order.
- Creates a timestamped backup beside the original configuration file.
- Validates the complete generated configuration with
dockerd --validatewhendockerdis available. - Installs the updated file with root ownership and mode
0644. - Restarts Docker through
systemctlorservice. - Restores the previous configuration automatically if Docker fails to restart.
The operation is idempotent. Adding an existing endpoint or removing an absent endpoint produces no configuration change and does not restart Docker.
Run the built-in smoke test on the Registry server:
make smokeThe smoke test pulls alpine:latest, tags it for the local Registry, pushes it, and removes the temporary local tag.
A manual test from a configured Docker client can be performed as follows:
docker pull alpine:latest
docker tag alpine:latest 172.20.4.171:5000/alpine:latest
docker push 172.20.4.171:5000/alpine:latest
docker pull 172.20.4.171:5000/alpine:latestTo query the Registry API directly:
curl http://172.20.4.171:5000/v2/A successful Registry normally returns an empty JSON object:
{}The deployment is configured through .env.
REGISTRY_IMAGE=registry:2
CONTAINER_NAME=registry
RESTART_POLICY=unless-stopped
REGISTRY_BIND_ADDRESS=0.0.0.0
REGISTRY_PORT=5000
REGISTRY_DATA_DIR=/var/lib/docker-registry
REGISTRY_STORAGE_DELETE_ENABLED=falseRegistry container image to run.
REGISTRY_IMAGE=registry:2Explicit Docker container name.
CONTAINER_NAME=registryDocker restart policy applied to the Registry container.
RESTART_POLICY=unless-stoppedHost address on which the Registry port is published.
The default listens on every interface:
REGISTRY_BIND_ADDRESS=0.0.0.0For an internal deployment, binding only to the server's LAN address is safer:
REGISTRY_BIND_ADDRESS=172.20.4.171Host port mapped to port 5000 inside the Registry container.
REGISTRY_PORT=5000Absolute host path used for persistent Registry storage.
REGISTRY_DATA_DIR=/var/lib/docker-registrymake prepare creates this directory as root:root with mode 0750.
Controls whether the Registry API permits image deletion.
REGISTRY_STORAGE_DELETE_ENABLED=falseKeep deletion disabled unless a deliberate retention and garbage-collection procedure has been established.
make help Show available targets
make init Create .env from .env.example when missing
make check Verify Docker Engine and Docker Compose
make prepare Create the persistent storage directory
make config Print the resolved Compose configuration
make pull Pull the configured Registry image
make up Create or update and start the Registry
make down Stop the Registry while preserving data
make restart Restart the Registry container
make logs Follow Registry logs
make ps Show Compose service status
make status Check the local Registry HTTP API
make smoke Push a temporary Alpine image
make connect Add an insecure Registry endpoint to Docker
make disconnect Remove an insecure Registry endpoint from Docker
make client-config Show Docker's insecure Registry configuration
Make variables can be overridden per invocation:
make up ENV_FILE=.env.production PROJECT_NAME=lab-registryThe Registry storage directory is bind-mounted into the container:
/var/lib/docker-registry -> /var/lib/registry
Removing or recreating the container does not delete stored images. Back up this directory using the same policy applied to other persistent service data.
To inspect its disk usage:
sudo du -sh /var/lib/docker-registryDo not manually edit files inside the Registry storage directory while the service is running.
This configuration intentionally provides no TLS and no authentication. As a result:
- Registry traffic is unencrypted.
- Docker credentials must not be sent to this endpoint.
- Any reachable host may be able to push or pull images.
- Image contents and tags may be modified by unauthorized internal clients.
Use this deployment only when all of the following are true:
- The Registry is on a trusted internal network.
- Firewall rules restrict access to approved hosts or subnets.
- The Registry port is not exposed to the public Internet.
- Stored images do not contain embedded secrets.
For production or multi-user environments, place the Registry behind TLS and add an authentication layer. Once TLS is enabled with a trusted certificate, remove the endpoint from Docker's insecure-registries list.
Inspect the service state and logs:
make ps
make logsValidate the resolved Compose configuration:
make configCheck whether the configured port is already in use:
sudo ss -lntp | grep ':5000'A typical message is:
http: server gave HTTP response to HTTPS client
The Docker client has not been configured to trust the HTTP Registry. Run:
make connect REGISTRY=SERVER_IP:5000Then verify the applied configuration:
make client-configThe configuration script automatically restores the previous daemon.json when restart fails. Inspect Docker logs for the underlying error:
sudo journalctl -u docker --no-pager -n 200Existing backups are stored beside the Docker daemon configuration using names similar to:
/etc/docker/daemon.json.backup-20260729-204000-12345
Recreate the directory with the expected ownership and mode:
make prepareThen restart the service:
make restart