Home Technical Support GitLab CI/CD: Artifacts in Docker, Continuous Deployment, and Registry Integration

GitLab CI/CD: Artifacts in Docker, Continuous Deployment, and Registry Integration

Last updated on Sep 08, 2026

GitLab CI/CD: Artifacts in Docker, Continuous Deployment, and Registry Integration

The Working With Artifacts In GitLab CI/CD Pipeline and Continuous Deployment using GitLab exercises walk through generating artifacts in Docker containers, pushing images to GitLab Registry, and deploying to a production server via SSH. These exercises appear across 7 courses. This article covers the most common failure modes and how to fix them.


Table of Contents

  1. Docker Artifacts: Why Files Created in Containers Disappear
  2. Volume Bind Mount Fix: -v $(pwd):/app
  3. Root Path Bind Mount Error
  4. GitLab Registry Login and Image Push
  5. Docker-in-Docker (dind) Service Requirement
  6. SSH Deployment to Production
  7. 5 Machines, 2-Hour Timeout
  8. Common Questions

Docker Artifacts: Why Files Created in Containers Disappear

Files Created Inside a Container Are Not on the Runner

A frequent point of confusion: if your CI job runs docker run alpine sh -c "echo 'result' > output.json" and then tries to save output.json as an artifact, the pipeline will fail:

cannot access 'output.json': No such file or directory

This happens because the file was created inside the container, not on the CI runner. When the container exits, the file is gone.

Fix: Use a Docker volume bind mount to share the runner's filesystem with the container:

docker run -i -v $(pwd):/app alpine sh -c "echo '{\"result\":\"XSS\"}' > /app/output.json"

This maps the runner's current directory to /app inside the container. Files written to /app are visible on the runner after the container exits.


Volume Bind Mount Fix: -v $(pwd):/app

Full Working Pipeline with Docker Artifacts

test:
  stage: test
  script:
    - echo "This is a test step."
    - docker run -i -v $(pwd):/app alpine sh -c "echo '{\"vulnerability\":\"XSS Injection\"}' > /app/vulnerabilities.json"
  artifacts:
    paths: [vulnerabilities.json]
  allow_failure: true

Key points:

  • -v $(pwd):/app mounts the current directory
  • The container writes to /app/vulnerabilities.json
  • The runner sees vulnerabilities.json in the working directory
  • artifacts: paths: picks it up after the job

Root Path Bind Mount Error

Can't Bind Mount to /

Docker rejects bind mounts to the root path:

invalid mount config for type "bind": invalid specification: destination can't be '/'

Fix: Always bind to a subdirectory like /app, /src, or /workspace. Never use / as the destination:

# WRONG:
docker run -v $(pwd):/ alpine ...

# CORRECT:
docker run -v $(pwd):/app alpine ...

GitLab Registry Login and Image Push

denied: access forbidden on Push

When the release job tries to push a Docker image to GitLab Container Registry without authenticating first, you'll see:

denied: access forbidden by registry

Fix: Add a before_script that logs in using GitLab predefined variables:

release:
  stage: release
  before_script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE .
    - docker push $CI_REGISTRY_IMAGE

Predefined Variables

GitLab provides these variables automatically:

Variable Description
$CI_REGISTRY Registry URL (e.g., gitlab-ce-123.example.com)
$CI_REGISTRY_USER Registry username (root)
$CI_REGISTRY_PASSWORD Registry password (from GitLab admin page)
$CI_REGISTRY_IMAGE Full image path ($CI_REGISTRY/root/project:tag)

Verify the Image Was Pushed

After a successful push, navigate to your GitLab project and click Package Registry or go to /root/django-nv/container_registry to see the pushed image.


Docker-in-Docker (dind) Service Requirement

Cannot connect to the Docker daemon

If your GitLab CI job needs to run Docker commands (docker build, docker push, etc.), you must declare the Docker service:

services:
  - docker:dind

Without this, all Docker commands fail. Add it at the job level or in a default: block to apply globally.


SSH Deployment to Production

Setting Up SSH Key for CI/CD Deployment

The continuous deployment exercise requires SSH from the CI runner to the production server. You need to store the SSH private key as a GitLab CI/CD variable:

  1. SSH into the prod machine: ssh root@prod-<machine-id>
  2. View the key: more /root/.ssh/id_rsa
  3. Copy the entire key (including BEGIN and END headers)
  4. In GitLab, go to Settings > CI/CD > Variables and add:
    • PROD_SSH_PRIVKEY: the private key content
    • PROD_USERNAME: root
    • PROD_HOSTNAME: prod-<machine-id>

Security warning: Storing SSH keys in GitLab CI variables is convenient but risky. In production, use HashiCorp Vault or similar secrets management.

Production Deployment Job

prod:
  stage: prod
  image: kroniak/ssh-client:3.6
  environment: production
  only:
    - main
  before_script:
    - mkdir -p ~/.ssh
    - echo "$PROD_SSH_PRIVKEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - eval "$(ssh-agent -s)"
    - ssh-add ~/.ssh/id_rsa
    - ssh-keyscan -H $PROD_HOSTNAME >> ~/.ssh/known_hosts
  script:
    - |
        ssh $PROD_USERNAME@$PROD_HOSTNAME << EOF
          docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
          docker rm -f django.nv
          docker run -d --name django.nv -p 8000:8000 $CI_REGISTRY_IMAGE
        EOF

Verify Deployment

After the pipeline completes, navigate to https://prod-<machine-id>.lab.practical-devsecops.training to see the deployed Task Manager application. Login with admin / admin.


5 Machines, 2-Hour Timeout

The continuous deployment exercise uses 5 machines: DevSecOps Box, GitLab CE, GitLab Runner, GitLab Registry, and Production. All 5 reset independently after 2 hours.

  • GitLab takes 3-5 minutes to start. A 502 error means it's still booting.
  • 404 errors after waiting mean the machine was recycled.
  • All work is lost when machines reset — including committed code, Docker images in the registry, and deployed applications.

Fix: Refresh the exercise page and click Start the Exercise again.


Common Questions

Question Answer
My artifact isn't generated even though the script creates the file. If the file is created inside a Docker container, use -v $(pwd):/app to bind mount the runner's filesystem.
docker push fails with "access forbidden." Add a before_script with docker login using $CI_REGISTRY_PASSWORD, $CI_REGISTRY_USER, and $CI_REGISTRY.
Docker commands fail in GitLab CI with "Cannot connect to Docker daemon." Add services: - docker:dind to your .gitlab-ci.yml.
I can't find $CI_REGISTRY_PASSWORD in GitLab variables. It's a predefined variable — no need to create it manually. It's auto-populated by GitLab.
The prod machine SSH connection is refused. The SSHD service may not be running yet. Wait 30 seconds and retry. If the 2-hour timeout hit, restart the exercise.
docker run fails with "destination can't be '/'." Bind mount to a subdirectory like /app or /src, never to root /.

Wrap-Up

Artifact management and continuous deployment in GitLab CI/CD involve Docker volume mounts, registry authentication, and SSH deployment. Key takeaways:

  • Docker volume binds (-v $(pwd):/app) are required to persist files from containers to the runner.
  • Never bind to / — use a subdirectory.
  • Registry login requires docker login with predefined GitLab variables in before_script.
  • services: - docker:dind is required for any Docker commands in CI.
  • 5 machines are involved in the continuous deployment exercise, all resetting after 2 hours.
  • The DevSecOps Box is stateless — it resets on any page refresh or tab close.

Reach out to support with your lab ID if you encounter errors not covered in this guide.