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
- Docker Artifacts: Why Files Created in Containers Disappear
- Volume Bind Mount Fix:
-v $(pwd):/app - Root Path Bind Mount Error
- GitLab Registry Login and Image Push
- Docker-in-Docker (dind) Service Requirement
- SSH Deployment to Production
- 5 Machines, 2-Hour Timeout
- 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):/appmounts the current directory- The container writes to
/app/vulnerabilities.json - The runner sees
vulnerabilities.jsonin 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:
- SSH into the prod machine:
ssh root@prod-<machine-id> - View the key:
more /root/.ssh/id_rsa - Copy the entire key (including
BEGINandENDheaders) - In GitLab, go to Settings > CI/CD > Variables and add:
PROD_SSH_PRIVKEY: the private key contentPROD_USERNAME:rootPROD_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 loginwith predefined GitLab variables inbefore_script. services: - docker:dindis 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.