|
| 1 | +--- |
| 2 | +title: "How to Use GitHub Actions + image-syncer for Automated Image Sync from Docker Hub to Azure ACR" |
| 3 | +date: 2026-01-25 |
| 4 | +truncate: true |
| 5 | +englishSlug: "how-to-sync-docker-hub-to-azure-acr-with-github" |
| 6 | +tags: [Docker Hub, Azure ACR, GitHub Actions, Image Sync, DevOps] |
| 7 | +--- |
| 8 | + |
| 9 | +## Automating Image Sync from Docker Hub to Azure ACR |
| 10 | + |
| 11 | +> This article explains how to use GitHub Actions and the image-syncer tool to automate image synchronization from Docker Hub to Azure Container Registry, solving the problem of slow Docker Hub access in mainland China and some Azure regions, while improving image availability and deployment efficiency in Azure environments. |
| 12 | +
|
| 13 | +## Background / Introduction |
| 14 | + |
| 15 | +The HagiCode project uses Docker images as its core runtime components, with the main images hosted on Docker Hub. As the project has evolved and Azure deployment needs have grown, we encountered the following pain points: |
| 16 | +- Slow image pulls, because access to Docker Hub is limited in mainland China and some Azure regions |
| 17 | +- Relying on a single image source creates a single point of failure risk |
| 18 | +- Using Azure Container Registry in Azure environments provides better network performance and integration experience |
| 19 | + |
| 20 | +To solve these problems, we need to establish an automated image synchronization mechanism that regularly syncs images from Docker Hub to Azure ACR, ensuring users get faster image pull speeds and higher availability in Azure environments. |
| 21 | + |
| 22 | +## About HagiCode |
| 23 | + |
| 24 | +We are building HagiCode, an AI-driven coding assistant that makes development smarter, more convenient, and more enjoyable. |
| 25 | + |
| 26 | +Smart: AI assistance throughout the entire process, from idea to code, boosting coding efficiency several times over. Convenient: Multi-threaded concurrent operations make full use of resources and keep the development workflow smooth. Fun: Gamification and an achievement system make coding less dull and more rewarding. |
| 27 | + |
| 28 | +The project is evolving rapidly. If you are interested in technical writing, knowledge management, or AI-assisted development, welcome to check it out on GitHub. |
| 29 | + |
| 30 | +## Technical Solution Comparison |
| 31 | + |
| 32 | +When defining the solution, we compared multiple technical approaches: |
| 33 | + |
| 34 | +### 1. image-syncer (final choice) |
| 35 | +- Incremental sync: only synchronizes changed image layers, significantly reducing network transfer |
| 36 | +- Resume support: synchronization can resume after network interruptions |
| 37 | +- Concurrency control: supports configurable concurrency to improve large image sync efficiency |
| 38 | +- Robust error handling: built-in retry mechanism for failures (3 times by default) |
| 39 | +- Lightweight deployment: single binary with no dependencies |
| 40 | +- Multi-registry support: compatible with Docker Hub, Azure ACR, Harbor, and more |
| 41 | + |
| 42 | +### 2. Docker CLI |
| 43 | +- No incremental sync support: each run requires pulling the full image content |
| 44 | +- Lower efficiency: large network transfer volume and longer execution time |
| 45 | +- Simple and easy to use: relies on familiar `docker pull` / `docker push` commands |
| 46 | + |
| 47 | +### 3. Azure CLI |
| 48 | +- Higher complexity: requires Azure CLI authentication setup |
| 49 | +- Functional limitations: `az acr import` is relatively limited |
| 50 | +- Native integration: integrates well with Azure services |
| 51 | + |
| 52 | +## Architecture Design Decisions |
| 53 | + |
| 54 | +### Decision 1: Set the sync frequency to daily at 00:00 UTC |
| 55 | +- Balances image freshness with resource consumption |
| 56 | +- Avoids peak business hours and reduces impact on other operations |
| 57 | +- Docker Hub images are usually updated after daily builds |
| 58 | + |
| 59 | +### Decision 2: Sync all image tags |
| 60 | +- Maintains full consistency with Docker Hub |
| 61 | +- Provides flexible version choices for users |
| 62 | +- Simplifies sync logic by avoiding complex tag filtering rules |
| 63 | + |
| 64 | +### Decision 3: Store credentials in GitHub Secrets |
| 65 | +- Natively supported by GitHub Actions with strong security |
| 66 | +- Simple to configure and easy to manage and maintain |
| 67 | +- Supports repository-level access control |
| 68 | + |
| 69 | +## Risk Assessment and Mitigation |
| 70 | + |
| 71 | +### Risk 1: Azure ACR credential leakage |
| 72 | +- Use GitHub Secrets for encrypted storage |
| 73 | +- Rotate ACR passwords regularly |
| 74 | +- Limit ACR account permissions to push-only |
| 75 | +- Monitor ACR access logs |
| 76 | + |
| 77 | +### Risk 2: Sync failures causing image inconsistency |
| 78 | +- image-syncer includes a built-in incremental sync mechanism |
| 79 | +- Automatic retry on failure (3 times by default) |
| 80 | +- Detailed error logs and failure notifications |
| 81 | +- Resume support |
| 82 | + |
| 83 | +### Risk 3: Excessive resource consumption |
| 84 | +- Incremental sync reduces network transfer |
| 85 | +- Configurable concurrency (`10` in the current setup) |
| 86 | +- Monitor the number and size of synchronized images |
| 87 | +- Run synchronization during off-peak hours |
| 88 | + |
| 89 | +## Core Solution |
| 90 | + |
| 91 | +We use an automated GitHub Actions + image-syncer solution to synchronize images from Docker Hub to Azure ACR. |
| 92 | + |
| 93 | +## Implementation Steps |
| 94 | + |
| 95 | +### 1. Preparation |
| 96 | +- Create or confirm an Azure Container Registry in Azure Portal |
| 97 | +- Create ACR access credentials (username and password) |
| 98 | +- Confirm access permissions for the Docker Hub image repository |
| 99 | + |
| 100 | +### 2. Configure GitHub Secrets |
| 101 | +Add the following secrets in the GitHub repository settings: |
| 102 | +- AZURE_ACR_USERNAME: Azure ACR username |
| 103 | +- AZURE_ACR_PASSWORD: Azure ACR password |
| 104 | + |
| 105 | +### 3. Create the GitHub Actions workflow |
| 106 | +Configure the workflow in `.github/workflows/sync-docker-acr.yml`: |
| 107 | +- Scheduled trigger: every day at 00:00 UTC |
| 108 | +- Manual trigger: supports `workflow_dispatch` |
| 109 | +- Extra trigger: run when the `publish` branch receives a push (for fast synchronization) |
| 110 | + |
| 111 | +### 4. Workflow execution flow |
| 112 | + |
| 113 | +| Sequence | Participant | Action | Description | |
| 114 | +| --- | --- | --- | --- | |
| 115 | +| 1 | GitHub Actions | Trigger workflow | Triggered by schedule, manual run, or a push to the `publish` branch | |
| 116 | +| 2 | GitHub Actions → image-syncer | Download and run the sync tool | Enter the actual sync phase | |
| 117 | +| 3 | image-syncer → Docker Hub | Fetch image manifests and tag list | Read source repository metadata | |
| 118 | +| 4 | image-syncer → Azure ACR | Fetch existing image information from the target repository | Determine the current target-side state | |
| 119 | +| 5 | image-syncer | Compare source and target differences | Identify image layers that need to be synchronized | |
| 120 | +| 6 | image-syncer → Docker Hub | Pull changed image layers | Transfer only the content that needs updating | |
| 121 | +| 7 | image-syncer → Azure ACR | Push changed image layers | Complete incremental synchronization | |
| 122 | +| 8 | image-syncer → GitHub Actions | Return synchronization statistics | Includes results, differences, and error information | |
| 123 | +| 9 | GitHub Actions | Record logs and upload artifacts | Useful for follow-up auditing and troubleshooting | |
| 124 | + |
| 125 | +## GitHub Actions Workflow Implementation |
| 126 | + |
| 127 | +Here is the actual workflow configuration in use (`.github/workflows/sync-docker-acr.yml`): |
| 128 | + |
| 129 | +```yaml |
| 130 | +name: Sync Docker Image to Azure ACR |
| 131 | + |
| 132 | +on: |
| 133 | + schedule: |
| 134 | + - cron: "0 0 * * *" # Every day at 00:00 UTC |
| 135 | + workflow_dispatch: # Manual trigger |
| 136 | + push: |
| 137 | + branches: [publish] |
| 138 | + |
| 139 | +permissions: |
| 140 | + contents: read |
| 141 | + |
| 142 | +jobs: |
| 143 | + sync: |
| 144 | + runs-on: ubuntu-latest |
| 145 | + |
| 146 | + steps: |
| 147 | + - name: Checkout code |
| 148 | + uses: actions/checkout@v4 |
| 149 | + |
| 150 | + - name: Download image-syncer |
| 151 | + run: | |
| 152 | + # Download the image-syncer binary |
| 153 | + wget https://github.com/AliyunContainerService/image-syncer/releases/download/v1.5.5/image-syncer-v1.5.5-linux-amd64.tar.gz |
| 154 | + tar -zxvf image-syncer-v1.5.5-linux-amd64.tar.gz |
| 155 | + chmod +x image-syncer |
| 156 | +
|
| 157 | + - name: Create auth config |
| 158 | + run: | |
| 159 | + # Generate the authentication configuration file (YAML format) |
| 160 | + cat > auth.yaml <<EOF |
| 161 | + hagicode.azurecr.io: |
| 162 | + username: "${{ secrets.AZURE_ACR_USERNAME }}" |
| 163 | + password: "${{ secrets.AZURE_ACR_PASSWORD }}" |
| 164 | + EOF |
| 165 | +
|
| 166 | + - name: Create images config |
| 167 | + run: | |
| 168 | + # Generate the image synchronization configuration file (YAML format) |
| 169 | + cat > images.yaml <<EOF |
| 170 | + docker.io/newbe36524/hagicode: hagicode.azurecr.io/hagicode |
| 171 | + EOF |
| 172 | +
|
| 173 | + - name: Run image-syncer |
| 174 | + run: | |
| 175 | + # Run synchronization (using the newer --auth and --images parameters) |
| 176 | + ./image-syncer --auth=./auth.yaml --images=./images.yaml --proc=10 --retries=3 |
| 177 | +
|
| 178 | + - name: Upload logs |
| 179 | + if: always() |
| 180 | + uses: actions/upload-artifact@v4 |
| 181 | + with: |
| 182 | + name: sync-logs |
| 183 | + path: image-syncer-*.log |
| 184 | + retention-days: 7 |
| 185 | +``` |
| 186 | +
|
| 187 | +## Configuration Details |
| 188 | +
|
| 189 | +### 1. Trigger conditions |
| 190 | +- Scheduled trigger: `cron: "0 0 * * *"` - runs every day at 00:00 UTC |
| 191 | +- Manual trigger: `workflow_dispatch` - allows users to run it manually in the GitHub UI |
| 192 | +- Push trigger: `push: branches: [publish]` - triggered when the publish branch receives a push (for fast synchronization) |
| 193 | + |
| 194 | +### 2. Authentication configuration (`auth.yaml`) |
| 195 | +```yaml |
| 196 | +hagicode.azurecr.io: |
| 197 | + username: "${{ secrets.AZURE_ACR_USERNAME }}" |
| 198 | + password: "${{ secrets.AZURE_ACR_PASSWORD }}" |
| 199 | +``` |
| 200 | + |
| 201 | +### 3. Image sync configuration |
| 202 | +```yaml |
| 203 | +docker.io/newbe36524/hagicode: hagicode.azurecr.io/hagicode |
| 204 | +``` |
| 205 | +This configuration means synchronizing all tags from `docker.io/newbe36524/hagicode` to `hagicode.azurecr.io/hagicode` |
| 206 | + |
| 207 | +### 4. image-syncer parameters |
| 208 | +- `--auth=./auth.yaml`: path to the authentication configuration file |
| 209 | +- `--images=./images.yaml`: path to the image synchronization configuration file |
| 210 | +- `--proc=10`: set concurrency to `10` |
| 211 | +- `--retries=3`: retry failures `3` times |
| 212 | + |
| 213 | +## GitHub Secrets Configuration Checklist |
| 214 | + |
| 215 | +Configure the following in `Settings → Secrets and variables → Actions` in the GitHub repository: |
| 216 | + |
| 217 | +| Secret Name | Description | Example Value | How to Get It | |
| 218 | +|------------|------|--------|---------| |
| 219 | +| AZURE_ACR_USERNAME | Azure ACR username | hagicode | Azure Portal → ACR → Access keys | |
| 220 | +| AZURE_ACR_PASSWORD | Azure ACR password | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | Azure Portal → ACR → Access keys → Password | |
| 221 | + |
| 222 | +## Usage Instructions |
| 223 | + |
| 224 | +### 1. Manually trigger synchronization |
| 225 | +1. Open the `Actions` tab of the GitHub repository |
| 226 | +2. Select the `Sync Docker Image to Azure ACR` workflow |
| 227 | +3. Click the `Run workflow` button |
| 228 | +4. Choose the branch and click `Run workflow` to confirm |
| 229 | + |
| 230 | +### 2. View synchronization logs |
| 231 | +1. Click a specific workflow run record on the `Actions` page |
| 232 | +2. View the execution logs for each step |
| 233 | +3. Download the `sync-logs` file from the `Artifacts` section at the bottom of the page |
| 234 | + |
| 235 | +### 3. Verify synchronization results |
| 236 | +```bash |
| 237 | +# Log in to Azure ACR |
| 238 | +az acr login --name hagicode |
| 239 | +
|
| 240 | +# List images and their tags |
| 241 | +az acr repository show-tags --name hagicode --repository hagicode --output table |
| 242 | +``` |
| 243 | + |
| 244 | +## Notes and Best Practices |
| 245 | + |
| 246 | +### 1. Security recommendations |
| 247 | +- Rotate Azure ACR passwords regularly (recommended every 90 days) |
| 248 | +- Use a dedicated ACR service account with push-only permissions |
| 249 | +- Monitor ACR access logs to detect abnormal access in time |
| 250 | +- Do not output credentials in logs |
| 251 | +- Do not commit credentials to the code repository |
| 252 | + |
| 253 | +### 2. Performance optimization |
| 254 | +- Adjust the `--proc` parameter: tune concurrency based on network bandwidth (recommended `5-20`) |
| 255 | +- Monitor synchronization time: if it takes too long, consider reducing concurrency |
| 256 | +- Clean up logs regularly: set a reasonable `retention-days` value (`7` days in the current setup) |
| 257 | + |
| 258 | +### 3. Troubleshooting |
| 259 | + |
| 260 | +#### Issue 1: Authentication failed |
| 261 | +``` |
| 262 | +Error: failed to authenticate to hagicode.azurecr.io |
| 263 | +``` |
| 264 | +Solution: |
| 265 | +1. Check whether GitHub Secrets are configured correctly |
| 266 | +2. Verify whether the Azure ACR password has expired |
| 267 | +3. Confirm whether the ACR service account permissions are correct |
| 268 | +
|
| 269 | +#### Issue 2: Network timeout |
| 270 | +``` |
| 271 | +Error: timeout waiting for response |
| 272 | +``` |
| 273 | +Solution: |
| 274 | +1. Check network connectivity |
| 275 | +2. Reduce concurrency (`--proc` parameter) |
| 276 | +3. Wait for the network to recover and trigger the workflow again |
| 277 | +
|
| 278 | +#### Issue 3: Incomplete image synchronization |
| 279 | +``` |
| 280 | +Warning: some tags failed to sync |
| 281 | +``` |
| 282 | +Solution: |
| 283 | +1. Check the synchronization logs to identify failed tags |
| 284 | +2. Manually trigger the workflow to synchronize again |
| 285 | +3. Verify that the source image on Docker Hub is working properly |
| 286 | +
|
| 287 | +### 4. Monitoring and alerts |
| 288 | +- Regularly check the `Actions` page to confirm workflow run status |
| 289 | +- Configure GitHub notifications to receive workflow failure alerts promptly |
| 290 | +- Monitor Azure ACR storage usage |
| 291 | +- Regularly verify tag consistency |
| 292 | +
|
| 293 | +## Frequently Asked Questions and Solutions |
| 294 | +
|
| 295 | +### Q1: How do I sync specific tags instead of all tags? |
| 296 | +
|
| 297 | +Modify the `images.yaml` configuration file: |
| 298 | +```yaml |
| 299 | +# Sync only the latest and v1.0 tags |
| 300 | +docker.io/newbe36524/hagicode:latest: hagicode.azurecr.io/hagicode:latest |
| 301 | +docker.io/newbe36524/hagicode:v1.0: hagicode.azurecr.io/hagicode:v1.0 |
| 302 | +``` |
| 303 | + |
| 304 | +### Q2: How do I sync multiple image repositories? |
| 305 | + |
| 306 | +Add multiple lines in `images.yaml`: |
| 307 | +```yaml |
| 308 | +docker.io/newbe36524/hagicode: hagicode.azurecr.io/hagicode |
| 309 | +docker.io/newbe36524/another-image: hagicode.azurecr.io/another-image |
| 310 | +``` |
| 311 | +
|
| 312 | +### Q3: How do I retry after a synchronization failure? |
| 313 | +- Automatic retry: image-syncer includes a built-in retry mechanism (`3` times by default) |
| 314 | +- Manual retry: click `Re-run all jobs` on the GitHub Actions page |
| 315 | + |
| 316 | +### Q4: How do I view detailed synchronization progress? |
| 317 | +- View real-time logs on the `Actions` page |
| 318 | +- Download the `sync-logs` artifact to see the full log file |
| 319 | +- The log file includes the synchronization status and transfer speed for each tag |
| 320 | + |
| 321 | +### Q5: How long does synchronization take? |
| 322 | +- Initial full synchronization: typically takes `10-30` minutes depending on image size |
| 323 | +- Incremental synchronization: usually `2-5` minutes if image changes are small |
| 324 | +- Time depends on network bandwidth, image size, and concurrency settings |
| 325 | + |
| 326 | +## Suggested Enhancements |
| 327 | + |
| 328 | +### 1. Add synchronization notifications |
| 329 | +Add a notification step to the workflow: |
| 330 | +```yaml |
| 331 | +- name: Notify on success |
| 332 | + if: success() |
| 333 | + run: | |
| 334 | + echo "Docker images synced successfully to Azure ACR" |
| 335 | +``` |
| 336 | + |
| 337 | +### 2. Implement image tag filtering |
| 338 | +Add tag filtering logic to the workflow: |
| 339 | +```yaml |
| 340 | +- name: Filter tags |
| 341 | + run: | |
| 342 | + # Sync only tags that start with v |
| 343 | + echo "docker.io/newbe36524/hagicode:v* : hagicode.azurecr.io/hagicode:v*" > images.yaml |
| 344 | +``` |
| 345 | + |
| 346 | +### 3. Add a synchronization statistics report |
| 347 | +```yaml |
| 348 | +- name: Generate report |
| 349 | + if: always() |
| 350 | + run: | |
| 351 | + echo "## Sync Report" >> $GITHUB_STEP_SUMMARY |
| 352 | + echo "- Total tags: $(grep -c 'synced' image-syncer-*.log)" >> $GITHUB_STEP_SUMMARY |
| 353 | + echo "- Sync time: ${{ steps.sync.outputs.duration }}" >> $GITHUB_STEP_SUMMARY |
| 354 | +``` |
| 355 | + |
| 356 | +## Conclusion |
| 357 | + |
| 358 | +With the method introduced in this article, we successfully implemented automated image synchronization from Docker Hub to Azure ACR. This solution uses the scheduled and manual trigger capabilities of GitHub Actions together with the incremental synchronization and error-handling features of image-syncer to ensure timely and consistent image synchronization. |
| 359 | + |
| 360 | +We also discussed security best practices, performance optimization, troubleshooting, and other related topics to help users better manage and maintain this synchronization mechanism. We hope this article provides valuable reference material for developers who need to deploy Docker images in Azure environments. |
| 361 | + |
| 362 | +## References |
| 363 | +- [HagiCode project GitHub repository](https://github.com/HagiCode-org/site) |
| 364 | +- [image-syncer official documentation](https://github.com/AliyunContainerService/image-syncer) |
| 365 | +- [Azure Container Registry official documentation](https://learn.microsoft.com/zh-cn/azure/container-registry/) |
| 366 | +- [GitHub Actions official documentation](https://docs.github.com/zh-cn/actions) |
| 367 | + |
| 368 | +--- |
| 369 | + |
| 370 | +## Reader Interaction |
| 371 | + |
| 372 | +Thank you for reading. If you found this article useful, please click the like button below 👍 so more people can discover it. |
| 373 | + |
| 374 | +## AI Assistance Disclosure |
| 375 | + |
| 376 | +This content was created with AI-assisted collaboration, reviewed by me, and reflects my own views and position. |
| 377 | + |
| 378 | +## Metadata |
| 379 | + |
| 380 | +- **Author:** [newbe36524](https://www.newbe.pro) |
| 381 | +- **Article URL:** [https://hagicode.com/blog/2026/01/25/how-to-sync-docker-hub-to-azure-acr-with-github](https://hagicode.com/blog/2026/01/25/how-to-sync-docker-hub-to-azure-acr-with-github) |
| 382 | +- **Copyright:** Unless otherwise stated, all articles on this blog are licensed under BY-NC-SA. Please include attribution when reprinting. |
0 commit comments