Preparing Infrastructure for Docker and GitLab CI/CD


Hi everyone,

in my last post, I introduced how I automated my blog publishing process using Docker and GitLab CI/CD. Before diving into the inner workings of the pipeline itself, I want to take a step back and share the foundational setup that made this automation possible. This post covers how I configured my environment to seamlessly handle Docker image builds, distribution, and deployment across machines—even behind firewalls.

Self-Hosting the GitLab Instance

To keep things private and in full control, my source code and CI/CD pipelines are hosted on an internal GitLab instance. This server not only hosts the repositories but also performs the Docker image builds via runners. These images are then stored in GitLab’s built-in container registry.

Key components:

  • GitLab runners configured with Docker-in-Docker (DinD) for building and tagging images.
  • Letsencrypt certificate for internal TLS encryption.
  • Registry authentication set up to allow secure access from external systems like Portainer.

Portainer and Docker Swarm for Deployment

For managing the deployment of Docker images, I use Portainer on top of a Docker Swarm cluster. This makes scaling and service management easier and more visual.

Why Portainer and Swarm?

  • Portainer provides a lightweight UI to monitor and manage Docker services.
  • Docker Swarm simplifies orchestration and automatically handles rolling updates when a new image is pushed.

Each deployment node in the Swarm is configured to:

  • Pull the latest Docker image from the GitLab container registry.
  • Automatically update the running service when a new tag is available.

Secure Networking with Tailscale + Headscale

One challenge with this setup: my Portainer node and GitLab instance are on different networks, and I didn’t want to expose the GitLab server publicly. To bridge that securely, I’m using Tailscale, with Headscale as the self-hosted coordination server.

How it fits:

  • Tailscale creates a private mesh network between nodes.
  • Headscale allows me to manage keys and access policies without relying on Tailscale's cloud service.
  • The Portainer server joins the Tailscale network, enabling it to securely pull images from the internal GitLab registry over a private channel.

This ensures my GitLab instance stays private, yet remains accessible to the systems that need to interact with it - without port forwarding, reverse proxies, or VPN headaches.

What’s Next?

Now that the groundwork is in place, I’ll walk you through the actual GitLab CI/CD pipeline in the next post. This will include the .gitlab-ci.yml structure, pipeline stages, and how I automated the image deployment through tagging strategies.