[{"data":1,"prerenderedAt":209},["ShallowReactive",2],{"seo-verification":3,"blog-install-gitlab-ce-on-a-vps-complete-guide-ma-en":6,"promo-bar-likely":208},{"google":4,"bing":5},"EycwPY2XMyTkVzas3n1ygeNJFGAH513qrMjfDljzsMQ","",{"key":7,"data":8},"blog-install-gitlab-ce-on-a-vps-complete-guide-ma-en",{"id":9,"slug":10,"slugs":11,"title":15,"excerpt":16,"readTime":17,"views":18,"isPinned":19,"noindex":19,"publishedAt":20,"updatedAt":21,"category":22,"categories":27,"featuredImage":29,"bgImage":30,"posterImage":31,"relatedSolution":29,"intro":32,"sections":33,"ctaTitle":151,"ctaBody":152,"ctaButton":153,"ctaUrl":154,"relatedPosts":155},235,"install-gitlab-ce-on-a-vps-complete-guide",{"fr":12,"en":10,"ar":13,"es":14},"installer-gitlab-ce-vps","تثبيت-gitlab-ce-على-vps-دليل-شامل","instalar-gitlab-ce-en-un-vps-guia-completa","Install GitLab CE on a VPS: complete guide","Deploy GitLab CE on your VPS with Docker Compose. Covers CVE-2026-90970 (CVSS 9.9): AI Gateway RCE, vulnerable versions and update commands.",18,7,false,"2026-08-07T00:00:00+00:00","2026-10-20T23:18:07+00:00",{"id":23,"name":24,"slug":25,"color":26,"icon":25},3,"Deployment","deploiement","bg-success\u002F10 text-success",[28],{"id":23,"name":24,"slug":25,"color":26,"icon":25},null,"\u002Fblog\u002Fcovers\u002Fbg.svg","\u002Fblog\u002Fcovers\u002Finstaller-gitlab-ce-vps-poster.svg","GitHub and Bitbucket host your repositories, but they set the rules: metered CI minutes, uncertain code ownership, pricing that rises with team size. GitLab CE (Community Edition, MIT\u002FEE Core licence) brings together on a single server a complete git forge, a CI\u002FCD engine with no imposed minute quotas, a private Docker registry and wikis — all without a SaaS subscription. This guide shows you how to install it in under thirty minutes on a VPS with Docker Compose, configure SMTP, connect a runner and avoid the classic pitfalls: a misconfigured GITLAB_OMNIBUS_CONFIG, SSL that expires for lack of ACME, an unregistered runner.",[34,38,50,53,84,87,90,93,96,99,102,105,107,110,148],{"type":35,"title":36,"body":37},"h2","Why GitLab CE instead of GitHub or Bitbucket","GitHub and Bitbucket are managed services: convenient to start with, but their pricing evolves with team size, CI minutes are capped on free plans, and your code lives on third-party infrastructure. GitLab CE reverses this equation: you deploy the forge on your VPS, you retain full control of the code and data, and CI\u002FCD is included without minute quotas imposed by a third party — only your machine's capacity limits the number of parallel jobs. For an agency or a development team that bills clients or handles sensitive data, this control is often non-negotiable. GitLab CE is today one of the most widely deployed self-hosted git forges: its codebase is public, its core is free, and ten years of existence have earned it an active contributor community.",{"type":39,"title":40,"items":41},"ul","What GitLab CE includes out of the box",[42,43,44,45,46,47,48,49],"**Complete git forge**: private and public repositories, merge requests, code review, branch protection and CODEOWNERS.","**Integrated CI\u002FCD** with no imposed minute limit: YAML pipelines (`.gitlab-ci.yml`), environments, deploy tokens and artifacts.","**Private Docker registry** hosted on your domain: `docker pull git.yourdomain.com\u002Fgroup\u002Fimage:tag` without a Docker Hub account.","**Wikis and pages** per project and per group, with full Markdown rendering.","**Issue tracking and milestones**: kanban board, labels, iterations and burndown charts included.","**Parallel runners**: register as many runners as you want on your infrastructure or CI workers.","**Webhooks** to notify a third-party tool (Slack, PagerDuty, your deployment server) on every push or merge.","**Granular RBAC** with five access levels (Guest, Reporter, Developer, Maintainer, Owner) and optional LDAP\u002FSAML support.",{"type":35,"title":51,"body":52},"Prerequisites: what you need before you start","GitLab CE is memory-intensive — it is one of its most cited drawbacks compared to Forgejo or Gitea. Plan for **4 GB of RAM minimum** for light usage (fewer than ten active users); **8 GB are recommended** as soon as you add runners on the same machine or enable the Docker registry. Below 4 GB, Puma and Sidekiq compete for memory and GitLab responds with 502 under load. For storage, allow at least 20 GB for the installation and initial repositories — plan for 50 GB if you intend to store Docker images in the registry. You also need: a domain name pointing to the VPS IP (for example `git.yourdomain.com`) for Let's Encrypt to issue a TLS certificate; ports 80, 443 and 22 open in your firewall (port 22 is used by GitLab for SSH pushes — if your system SSH daemon also listens on 22, move it to another port); Docker Engine and the Compose v2 plugin installed (`docker compose version` should return `v2.x`).",{"type":54,"title":55,"steps":56},"steps","From installation to your first project",[57,60,63,66,69,72,75,78,81],{"title":58,"body":59},"Create the directory structure","Create a dedicated folder and the three persistent volumes GitLab uses: `mkdir -p \u002Fopt\u002Fgitlab\u002F{config,logs,data}`. These directories will be mounted into the container; without them, configuration and repositories vanish on every `docker compose down`.",{"title":61,"body":62},"Write the docker-compose.yml file","Create `\u002Fopt\u002Fgitlab\u002Fdocker-compose.yml`. The `GITLAB_OMNIBUS_CONFIG` key concentrates all instance-specific configuration — replace `git.yourdomain.com` with your actual domain. Define the gitlab service with image `gitlab\u002Fgitlab-ce:17.2.1-ce.0`, `restart: unless-stopped`, the hostname, environment variables (including `GITLAB_OMNIBUS_CONFIG` containing `external_url`, Let's Encrypt settings and SMTP parameters), ports 80, 443 and 22, volumes for `\u002Fetc\u002Fgitlab`, `\u002Fvar\u002Flog\u002Fgitlab` and `\u002Fvar\u002Fopt\u002Fgitlab`, `shm_size: 256m` and `env_file: .env`. **Important note**: the `external_url` value determines whether GitLab generates `http:\u002F\u002F` or `https:\u002F\u002F` URLs, and whether Let's Encrypt is attempted. It must exactly match a domain resolvable from the Internet — an incorrect value is the leading cause of access errors.",{"title":64,"body":65},"Set GITLAB_OMNIBUS_CONFIG","Inside `GITLAB_OMNIBUS_CONFIG`, define at minimum: `external_url 'https:\u002F\u002Fgit.yourdomain.com'`; `letsencrypt['enable'] = true` with `letsencrypt['contact_emails'] = ['admin@yourdomain.com']`; SMTP parameters — `gitlab_rails['smtp_enable'] = true`, address, port 587, user name, `gitlab_rails['smtp_password'] = ENV['GITLAB_SMTP_PASSWORD']`, authentication `login`, `smtp_enable_starttls_auto = true`, `gitlab_email_from`; and if you enable the registry: `registry_external_url 'https:\u002F\u002Fregistry.yourdomain.com'`. All these lines must be **inside** the `GITLAB_OMNIBUS_CONFIG` block, not as separate environment variables — a key outside the block is silently ignored.",{"title":67,"body":68},"Create the .env file for secrets","Create `\u002Fopt\u002Fgitlab\u002F.env` and restrict its permissions: `touch \u002Fopt\u002Fgitlab\u002F.env && chmod 600 \u002Fopt\u002Fgitlab\u002F.env`. Add your sensitive variables — at minimum `GITLAB_SMTP_PASSWORD=your_smtp_password`. Never put a password in plain text in the Compose file: it will end up versioned or shared. Add `.env` to your `.gitignore` if you version the configuration.",{"title":70,"body":71},"Start GitLab and wait for initialisation","Launch the stack: `docker compose -f \u002Fopt\u002Fgitlab\u002Fdocker-compose.yml up -d`. The first start takes time: GitLab initialises the database, compiles assets and configures Nginx and Puma. **Wait about 5 minutes** before accessing the web interface. Follow progress with `docker compose -f \u002Fopt\u002Fgitlab\u002Fdocker-compose.yml logs -f gitlab` and wait for the `gitlab Reconfigured!` message.",{"title":73,"body":74},"Retrieve the initial root password","On first initialisation, GitLab generates a temporary password for the `root` account. Retrieve it with: `docker exec -it gitlab-gitlab-1 grep 'Password:' \u002Fetc\u002Fgitlab\u002Finitial_root_password`. This file is **automatically deleted 24 hours** after the first start — note the password immediately.",{"title":76,"body":77},"Log in, change the password and disable open registration","Open `https:\u002F\u002Fgit.yourdomain.com` in your browser. Log in with `root` and the password retrieved above. Go to **User Settings → Password** and set a new strong password. Create your first non-root user: **Admin Area → Users → New User**. For internal use, disable public sign-up: **Admin Area → Settings → General → Sign-up restrictions → uncheck 'Sign-up enabled'**.",{"title":79,"body":80},"Register a GitLab Runner","CI\u002FCD pipelines require at least one runner. Install `gitlab-runner` on the VPS or a dedicated machine (download the binary from the official GitLab Runner page). Retrieve the registration token from **Admin Area → Runners** (shared) or from your project's **Settings → CI\u002FCD → Runners** (project runner). Register: `gitlab-runner register --url https:\u002F\u002Fgit.yourdomain.com --registration-token YOUR_TOKEN --executor docker --docker-image alpine:latest`. Once registered, the runner appears green in the interface and your `.gitlab-ci.yml` pipelines can execute.",{"title":82,"body":83},"Verify email delivery","Test SMTP from the GitLab Rails console: `docker exec -it gitlab-gitlab-1 gitlab-rails console` then type `Notify.test_email('your@email.com', 'Test GitLab', 'Hello').deliver_now`. If the email does not arrive, check that `smtp_*` keys are inside `GITLAB_OMNIBUS_CONFIG` and review the logs: `docker exec -it gitlab-gitlab-1 tail -f \u002Fvar\u002Flog\u002Fgitlab\u002Fgitlab-rails\u002Fproduction.log`.",{"type":85,"body":86},"tip","**Automatic backups.** GitLab ships a full backup command (repositories, database, uploads): `docker exec -t gitlab-gitlab-1 gitlab-backup create`. Schedule it in `crontab -e` with `0 3 * * * docker exec -t gitlab-gitlab-1 gitlab-backup create CRON=1`. Archives are created in `\u002Fvar\u002Fopt\u002Fgitlab\u002Fbackups` (mounted at `\u002Fopt\u002Fgitlab\u002Fdata\u002Fbackups` on the host) and timestamped. Sync this folder to external storage (S3, rclone) — a local backup is not a backup.",{"type":35,"title":88,"body":89},"Configure the Docker runner and write your first CI\u002FCD pipeline","The Docker executor is the recommended choice for most projects: each job runs in an isolated container, without polluting the runner environment. After registering the runner (step 8 above), its configuration file `\u002Fetc\u002Fgitlab-runner\u002Fconfig.toml` contains a `[runners.docker]` block. Add `volumes = [\"\u002Fcache\"]` to retain cache between builds, and `pull_policy = [\"if-not-present\"]` to avoid pulling the image on every job if it is already present locally — this noticeably reduces the duration of first stages.\n\nA basic pipeline for a Node.js or PHP project fits in about twenty lines in `.gitlab-ci.yml` at the root of the repository:\n\n```yaml\nstages:\n  - test\n  - build\n  - deploy\n\nvariables:\n  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA\n\ntest:unit:\n  stage: test\n  image: node:20-alpine\n  cache:\n    key: $CI_COMMIT_REF_SLUG\n    paths:\n      - node_modules\u002F\n  script:\n    - npm ci\n    - npm test\n  only:\n    - merge_requests\n    - main\n\nbuild:image:\n  stage: build\n  image: docker:26\n  services:\n    - docker:26-dind\n  before_script:\n    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY\n  script:\n    - docker build -t $DOCKER_IMAGE .\n    - docker push $DOCKER_IMAGE\n  only:\n    - main\n\ndeploy:prod:\n  stage: deploy\n  image: alpine:latest\n  before_script:\n    - apk add --no-cache openssh-client\n    - eval $(ssh-agent -s)\n    - echo \"$SSH_PRIVATE_KEY\" | ssh-add -\n  script:\n    - ssh -o StrictHostKeyChecking=no user@yourserver.com \"docker pull $DOCKER_IMAGE && docker compose up -d\"\n  only:\n    - main\n  when: manual\n```\n\nThe variables `CI_REGISTRY_USER`, `CI_REGISTRY_PASSWORD` and `CI_REGISTRY` are **injected automatically** by GitLab CE for the built-in registry — no manual configuration needed. The `SSH_PRIVATE_KEY` variable must be defined in **Settings → CI\u002FCD → Variables** of the project, marked **Masked** so it does not appear in logs. The `deploy:prod` job is set to `when: manual`: it does not trigger automatically but waits for a click in the interface or via the API — a minimal safety gate before writing to production.\n\nTo check that the runner picked up the job, go to **CI\u002FCD → Jobs**: each job shows its assigned runner, duration and exit code. A job stuck on 'pending' indicates the runner has no available slot (check `concurrent` in `\u002Fetc\u002Fgitlab-runner\u002Fconfig.toml`, default 1) or is offline.",{"type":35,"title":91,"body":92},"Configuring SMTP notifications in detail","GitLab sends emails on every merge request, mention, finished pipeline or security alert. A missing or incorrect SMTP configuration silently cuts these notifications — users only notice after an incident.\n\nThe full block to place in `GITLAB_OMNIBUS_CONFIG` for an authenticated SMTP relay (example with a STARTTLS-compatible server on port 587):\n\n```ruby\ngitlab_rails['smtp_enable'] = true\ngitlab_rails['smtp_address'] = 'smtp.yourdomain.com'\ngitlab_rails['smtp_port'] = 587\ngitlab_rails['smtp_user_name'] = 'gitlab@yourdomain.com'\ngitlab_rails['smtp_password'] = ENV['GITLAB_SMTP_PASSWORD']\ngitlab_rails['smtp_authentication'] = 'login'\ngitlab_rails['smtp_enable_starttls_auto'] = true\ngitlab_rails['gitlab_email_from'] = 'gitlab@yourdomain.com'\ngitlab_rails['gitlab_email_reply_to'] = 'noreply@yourdomain.com'\n```\n\nIf your relay requires direct TLS (port 465), replace `smtp_enable_starttls_auto = true` with `smtp_tls = true` and adjust the port. For relays that require no authentication (internal server, local Postfix), set `smtp_authentication = false` and remove `smtp_user_name` and `smtp_password`.\n\nMain pitfalls: a key defined as a Docker environment variable rather than inside the Omnibus block is silently ignored; a YAML indentation error in docker-compose.yml can cut the block mid-way; always test with `Notify.test_email(…).deliver_now` from the Rails console after each change.",{"type":35,"title":94,"body":95},"Backing up and restoring GitLab","GitLab CE ships a full backup command that archives in a single tar file the git repositories, the PostgreSQL database, uploads, CI artifacts and configuration keys. The reference command:\n\n```bash\ndocker exec -t gitlab-gitlab-1 gitlab-backup create\n```\n\nThe archive is created in `\u002Fvar\u002Fopt\u002Fgitlab\u002Fbackups\u002F` inside the container, i.e. `\u002Fopt\u002Fgitlab\u002Fdata\u002Fbackups\u002F` on the host if you mounted the volume as shown. It is named `\u003Ctimestamp>_\u003Cversion>_gitlab_backup.tar`.\n\n**Important**: `gitlab-backup create` does **not** include two critical files — `\u002Fetc\u002Fgitlab\u002Fgitlab.rb` (the Omnibus configuration) and `\u002Fetc\u002Fgitlab\u002Fgitlab-secrets.json` (the encryption keys). Without these files a restore fails or decrypts tokens incorrectly. Back them up separately:\n\n```bash\ntar czf \u002Fopt\u002Fgitlab\u002Fconfig-secrets-$(date +%Y%m%d).tar.gz \\\n  \u002Fopt\u002Fgitlab\u002Fconfig\u002Fgitlab.rb \\\n  \u002Fopt\u002Fgitlab\u002Fconfig\u002Fgitlab-secrets.json\n```\n\n**Restoring** from a backup requires three steps in order: install GitLab CE of the **same minor version** that produced the backup; copy the tar archive to `\u002Fopt\u002Fgitlab\u002Fdata\u002Fbackups\u002F` and restore `gitlab.rb` and `gitlab-secrets.json`; then stop write services and run the restore:\n\n```bash\ndocker exec -it gitlab-gitlab-1 gitlab-ctl stop puma\ndocker exec -it gitlab-gitlab-1 gitlab-ctl stop sidekiq\ndocker exec -it gitlab-gitlab-1 gitlab-backup restore BACKUP=\u003Ctimestamp>_\u003Cversion>\ndocker exec -it gitlab-gitlab-1 gitlab-ctl restart\ndocker exec -it gitlab-gitlab-1 gitlab-rake gitlab:check SANITIZE=true\n```\n\nThe `gitlab:check` command at the end verifies installation integrity and flags corrupted repositories or incorrect permissions. Schedule the full backup at 3 am and sync to external storage (S3 via rclone, remote NAS) — a local backup that burns with the VPS is not a backup.",{"type":35,"title":97,"body":98},"Troubleshooting: the three most common failures","**502 Bad Gateway at startup.** GitLab takes about 5 minutes to become fully operational. If the 502 persists, check that Puma has started: `docker exec gitlab-gitlab-1 gitlab-ctl status puma`. Insufficient RAM is the most frequent cause — ensure you have at least 4 GB available. **SMTP not working.** Check first: is the `gitlab_rails['smtp_*']` block inside `GITLAB_OMNIBUS_CONFIG`, not defined as a separate environment variable? Incorrect indentation in the YAML or a key outside the omnibus block is silent — GitLab starts normally but ignores the SMTP configuration. Test from the Rails console (step 9). **Runner not connected.** If the runner shows grey or offline, check it can reach your instance: `gitlab-runner verify --url https:\u002F\u002Fgit.yourdomain.com`. A self-signed TLS certificate or a domain not resolvable from the runner machine are the usual causes. Also check that the token used at registration is for the correct level (instance, group or project).",{"type":35,"title":100,"body":101},"Advanced troubleshooting: stuck Sidekiq, Puma timeout and OOM on Gitaly","**Sidekiq queue stuck — jobs blocked indefinitely.** Sidekiq processes GitLab's asynchronous jobs (emails, imports, webhooks, indexing). If jobs accumulate without being consumed, check the worker state first:\n\n```bash\ndocker exec gitlab-gitlab-1 gitlab-ctl status sidekiq\ndocker exec gitlab-gitlab-1 gitlab-rake gitlab:sidekiq:check\n```\n\nA stuck queue often comes from a job that raised an uncaught exception and went into infinite retry. To clear a specific queue without restarting GitLab: `docker exec -it gitlab-gitlab-1 gitlab-rails console -e production` then `Sidekiq::Queue.new('mailers').clear`. If Sidekiq consumes more than 1 GB of RAM, `docker exec gitlab-gitlab-1 gitlab-ctl restart sidekiq` re-initialises it without losing pending jobs (they remain in Redis).\n\n**Puma timeout — intermittent 503 under load.** Puma timeouts (slow database calls, poorly optimised Rails requests) appear in `\u002Fvar\u002Flog\u002Fgitlab\u002Fpuma\u002Fpuma_stderr.log`. The first action is to raise the timeout if the VPS is slow but functional: in `GITLAB_OMNIBUS_CONFIG`, add `puma['worker_timeout'] = 90` (default 60 s). If timeouts persist, the cause is almost always insufficient RAM or an ill-adapted worker count — see the RAM optimisation section.\n\n**OOM on Gitaly — repositories inaccessible after a kill.** Gitaly handles git operations (clone, fetch, push). If the kernel kills it by OOM (check with `dmesg | grep -i kill`), all git access fails until the service restarts. Monitor with `docker exec gitlab-gitlab-1 gitlab-ctl status gitaly` and, on RAM-constrained VPS, limit automatic maintenance: `gitaly['configuration']['git']['catfile_cache_size'] = 5` (default 100) reduces cached git objects. For repeated OOM, the durable solution is more RAM or adding swap.\n\n**Container exited at startup.** If the container exits immediately after `docker compose up`, check pre-exit logs: `docker compose logs gitlab | tail -50`. The most frequent causes are a port 22 conflict (system SSH daemon active), a volume not writable (`chown -R 998:998 \u002Fopt\u002Fgitlab\u002Fdata`) or a `GITLAB_OMNIBUS_CONFIG` with invalid Ruby syntax (missing comma, unclosed quote).",{"type":35,"title":103,"body":104},"Security: CVE-2026-90970 and critical update","On October 2, 2026, GitLab disclosed **CVE-2026-90970**, a critical vulnerability (CVSS 9.9) affecting the **AI Gateway** component of self-hosted deployments. The flaw is a server-side template injection (SSTI) in custom flow prompt templates: an authenticated user with Duo Agent Platform access can submit a crafted flow configuration to escape the sandbox and **execute arbitrary commands on the underlying host**.\n\n**What is affected.** Only self-hosted instances that deploy the AI Gateway component. GitLab.com and Dedicated instances were patched automatically. Exploitation requires Duo Agent Platform access and no in-the-wild exploitation was reported at the time of disclosure.\n\n**Vulnerable and fixed versions.**\n\n| Branch | Last vulnerable version | Fixed version |\n|---|---|---|\n| 19.4.x | 19.4.0 | **19.4.1** |\n| 19.3.x | 19.3.1 | **19.3.2** |\n| 18.1.6 – 19.2.x | 19.2.3 | **19.2.4** |\n\n**Updating the AI Gateway on Docker.**\n\n```bash\n# Stop and remove the existing container\ndocker stop gitlab-ai-gateway && docker rm gitlab-ai-gateway\n\n# Pull the patched image (replace the tag with your branch)\ndocker pull registry.gitlab.com\u002Fgitlab-org\u002Fmodelops\u002Fapigw\u002Fself-hosted-v19.4.1-ee\n# or: self-hosted-v19.3.2-ee \u002F self-hosted-v19.2.4-ee\n\n# Restart with the same environment variables\ndocker run -d --name gitlab-ai-gateway \\\n  --restart unless-stopped \\\n  [your usual options] \\\n  registry.gitlab.com\u002Fgitlab-org\u002Fmodelops\u002Fapigw\u002Fself-hosted-v19.4.1-ee\n\n# Verify the deployed version\ndocker ps --format '{{.Image}}' | grep ai-gateway\n```\n\n**Updating via Helm \u002F Kubernetes.** Update `image.tag` in your Helm values to the patched version, set `imagePullPolicy: Always`, and trigger a `helm upgrade`.\n\nAfter updating, confirm the container is running the patched image: `docker inspect \u003Ccontainer> --format '{{.Config.Image}}'`.",{"type":85,"body":106},"**Disable the AI Gateway if you don't use it.** If your GitLab CE instance does not use Duo features (AI code completion, chat), the AI Gateway can simply be stopped and removed from your `docker-compose.yml`. A component that is not deployed cannot be exploited. Verify that no pipelines depend on the Gateway API before removing it.",{"type":35,"title":108,"body":109},"RAM optimisation: swap and Puma workers","GitLab CE in its default configuration allocates several Puma workers and as many Sidekiq threads as CPU cores detected. On a 4 GB VPS with 2 vCPU, this can exceed available memory as soon as a heavy pipeline runs in parallel. Two immediate levers.\n\n**Reduce Puma workers.** In `GITLAB_OMNIBUS_CONFIG`:\n\n```ruby\npuma['worker_processes'] = 2\npuma['min_threads'] = 1\npuma['max_threads'] = 4\n```\n\nEach Puma worker consumes about 200 to 300 MB. Two workers are sufficient for fewer than fifteen simultaneous active users. A single worker (`worker_processes = 0`) is possible but removes worker fault tolerance.\n\n**Add swap.** GitLab recommends at least as much swap as RAM. On a VPS without preconfigured swap:\n\n```bash\nfallocate -l 4G \u002Fswapfile\nchmod 600 \u002Fswapfile\nmkswap \u002Fswapfile\nswapon \u002Fswapfile\necho '\u002Fswapfile none swap sw 0 0' >> \u002Fetc\u002Ffstab\n```\n\nSwap does not replace RAM — it prevents OOM crashes during momentary load spikes — but it slows GitLab down if the system relies on it permanently. If `free -h` shows regular swap usage at idle, the VPS is undersized.\n\n**RAM and CPU recommendations by team size**\n\n| Team size | Recommended RAM | vCPU | Notes |\n|---|---|---|---|\n| 1–5 developers | 4 GB | 2 | Runners on the same machine possible with swap |\n| 5–15 developers | 8 GB | 4 | Dedicated runner advised beyond 8+ pipelines\u002Fday |\n| 15–30 developers | 16 GB | 8 | Active Docker registry, Gitaly under load |\n| 30+ developers | 32 GB+ | 16+ | Separate Gitaly and Sidekiq on dedicated machines |",{"type":111,"title":112,"headers":113,"rows":117},"comparison","GitLab CE, Forgejo or Gitea: how to choose",[114,115,116],"Criterion","GitLab CE","Forgejo \u002F Gitea",[118,122,126,130,134,136,140,144],[119,120,121],"Licence","MIT \u002F EE Core","MIT",[123,124,125],"Idle RAM","300–600 MB (Puma + Sidekiq)","30–50 MB",[127,128,129],"Native CI\u002FCD","Yes (`.gitlab-ci.yml`, runners)","Forgejo: yes via Woodpecker CI; Gitea: no native CI",[131,132,133],"Integrated Docker registry","Yes","Forgejo: yes; Gitea: not native",[135,132,132],"Per-project wikis",[137,138,139],"LDAP \u002F SAML","Yes (CE)","Forgejo: yes; Gitea: LDAP yes, SAML no",[141,142,143],"Learning curve","High (feature-rich interface)","Low to medium",[145,146,147],"Best for","Teams of 5+ needing CI, registry and RBAC","Lightweight teams, simple forge, low RAM footprint",{"type":35,"title":149,"body":150},"When to choose GitLab CE, when to choose Forgejo","GitLab CE is the right choice if your team needs robust CI\u002FCD directly integrated into the forge, a private Docker registry on your domain, deployment pipelines with environments and deploy tokens, or project management with issues, milestones and kanban without a third-party tool. Its memory footprint (4–8 GB) is the price of this functional richness. Forgejo or Gitea are the answer when RAM is the main constraint, when the forge is the only requirement (no integrated CI) and you run CI with an external tool (Woodpecker, Drone, GitHub Actions via a runner). On a 2 GB VPS, GitLab CE is not viable; Forgejo runs on 256 MB. On a VPS of 8 GB dedicated to a development team, GitLab CE provides a complete environment that GitHub puts on its SaaS platform — under your control and without a monthly per-user subscription.","A VPS ready for GitLab CE","GitLab CE needs a VPS with 4 to 8 GB of RAM, generous disk space and a dedicated IP. Our ServOrbit VPS are delivered with your choice of Ubuntu 24.04 LTS, Ubuntu 22.04 LTS or Debian 12 and immediate root access — enough to have your forge online in thirty minutes.","See ServOrbit VPS","\u002Fsolutions\u002Fdeveloppeurs",[156,176,192],{"id":157,"slug":158,"slugs":159,"title":163,"excerpt":164,"readTime":165,"views":166,"isPinned":19,"noindex":19,"publishedAt":167,"updatedAt":168,"category":169,"categories":173,"featuredImage":29,"bgImage":30,"posterImage":175,"relatedSolution":29},212,"woodpecker-ci-and-forgejo-cicd-pipeline-on-a-vps",{"fr":160,"en":158,"ar":161,"es":162},"woodpecker-ci-pipeline-vps-forgejo","woodpecker-ci-وforgejo-خط-أنابيب-cicd-على-خادم-vps","woodpecker-ci-forgejo-pipeline-cicd-vps","Woodpecker CI and Forgejo: CI\u002FCD pipeline on a VPS","Deploy Woodpecker CI v3 with Forgejo on your VPS: Docker Compose setup, YAML pipelines, secrets, multi-arch runners and OAuth troubleshooting.",12,2,"2026-08-02T00:00:00+00:00","2026-11-08T01:13:13+00:00",{"id":166,"name":170,"slug":171,"color":172,"icon":171},"Automation","automatisation","bg-brand-action\u002F10 text-brand-action",[174],{"id":166,"name":170,"slug":171,"color":172,"icon":171},"\u002Fblog\u002Fcovers\u002Fwoodpecker-ci-pipeline-vps-forgejo-poster.svg",{"id":177,"slug":178,"slugs":179,"title":183,"excerpt":184,"readTime":165,"views":185,"isPinned":19,"noindex":19,"publishedAt":186,"updatedAt":187,"category":188,"categories":189,"featuredImage":29,"bgImage":30,"posterImage":191,"relatedSolution":29},229,"docker-compose-in-production-10-point-checklist",{"fr":180,"en":178,"ar":181,"es":182},"docker-compose-production-checklist","docker-compose-في-الإنتاج-قائمة-التحقق-من-10-نقاط","checklist-docker-compose-en-produccion","Docker Compose in Production: 10-Point Checklist","Docker Compose in production: 10 settings, health checks, secrets, rollback, troubleshooting and moving from docker-compose v1 to docker compose on Ubuntu 26.04.",1,"2026-08-06T00:00:00+00:00","2026-10-23T13:27:09+00:00",{"id":23,"name":24,"slug":25,"color":26,"icon":25},[190],{"id":23,"name":24,"slug":25,"color":26,"icon":25},"\u002Fblog\u002Fcovers\u002Fdocker-compose-production-checklist-poster.svg",{"id":193,"slug":194,"slugs":195,"title":199,"excerpt":200,"readTime":201,"views":166,"isPinned":19,"noindex":19,"publishedAt":202,"updatedAt":203,"category":204,"categories":205,"featuredImage":29,"bgImage":30,"posterImage":207,"relatedSolution":29},207,"k3s-lightweight-kubernetes-on-your-vps",{"fr":196,"en":194,"ar":197,"es":198},"k3s-kubernetes-leger-vps","k3s-كوبيرنيتيس-خفيف-على-vps-الخاص-بك","k3s-kubernetes-ligero-en-un-vps","K3s: lightweight Kubernetes on your VPS","Run a CNCF-certified Kubernetes cluster on a modest VPS with K3s, the Rancher\u002FSUSE distribution that fits in 60 MB.",4,"2026-08-01T00:00:00+00:00","2026-09-07T11:26:10+00:00",{"id":23,"name":24,"slug":25,"color":26,"icon":25},[206],{"id":23,"name":24,"slug":25,"color":26,"icon":25},"\u002Fblog\u002Fcovers\u002Fk3s-kubernetes-leger-vps-poster.svg",true,1794270433943]