When looking for self-hosted image hosting or private cloud storage solutions, MinIO is often the first choice. However, for individual developers or small teams, Garage is a lighter, higher-performance, and more flexibly configured alternative. It is written in Rust, has extremely low resource usage, and is perfectly compatible with the AWS S3 protocol.
This article will detail how to deploy a production-ready Garage object storage service on the 1Panel panel using Docker Compose. This article will record and solve common configuration pitfalls (such as CORS cross-origin issues, WebUI authentication, domain binding, etc.) to create a private OSS that is both secure and easy to use.
s3.example.com —— S3 API Interface (For upload/management tools)img.example.com —— Public Access Domain (For referencing images in blogs/websites)admin.example.com —— Web Management Dashboard (WebUI)garage.toml)/opt/garage/config.garage.toml and fill in the following content:docker-compose.yml)Key Points:
garage.toml to automatically read the admin_token.$ symbol in Bcrypt password hashes must be written as $$.Create the orchestration garage-stack in 1Panel with the following content:
Start the orchestration and ensure both containers show as "Running".
Newly started Garage is in an "unassigned role" state and must be initialized.
View Node ID:
Execute in 1Panel terminal or SSH:
Copy the displayed Node ID, e.g., a8795c63e0c82b0b.
At this point, refresh the WebUI, and the status should turn to green Healthy.
To use it as an image host, we need to create a bucket and allow public access.
In the WebUI, click Buckets -> Create Bucket, and name it photo.
This is key to allowing images to be accessed directly via https://img.example.com/xxx.jpg.
You can also set the Alias directly in the webui and check the box to enable Website.

blog-key).photo bucket.
docker exec -it garage /garage bucket allow --read --write --key blog-key photoWe need to establish three reverse proxies in the "Websites" function of 1Panel:
| Domain | Proxy Address | Purpose |
|---|---|---|
s3.example.com | http://127.0.0.1:3900 | S3 API (Fill this in PicList/Lsky) |
admin.example.com | http://127.0.0.1:3903 | WebUI Management Dashboard |
img.example.com | http://127.0.0.1:3902 | Public Image Access (Note the port is 3902) |
Remember to apply for and enable HTTPS certificates for all domains.
Security Hint: When using Host mode, it is recommended to only allow ports 80/443 for Nginx in the 1Panel firewall or the cloud provider's security group. Do not directly expose Garage's ports 3900-3903 to the outside world; let all requests pass through the Nginx reverse proxy for better security.
If you reference images on a blog or other websites, the browser will block cross-origin requests. We need to forcibly allow CORS in the Nginx configuration for img.example.com.
In 1Panel Website Settings -> Configuration File, find the location / block and add the following:
Save and reload Nginx.
Now the service is fully ready. Fill in the following configuration in your image hosting tool:
https://s3.example.comphotogarage (Default)https://img.example.commetadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
# Single node deployment must be set to 1; adjust for multi-node as needed
replication_factor = 1
# 32-byte random key (generated by openssl rand -hex 32)
rpc_secret = "c9a4b8d7e6f5a1c2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6"
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = "s3.example.com" # Fill in your API domain
[s3_web]
bind_addr = "[::]:3902"
root_domain = "img.example.com" # Fill in your public access domain
index = "index.html"
[admin]
api_bind_addr = "[::]:3903"
metrics_token = "change_me_please" # Token for metrics scraping
admin_token = "change_me_please" # Admin token, WebUI will read automatically
services:
garage:
image: dxflrs/garage:v2.2.0 # Newer version
container_name: garage
restart: always
network_mode: host # Host mode, directly use host ports
volumes:
- /opt/garage/config/garage.toml:/etc/garage.toml
- /opt/garage/meta:/var/lib/garage/meta
- /opt/garage/data:/var/lib/garage/data
garage-webui:
image: khairul169/garage-webui:latest
container_name: garage-webui
restart: always
network_mode: host # WebUI also uses Host mode for convenient communication
volumes:
# Must mount the configuration file, otherwise WebUI cannot automatically authenticate
- /opt/garage/config/garage.toml:/etc/garage.toml:ro
environment:
API_BASE_URL: "http://127.0.0.1:3903"
S3_ENDPOINT_URL: "http://127.0.0.1:3900"
# Native login config: username:Bcrypt hash (note $$ escaping)
# Generation command: python3 -c "import bcrypt; print(bcrypt.hashpw(b'your_password', bcrypt.gensalt()).decode())"
# The example password below is 123456
AUTH_USER_PASS: 'admin:$$2b$$12$$zm7W20msXJc5fVgj.fjGR.GfGQS2M1abvQf5k.aWZTQBZGv4QexpC'
docker exec -it garage /garage status
# Assign 500GB logical space (does not immediately occupy disk space)
docker exec -it garage /garage layout assign -z dc1 -c 500G <YourNodeID>
# Apply changes
docker exec -it garage /garage layout apply --version 1
# 1. Bind domain alias (let Garage know this domain corresponds to the photo bucket)
docker exec -it garage /garage bucket alias set photo img.example.com
# 2. Enable website mode (allow anonymous GET requests)
docker exec -it garage /garage bucket website --allow photo --index index.html
location / {
# CORS Configuration
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, HEAD' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
# Remaining configuration
}
}