If you work on an Apple Silicon MacBook — whether that is an M1, M2, M3, or M4 chip — and you want a fully self-hosted GitLab Community Edition instance running locally on your machine, this guide is for you. We will walk through a complete, production-grade deployment using Podman as the container runtime, self-signed TLS certificates, and named persistent volumes. Every design decision is explained so you understand not just what to run, but why.
What Is GitLab and Why Run It Locally?
GitLab is an open-core, end-to-end DevSecOps platform that consolidates source code management, CI/CD pipelines, a built-in container registry, dependency and secret scanning, issue tracking, merge-request-based code review, and project wikis — all within a single, self-contained application. Unlike fragmented toolchains that stitch together separate systems, GitLab’s Omnibus package ships everything pre-integrated, making it one of the most operationally straightforward platforms to self-host.
While GitLab’s SaaS offering at gitlab.com is widely used, its self-hosted story is arguably the strongest in the industry. The Community Edition (CE) is fully open-source and includes the complete feature set needed for most engineering teams.
Use Cases for a Local GitLab Instance on Your MacBook
Running GitLab locally on your Apple Silicon MacBook is not just a learning exercise — it is a legitimate engineering workflow for the following scenarios:
- Air-gapped and offline development: Work on sensitive or compliance-bound codebases entirely offline, without any data leaving your machine or touching a SaaS provider’s infrastructure.
- CI/CD pipeline development and testing: Iterate rapidly on
.gitlab-ci.ymlpipeline definitions without consuming shared runner minutes, polluting team pipelines, or waiting for remote infrastructure to respond. - GitOps and Kubernetes workflow prototyping: Pair a local GitLab instance with a local Kubernetes cluster — such as minikube, kind, or Rancher Desktop — to develop and test full GitOps workflows using tools like Argo CD or Flux before promoting them to production clusters.
- Security policy research and validation: Evaluate SAST, DAST, dependency scanning, and secrets detection rules against your own repositories in a controlled environment before enforcing them organisation-wide.
- Webhook and integration development: Build and debug outbound webhooks, API integrations, and custom tooling against a real GitLab API without exposing a production instance through tunnels or public endpoints.
- Engineering onboarding and training: Provide new engineers with a disposable, consequence-free GitLab environment that faithfully mirrors the production platform, complete with real repositories and pipelines.
- Disaster recovery validation: Restore GitLab backup archives locally to verify the integrity of your DR runbooks without any risk to live data or production availability.
The approach in this guide follows the official Install GitLab by using Docker Engine documentation, adapted to use Podman as the container runtime on an ARM-based macOS host.
Why Podman Instead of Docker on Apple Silicon?
Docker is the widely recognised default for container workflows, and the official GitLab documentation references it throughout. However, on an Apple Silicon MacBook there are well-founded technical and operational reasons to choose Podman instead — particularly for a long-running, privileged service like a self-hosted GitLab instance.
- Daemonless Architecture Eliminates a Single Point of Failure
Docker requires a persistent background daemon (dockerd) running as root at all times. Podman is daemonless: each container is spawned as a direct child process of the invoking user, with no centralised runtime process in between. This means a failing container cannot cascade into a runtime-wide outage, the attack surface is meaningfully smaller, and there is no daemon to restart when something goes wrong. - Explicit Privilege Model — Rootless by Default, Rootful When Required
Podman supports both rootless and rootful container modes as first-class, clearly documented options. For this GitLab deployment, we will use rootful mode because GitLab’s embedded Nginx must bind to privileged ports 80 and 443. This is an explicit, auditable configuration decision — not an implicit behaviour baked into a background daemon process. - Native Apple Hypervisor Performance via
applehv
On Apple Silicon (M1/M2/M3/M4), Podman Machine can leverage Apple’s nativeVirtualization.framework— referred to as theapplehvprovider — rather than a third-party hypervisor such as QEMU. The Apple Hypervisor runs with hardware-assisted virtualisation built directly into the Apple SoC, delivering near-bare-metal VM performance. For a workload like GitLab’s Omnibus stack, which simultaneously drives PostgreSQL, Gitaly, Redis, Puma, Sidekiq, and Nginx, this I/O performance advantage is significant and directly felt in pipeline execution latency. - Full OCI Compliance — Same Images, Zero Changes
Podman is a fully OCI (Open Container Initiative) compliant runtime. It consumes the same container images from Docker Hub without any modification. The GitLab CE image used in this guide runs identically under Podman as it would under Docker — there are no image patches, shim layers, or compatibility workarounds required. - Open Source with No Licensing Constraints
Docker Desktop on macOS requires a paid subscription for use in professional or enterprise environments. Podman is fully open source under the Apache 2.0 licence, with no usage tiers, subscription requirements, or feature gating. For SREs building internal tooling, running personal labs, or working in organisations sensitive to software licensing costs, this is a meaningful operational advantage. - Production-Aligned Operational Patterns
Podman’s architecture and CLI are deliberately aligned with how containers are managed on Linux production servers, particularly in systemd-based environments. The operational patterns you establish running GitLab with Podman locally — volume management, machine lifecycle, port publishing — map directly to how Podman is used in production Linux deployments, making this local setup genuinely instructive beyond the laptop.
Summary: Podman provides a daemonless, OCI-compliant, Apple-native container runtime with a principled privilege model, no licensing friction, and demonstrably better performance on Apple Silicon hardware. For a self-hosted GitLab instance that needs to be stable, secure, and long-lived on a MacBook, it is the right choice.
Prerequisites
Before beginning, confirm the following:
- Apple Silicon MacBook — Any Mac with an M1, M2, M3, or M4 chip running macOS Ventura 13 or later. This guide is written and validated specifically for the ARM64 architecture.
- At least 8 GB of system RAM — We will allocate 5 GB to the Podman VM. A Mac with 8 GB total is workable; 16 GB or more is recommended for a comfortable experience alongside other applications.
- At least 20 GB of free disk space — GitLab’s container image is approximately 3 GB; the PostgreSQL data, Git repositories, and logs will grow beyond that over time.
- Administrative (sudo) access — Required for Homebrew installation, modifying
/etc/hosts, and inspecting system ports. - A terminal application — The built-in Terminal.app or a third-party alternative such as iTerm2.
Step 1 — Install Homebrew
Homebrew is the standard package manager for macOS, widely adopted across the industry and self-described as “The Package Manager for Everywhere.” It provides a consistent, dependency-aware mechanism for installing, updating, and managing thousands of open-source tools — including Podman.
If Homebrew is not already installed on your MacBook, run the official installation script from your terminal:
❯ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"─╯==> Checking for `sudo` access (which may request your password)...Password:==> This script will install:/opt/homebrew/bin/brew/opt/homebrew/share/doc/homebrew/opt/homebrew/share/man/man1/brew.1/opt/homebrew/share/zsh/site-functions/_brew/opt/homebrew/etc/bash_completion.d/brew/opt/homebrew/etc/paths.d/homebrewPress RETURN/ENTER to continue or any other key to abort:==> /usr/bin/sudo /usr/sbin/chown -R ayush:admin /opt/homebrew==> Downloading and installing Homebrew...remote: Enumerating objects: 606, done.remote: Counting objects: 100% (410/410), done.remote: Compressing objects: 100% (58/58), done.remote: Total 606 (delta 373), reused 354 (delta 352), pack-reused 196 (from 2)==> /usr/bin/sudo /bin/mkdir -p /etc/paths.d==> /usr/bin/sudo tee /etc/paths.d/homebrew/opt/homebrew/bin==> /usr/bin/sudo /usr/sbin/chown root:wheel /etc/paths.d/homebrew==> /usr/bin/sudo /bin/chmod a+r /etc/paths.d/homebrew==> Updating Homebrew...Updated 1 tap (homebrew/cask).==> Installation successful!==> Homebrew has enabled anonymous aggregate formulae and cask analytics.Read the analytics documentation (and how to opt-out) here: https://docs.brew.sh/AnalyticsNo analytics data has been sent yet (nor will any be during this install run).==> Homebrew is run entirely by unpaid volunteers. Please consider donating: https://github.com/Homebrew/brew#donations==> Next steps:- Run brew help to get started- Further documentation: https://docs.brew.sh
On Apple Silicon, Homebrew installs to /opt/homebrew (rather than /usr/local used on Intel Macs). The installer will prompt for your macOS password, install Xcode Command Line Tools if they are absent, and print post-installation instructions for adding Homebrew to your shell’s PATH. Follow those instructions — they typically involve adding one line to ~/.zprofile or ~/.zshrc and sourcing it.
Verify the installation before proceeding:
❯ brew --version─╯Homebrew 6.0.6
Step 2 — Install Podman
With Homebrew available, installing Podman is a single command:
❯ brew install podman─╯✔︎ JSON API packages.arm64_tahoe.jws.json Downloaded 15.2MB/ 15.2MBWarning: podman 6.0.0 is already installed and up-to-date.To reinstall 6.0.0, run: brew reinstall podman❯ podman --version─╯podman version 6.0.0
Step 3 — Create and Start a Podman Machine
Why Podman Requires a Virtual Machine on macOS
Containers are a Linux-native technology — they rely on Linux kernel primitives including namespaces for process isolation and cgroups for resource control. macOS does not expose these kernel interfaces natively. To run containers on macOS, Podman manages a lightweight Linux virtual machine called a Podman Machine, which serves as the actual container host. All container operations are transparently proxied from your Mac through to this VM.
This is architecturally equivalent to what Docker Desktop does internally. The difference is that Podman exposes this layer explicitly, giving you direct control over the VM’s allocated resources, the hypervisor backend, and whether the runtime operates in rootless or rootful mode. You are not running containers directly on macOS — you are running them inside a managed Linux VM that Podman provisions and maintains on your behalf.
Explore Available Machine Subcommands
Before initialising a machine, it is worth reviewing the full set of available subcommands:
❯ podman machine -h─╯Manage a virtual machineDescription: Manage a virtual machine. Virtual machines are used to run Podman.Usage: podman machine [command]Available Commands: cp Securely copy contents between the virtual machine info Display machine host info init Initialize a virtual machine inspect Inspect an existing machine list List machines os Manage a Podman virtual machine's OS reset Remove all machines rm Remove an existing machine set Set a virtual machine setting ssh SSH into an existing machine start Start an existing machine stop Stop an existing machine
This outputs the complete reference for Podman Machine management, including init, start, stop, rm, inspect, list, and ssh. Familiarising yourself with these commands will serve you well when managing the machine’s lifecycle going forward.
List Existing Machines
Confirm there are no pre-existing machines before creating one:
❯ podman machine list─╯NAME VM TYPE CREATED LAST UP CPUS MEMORY DISK SIZE
On a fresh Podman installation, this will display an empty table.
Initialise the Podman Machine
❯ podman machine init --provider applehv --memory 5120 --rootful=true─╯Looking up Podman Machine image at quay.io/podman/machine-os:6.0 to create VMGetting image source signaturesCopying blob f8214f15a24a done | Copying config 44136fa355 done | Writing manifest to image destinationf8214f15a24a774533d7534ec856c078aa9a6fe355681e779091dd9304a8a140Extracting compressed file: podman-machine-default-arm64.raw: done Machine init completeTo start your machine run: podman machine start
Each flag serves a specific and deliberate purpose:
--provider applehv— Instructs Podman to use Apple’s nativeVirtualization.frameworkas the hypervisor backend, commonly referred to asapplehv(Apple Hypervisor). On Apple Silicon Macs, this provider uses hardware-assisted virtualisation that is deeply integrated into the ARM SoC, resulting in near-native I/O throughput and significantly lower CPU overhead compared to the QEMU software emulator. For a workload as I/O-intensive as GitLab’s Omnibus stack — which concurrently runs PostgreSQL, Gitaly, Redis, Puma workers, and Sidekiq — choosingapplehvis not merely a preference; it is the correct engineering decision for acceptable performance on an ARM MacBook.--memory 5120— Allocates 5,120 MB (5 GB) of RAM to the Linux VM. GitLab’s Omnibus package bundles an entire production stack inside a single container: a full PostgreSQL database, the Gitaly Git server, a Redis cache, multiple Puma application worker processes, Sidekiq background job processors, and an Nginx reverse proxy. The GitLab documentation states a hard minimum of 4 GB; 5 GB provides a practical operational buffer and noticeably reduces the likelihood of OOM conditions during peak pipeline activity.--rootful=true— Configures the Podman Machine so that containers run as the root user inside the Linux VM. This is a firm requirement for this deployment because GitLab’s embedded Nginx must bind to privileged ports 80 (HTTP) and 443 (HTTPS). On Linux, binding to any port below 1024 requires root privileges. Without rootful mode, the container will fail to acquire these port bindings and GitLab’s web interface will be unreachable. This configuration mirrors the privilege model used in GitLab’s own official Docker deployment documentation.
Start the Machine
❯ podman machine start─╯Starting machine "podman-machine-default"API forwarding listening on: /var/folders/s_/6y64fq817cx15w0ddwgwxr740000gn/T/podman/podman-machine-default-api.sockThe system helper service is not installed; the default Docker API socketaddress can't be used by podman. If you would like to install it, run the following commands: sudo /opt/homebrew/Cellar/podman/6.0.0/bin/podman-mac-helper install podman machine stop; podman machine startYou can still connect Docker API clients by setting DOCKER_HOST using thefollowing command in your terminal session: export DOCKER_HOST='unix:///var/folders/s_/6y64fq817cx15w0ddwgwxr740000gn/T/podman/podman-machine-default-api.sock'Machine "podman-machine-default" started successfully
Confirm the Machine Is Running
❯ podman machine list─╯NAME VM TYPE CREATED LAST UP CPUS MEMORY DISK SIZEpodman-machine-default applehv 3 minutes ago Currently running 5 5GiB 100GiB
The output should show the machine in a Currently running state along with its CPU allocation, memory, disk size, and the local socket path through which the Podman CLI communicates with it. All subsequent podman commands will be transparently forwarded to this VM.
Step 4 — Create Persistent Volumes
GitLab’s Omnibus stack writes to three logically distinct data paths. We will create a dedicated named volume for each one. Named volumes are managed by Podman directly within the VM’s filesystem, offering cleaner lifecycle management than bind-mounting host directories and ensuring data persists independently of the container’s lifecycle.
❯ podman volume create gitlab-config─╯gitlab-config❯ podman volume create gitlab-logs─╯gitlab-logs❯ podman volume create gitlab-data─╯gitlab-data
| Volume Name | Container Mount Point | Contents |
| gitlab-config | /etc/gitlab | Omnibus configuration (gitlab.rb), application secrets, and SSL certificate references |
| gitlab-logs | /var/log/gitlab | Structured logs from Nginx, Puma, Sidekiq, PostgreSQL, Gitaly, and other internal services |
| gitlab-data | /var/opt/gitlab | PostgreSQL database files, bare Git repositories, uploaded attachments, and the container registry |
Because these volumes exist independently of the container, future upgrades are straightforward: stop the running GitLab container, pull the new image version, and re-run the podman run command referencing the same volume names. All configuration, repository data, and application state is preserved across the upgrade.
Step 5 — Generate Self-Signed TLS Certificates
GitLab serves all web traffic over HTTPS. For a locally deployed instance, we will use a self-signed TLS certificate rather than Let’s Encrypt, which requires a publicly reachable domain and active ACME challenge validation — neither of which applies in a local lab context.
We generate the certificate into a dedicated local directory, which will be bind-mounted directly into the container:
❯ mkdir gitlab-ssl❯ openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \─╯ -keyout ./gitlab-ssl/gitlab.local.key \ -out ./gitlab-ssl/gitlab.local.crt \ -subj "/CN=gitlab.local"...+..........+..+.......+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*....+....+......+...+...........+....+..+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+............+..+...+.......+...+.....+.......+.....+..................+.......+........+......+.+.....+.+.....+.............+......+.....+....+.....+....+...+........+....+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++....+....+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.......+..+.........+.......+.....+...+......+...+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*..........+......+...+.+.....+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Flag Reference
-x509— Produce a self-signed certificate directly, bypassing the certificate signing request (CSR) workflow.-nodes— Generate the private key without passphrase encryption. GitLab’s embedded Nginx reads the key on startup and cannot interactively prompt for a passphrase in a containerised environment.-days 3650— Set a validity period of ten years. Appropriate for a long-lived local lab; in a production environment, use short-lived certificates with automated renewal.-newkey rsa:2048— Generate a new 2048-bit RSA key pair alongside the certificate.-subj "/CN=gitlab.local"— Set the certificate’s Common Name (CN) field togitlab.local, which is the hostname through which we will access this GitLab instance.
Critical: The Hostname Must Resolve to Your Machine’s IP Address
The hostname specified in the
CNfield —gitlab.localin this guide — must resolve to the IP address of the machine hosting the container. If it does not, your browser and Git client will be unable to reach GitLab, and TLS verification will fail for any client that validates the certificate’s CN against the hostname it connected to.
You have two options for establishing this name resolution:
- Internal DNS record — If you manage a local DNS server (such as Pi-hole, pfSense, or Unbound), add an A record for
gitlab.localpointing to the private LAN IP address of your MacBook. This makes the hostname resolvable from any device on your network and is the preferred approach for a shared lab environment. /etc/hostsentry — For a quick, single-machine override, add the mapping directly to your Mac’s/etc/hostsfile. This approach is local to your Mac only. We will use this method in Step 8.
If your chosen hostname differs from gitlab.local — for example, git.internal.company.dev — replace every occurrence of gitlab.local throughout this guide with your chosen hostname, including in the -subj field, the GITLAB_OMNIBUS_CONFIG environment variable, and the /etc/hosts entry.
To suppress browser TLS warnings for the self-signed certificate, import gitlab-ssl/gitlab.local.crt into your macOS Keychain and mark it as “Always Trust.” For a private lab environment, accepting the browser warning or using curl -k is an equally valid approach.
Step 6 — Run the GitLab Container
Selecting the Correct GitLab Image
GitLab publishes container images for two editions on Docker Hub:
- Enterprise Edition (EE):
gitlab/gitlab-ee:<version>-ee.0 - Community Edition (CE):
gitlab/gitlab-ce:<version>-ce.0
This guide uses the Community Edition, which is fully open source and includes the complete GitLab feature set — source code management, CI/CD pipelines, container registry, merge requests, and issue tracking — without requiring a commercial licence. For a local lab or personal infrastructure setup, CE is the appropriate and sufficient choice.
Available CE image tags can be browsed via the official GitLab documentation: Find the GitLab version and edition to use. We will pin to a specific, known-good release — gitlab/gitlab-ce:18.8.11-ce.0 — rather than using the :latest tag, which can introduce uncontrolled upgrades between container restarts.
Launch the Container
❯ podman run --detach \─╯ --hostname gitlab.local \ --env GITLAB_OMNIBUS_CONFIG=" \ external_url 'https://gitlab.local'; \ letsencrypt['enable'] = false; \ nginx['ssl_certificate'] = '/etc/gitlab/ssl/gitlab.local.crt'; \ nginx['ssl_certificate_key'] = '/etc/gitlab/ssl/gitlab.local.key'; \ gitlab_rails['gitlab_shell_ssh_port'] = 2222; \ " \ --publish 0.0.0.0:443:443 \ --publish 0.0.0.0:80:80 \ --publish 0.0.0.0:2222:22 \ --name gitlab \ --restart always \ --volume gitlab-config:/etc/gitlab \ --volume gitlab-logs:/var/log/gitlab \ --volume gitlab-data:/var/opt/gitlab \ --volume ./gitlab-ssl:/etc/gitlab/ssl \ --shm-size 256m \ gitlab/gitlab-ce:18.8.11-ce.0Resolving "gitlab/gitlab-ce" using unqualified-search registries (/usr/share/containers/registries.conf.d/999-podman-machine.conf)Trying to pull docker.io/gitlab/gitlab-ce:18.8.11-ce.0...Getting image source signaturesCopying blob sha256:8836b74eb9080b2015916e2080f932936adde97dcfcf99b38630b322a3bd1e5fCopying blob sha256:10099e45057d21c1274cee2e8a483b8f769bd5967fa177cbd420fc7c6dfc340eCopying blob sha256:fff3795b437199a0b714aadba6fb2c251d7da853c3e257d3fed1d2c8d0f05158Copying blob sha256:bd6839b2f7f78c6b2714292e48aa9a0520e7a5fd850717bd5e608b42793fcb85Copying blob sha256:58617a37047efe05c7f9b0ac6657e78e22f7d5369ed2401086778d0800fa47e8Copying blob sha256:8c4ac8798565c02eeaf42c2fa65354fe451b75650f52db3849fa1a62ca140841Copying blob sha256:bb5dd042c2130ebe84023523fd417937e326a80a382968d8be84bec0b2e2f3dbCopying blob sha256:f59c6cfb880aab68ce6db06c196fa2fddfecd4230581012a375c298bb3c1877fCopying blob sha256:9ce41ef7d28fffbb608b95c21ee381a5bd5e9fd60e0dba96ae9b73c1c9da4437Copying config sha256:ce75ccefc6d793bafcbf451cf3e6c58b21f52b8613a06384d0cd24bb3f8d014bWriting manifest to image destinationa4b2640465fabb89cb811a6d1a2532078411f7f050c6a7aec29cd93b88b277e1
Command Breakdown
| Flag / Option | Explanation |
| –detach | Starts the container in the background and immediately returns the container ID. Your terminal session remains available for subsequent commands. |
| –hostname gitlab.local | Sets the container’s internal hostname to gitlab.local. GitLab’s Omnibus initialisation process uses this value when constructing internal service URLs and configuring inter-process communication. |
| –env GITLAB_OMNIBUS_CONFIG | Injects Ruby configuration directly into /etc/gitlab/gitlab.rb at container startup. This is the recommended method for configuring GitLab Omnibus in a containerised environment, as it keeps configuration declarative and avoids manual edits inside a running container. |
| external_url https://gitlab.local | Defines the canonical public URL for this GitLab instance. This value is used to generate every link, repository clone URL, and webhook callback URL that GitLab emits — setting it incorrectly results in broken links throughout the interface. |
| letsencrypt[‘enable’] = false | Disables GitLab’s built-in Let’s Encrypt integration. Since we are supplying our own self-signed certificate, Omnibus must not attempt ACME HTTP-01 challenge validation, which would fail against a non-public local hostname. |
nginx['ssl_certificate'] and nginx['ssl_certificate_key'] | Points GitLab’s embedded Nginx to the self-signed certificate and private key generated in Step 5. These paths reference the container-internal location where we mount the gitlab-ssl directory. |
| gitlab_rails[‘gitlab_shell_ssh_port’] = 2222 | Instructs GitLab to advertise SSH clone URLs using port 2222 rather than the standard port 22. This is necessary because we remap the container’s SSH port 22 to host port 2222, avoiding a conflict with macOS’s own SSH daemon. |
| –publish 0.0.0.0:443:443 | Publishes the container’s HTTPS port 443 on all network interfaces of the host, making GitLab reachable over TLS from both localhost and the Mac’s LAN IP address. |
| –publish 0.0.0.0:80:80 | Publishes the container’s HTTP port 80. GitLab’s Nginx will issue a 301 redirect from HTTP to HTTPS for all requests. |
| –publish 0.0.0.0:2222:22 | Maps the container’s SSH port 22 to host port 2222. SSH-based Git operations will use the remote URL format ssh://git@gitlab.local:2222/<namespace>/<repo>.git. |
| –name gitlab | Assigns the container a stable name, allowing you to reference it consistently in subsequent podman logs, podman exec, podman stop, and podman start commands. |
| –restart always | Configures Podman to automatically restart this container if it exits unexpectedly or if the Podman Machine is rebooted, ensuring GitLab remains available as a persistent local service. |
| –volume gitlab-config:/etc/gitlab | Mounts the gitlab-config named volume to persist all Omnibus configuration files, including gitlab.rb and generated application secrets. |
| –volume gitlab-logs:/var/log/gitlab | Mounts the gitlab-logs named volume to retain log output from all GitLab services across container restarts. |
| –volume gitlab-data:/var/opt/gitlab | Mounts the gitlab-data named volume for all application data: PostgreSQL database files, bare Git repository storage, user uploads, and registry blobs. |
| –volume ./gitlab-ssl:/etc/gitlab/ssl | Bind-mounts the local gitlab-ssl directory into the container, making the self-signed certificate and private key available to Nginx at the path configured in GITLAB_OMNIBUS_CONFIG. This command must be run from the directory containing the gitlab-ssl folder. |
| –shm-size 256m | Increases the container’s shared memory allocation from the default 64 MB to 256 MB. GitLab’s Prometheus metrics scraping and PostgreSQL’s shared buffer management can exhaust the smaller default, resulting in subtle and difficult-to-diagnose process failures. |
Step 7 — Monitor Startup Logs
On its first start, GitLab Omnibus executes a full gitlab-ctl reconfigure pass: it initialises the PostgreSQL database schema, generates cryptographic secrets, configures all internal services, and starts the Nginx reverse proxy. This initial configuration process typically takes between three and seven minutes, depending on the performance of the Podman Machine VM and your MacBook’s SSD.
Stream the container logs to observe progress:
❯ podman logs gitlab --follow
Press Ctrl+C to detach from the log stream. The container continues running in the background; the log-following session is purely observational.
You will see a stream of Chef-style resource provisioning output as Omnibus configures each component in sequence. The instance is fully initialised and ready to serve traffic when you observe the following line in the log output:
❯ podman logs gitlab | grep -i "gitlab Reconfigured"─╯cat: /var/opt/gitlab/gitlab-rails/VERSION: No such file or directory/opt/gitlab/embedded/bin/runsvdir-start: line 24: ulimit: pending signals: cannot modify limit: Operation not permitted/opt/gitlab/embedded/bin/runsvdir-start: line 34: ulimit: max user processes: cannot modify limit: Operation not permitted/opt/gitlab/embedded/bin/runsvdir-start: line 37: /proc/sys/fs/file-max: Read-only file system/opt/gitlab/embedded/lib/ruby/gems/3.2.0/gems/ffi-yajl-2.6.0/lib/ffi_yajl/encoder.rb:42: warning: undefining the allocator of T_DATA class FFI_Yajl::Ext::Encoder::YajlGen/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:2: warning: already initialized constant SELinuxDistroHelper::REDHAT_RELEASE_FILE/opt/gitlab/embedded/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:2: warning: previous definition of REDHAT_RELEASE_FILE was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:3: warning: already initialized constant SELinuxDistroHelper::OS_RELEASE_FILE/opt/gitlab/embedded/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:3: warning: previous definition of OS_RELEASE_FILE was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:4: warning: already initialized constant SELinuxDistroHelper::MIN_SUPPORTED_RHEL_VERSION/opt/gitlab/embedded/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:4: warning: previous definition of MIN_SUPPORTED_RHEL_VERSION was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:5: warning: already initialized constant SELinuxDistroHelper::MAX_SUPPORTED_RHEL_VERSION/opt/gitlab/embedded/cookbooks/package/libraries/helpers/selinux_distro_helper.rb:5: warning: previous definition of MAX_SUPPORTED_RHEL_VERSION was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/helpers/secrets_helper.rb:4: warning: already initialized constant SecretsHelper::SECRETS_FILE/opt/gitlab/embedded/cookbooks/package/libraries/helpers/secrets_helper.rb:4: warning: previous definition of SECRETS_FILE was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/helpers/secrets_helper.rb:5: warning: already initialized constant SecretsHelper::SECRETS_FILE_CHEF_ATTR/opt/gitlab/embedded/cookbooks/package/libraries/helpers/secrets_helper.rb:5: warning: previous definition of SECRETS_FILE_CHEF_ATTR was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/helpers/secrets_helper.rb:6: warning: already initialized constant SecretsHelper::SKIP_GENERATE_SECRETS_CHEF_ATTR/opt/gitlab/embedded/cookbooks/package/libraries/helpers/secrets_helper.rb:6: warning: previous definition of SKIP_GENERATE_SECRETS_CHEF_ATTR was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/gitlab_cluster.rb:16: warning: already initialized constant GitlabCluster::CONFIG_PATH/opt/gitlab/embedded/cookbooks/package/libraries/gitlab_cluster.rb:16: warning: previous definition of CONFIG_PATH was here/opt/gitlab/embedded/cookbooks/cache/cookbooks/package/libraries/gitlab_cluster.rb:17: warning: already initialized constant GitlabCluster::JSON_FILE/opt/gitlab/embedded/cookbooks/package/libraries/gitlab_cluster.rb:17: warning: previous definition of JSON_FILE was heregitlab Reconfigured!
Step 8 — Add a Local DNS Entry
The hostname gitlab.local is not registered in any public DNS infrastructure. Before a browser or Git client on your Mac can route traffic to it, the operating system must know which IP address it corresponds to. The quickest approach for a single-machine setup is to add an entry to /etc/hosts.
First, determine your Mac’s current local network IP address:
❯ ipconfig getifaddr en0─╯192.168.0.24
Then append the mapping to /etc/hosts, replacing 192.168.1.x with the IP address returned above:
❯ sudo sh -c 'echo "192.168.0.24 gitlab.local" >> /etc/hosts'─╯Password:
Confirm name resolution is working:
❯ ping -c 1 gitlab.local─╯PING gitlab.local (192.168.0.24): 56 data bytes64 bytes from 192.168.0.24: icmp_seq=0 ttl=64 time=11.414 ms--- gitlab.local ping statistics ---1 packets transmitted, 1 packets received, 0.0% packet lossround-trip min/avg/max/stddev = 11.414/11.414/11.414/nan ms
Note: An
/etc/hostsentry is local to your MacBook only. If you want other devices on your local network to reach this GitLab instance by hostname, add an A record in your local DNS server (Pi-hole, pfSense, Unbound, etc.) instead, pointinggitlab.localat your Mac’s LAN IP address.
Step 9 — Understanding gvproxy and Fixing Network Access
The Symptom
After completing all the steps above, you may observe an initially confusing behaviour: GitLab responds correctly over localhost:
❯ curl -k https://localhost─╯<html><body>You are being <a href="https://localhost/users/sign_in">redirected</a>.</body></html>%
But the same request via the gitlab.local hostname — or the Mac’s LAN IP address — fails:
❯ curl -k https://gitlab.local/─╯curl: (35) Recv failure: Connection reset by peer
The Cause — gvproxy and the macOS Application Firewall
Inspecting which processes are bound to the published ports reveals the mechanism at work:
❯ sudo lsof -i :443 -i :80 -i :22 | grep LISTEN─╯gvproxy 96793 ayush 17u IPv6 0x396a9ab56a972bc7 0t0 TCP *:http (LISTEN)gvproxy 96793 ayush 18u IPv6 0x2ba1ed3d28cb3be1 0t0 TCP *:https (LISTEN)
The process holding these sockets is gvproxy — Podman’s user-space network proxy, implemented in Go. When a Podman Machine is running with published ports, gvproxy is responsible for forwarding traffic between the macOS host’s network interfaces and the Linux VM’s internal network, where the container actually listens. It is the bridge that makes --publish port mappings functional on macOS, where the VM sits on a virtual network that is not directly addressable from the outside.gvproxy binds on all interfaces (*) and is therefore technically reachable from the network — however, macOS’s built-in Application Firewall intercepts inbound connections before they reach the socket. Crucially, the firewall treats loopback connections (127.0.0.1 / localhost) as inherently local and permits them without consulting its rules, while connections arriving on your Wi-Fi or Ethernet interface are subject to the firewall’s application-level allow/deny policy. Because gvproxy has not been explicitly added to the firewall’s allow list, those external connections are silently dropped — explaining exactly why localhost succeeds while the hostname or LAN IP fails.
The Resolution — Allow gvproxy Through the macOS Firewall
The fix is to explicitly authorise gvproxy to accept incoming network connections in macOS System Settings:
- Open System Settings on your Mac.
- Navigate to Network → Firewall.
- Click Options….
- Locate
gvproxyin the application list. If it does not appear, click the + button and add it manually from/opt/homebrew/bin/gvproxyor the path shown inwhich gvproxy. - Set the connection permission to “Allow incoming connections”.
- Click OK and close System Settings.

With the firewall rule in place, retry the request using the hostname:
❯ curl -k https://gitlab.local/─╯<html><body>You are being <a href="https://gitlab.local/users/sign_in">redirected</a>.</body></html>%
GitLab is now fully accessible from the machine’s IP address and the configured hostname. Any other device on your local network that can resolve gitlab.local to your Mac’s IP (via a local DNS record) will also be able to reach the instance.
Step 10 — First Login and Initial Root Password
During first-time initialisation, GitLab generates a random password for the built-in root administrator account and writes it to a file inside the container at /etc/gitlab/initial_root_password. This file is automatically deleted 24 hours after the first successful gitlab-ctl reconfigure run, so retrieve the password before that window closes.
❯ podman exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password─╯Password: YfI3sMuG/I/mlMHKHGtSNpDBynQv5WRF0C+XLUxp7BE=
Open a browser and navigate to https://gitlab.local. If you have not added the self-signed certificate to your macOS Keychain, the browser will display a TLS warning — proceed past it for the initial setup, or add the certificate as a trusted root to eliminate the warning permanently.
Sign in with the following credentials:
- Username:
root - Password: the value retrieved from the command above
After your first successful login, immediately change the root password by navigating to User Settings → Password. The auto-generated password is a temporary bootstrap credential and should not be retained.


Quick Reference — Full Command Sequence
The following is a condensed reference of the entire setup sequence, suitable for revisiting or reproducing the deployment on a new machine:
# ── Step 1: Install Homebrew ────────────────────────────────────❯ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"# ── Step 2: Install Podman ──────────────────────────────────────❯ brew install podman# ── Step 3: Initialise and start the Podman Machine ─────────────❯ podman machine init --provider applehv --memory 5120 --rootful=true❯ podman machine start❯ podman machine list# ── Step 4: Create persistent named volumes ──────────────────────❯ podman volume create gitlab-config❯ podman volume create gitlab-logs❯ podman volume create gitlab-data# ── Step 5: Generate a self-signed TLS certificate ───────────────❯ mkdir gitlab-ssl❯ openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout ./gitlab-ssl/gitlab.local.key \ -out ./gitlab-ssl/gitlab.local.crt \ -subj "/CN=gitlab.local"# ── Step 6: Run the GitLab Community Edition container ───────────❯ podman run --detach \ --hostname gitlab.local \ --env GITLAB_OMNIBUS_CONFIG=" \ external_url 'https://gitlab.local'; \ letsencrypt['enable'] = false; \ nginx['ssl_certificate'] = '/etc/gitlab/ssl/gitlab.local.crt'; \ nginx['ssl_certificate_key'] = '/etc/gitlab/ssl/gitlab.local.key'; \ gitlab_rails['gitlab_shell_ssh_port'] = 2222; \ " \ --publish 0.0.0.0:443:443 \ --publish 0.0.0.0:80:80 \ --publish 0.0.0.0:2222:22 \ --name gitlab \ --restart always \ --volume gitlab-config:/etc/gitlab \ --volume gitlab-logs:/var/log/gitlab \ --volume gitlab-data:/var/opt/gitlab \ --volume ./gitlab-ssl:/etc/gitlab/ssl \ --shm-size 256m \ gitlab/gitlab-ce:18.8.11-ce.0# ── Step 7: Monitor first-boot initialisation ────────────────────❯ podman logs gitlab --follow# Wait for: "gitlab Reconfigured!" — then Ctrl+C to detach# ── Step 8: Add a local DNS entry ────────────────────────────────# Replace 192.168.1.x with your Mac's actual LAN IP (ipconfig getifaddr en0)❯ sudo sh -c 'echo "192.168.0.24 gitlab.local" >> /etc/hosts'# ── Step 9: Allow gvproxy through the macOS Firewall ─────────────# System Settings → Network → Firewall → Options# → Set gvproxy to "Allow incoming connections"# ── Step 10: Retrieve the initial root password ───────────────────❯ podman exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password# Then log in at https://gitlab.local with username: root
Closing Thoughts
You now have a fully operational, TLS-secured GitLab Community Edition instance running locally on your Apple Silicon MacBook, backed by persistent named volumes and served through Podman’s native Apple Hypervisor integration. The setup is stable across machine reboots, upgradeable without data loss, and aligned with the same operational patterns you would use in a production Linux environment.
The one non-obvious hurdle in this entire process is the gvproxy firewall step — it is easy to miss and produces a misleadingly inconsistent symptom (localhost works, hostname does not). If that caught you off guard, you are in good company; it is one of the more common points of confusion in Podman-on-macOS deployments.
From here, the natural next steps are creating your first project, registering a GitLab Runner for local CI/CD pipeline execution, and connecting GitLab to your local Kubernetes cluster if that is part of your workflow.
Have questions, corrections, or additions? Leave a comment below — feedback from other engineers running this on their Apple Silicon machines is always welcome and helps keep this guide accurate.
Leave a Reply