GitLab CI/CD runs jobs defined in .gitlab-ci.yml on shared or project runners. How pipelines, stages and environments work, and how to deploy an Encore backend from a GitLab job using the platform API or encore build docker.
GitLab CI/CD is the pipeline system built into GitLab: you describe jobs in YAML, runners execute them, and the same product that hosts the repository shows the pipeline next to the merge request. For a backend, that usually means install dependencies, run tests, build an artifact and deploy to staging or production.
GitLab stays the CI product, and Encore is infrastructure from code whose platform builds, provisions and deploys the backend when a rollout starts. This guide covers GitLab pipelines and runners first, then two ways to ship an Encore app from a GitLab job.
The GitLab pipelines documentation and runners documentation cover the full model.
.gitlab-ci.ymlA pipeline is created on push, merge request, schedule or manual run. It is made of stages that run in order; if any required job in a stage fails, later stages are skipped. A minimal backend pipeline looks like this:
# .gitlab-ci.yml
stages:
- test
- deploy
variables:
NODE_VERSION: "22"
test:
stage: test
image: node:${NODE_VERSION}
script:
- npm ci
- npm test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy_staging:
stage: deploy
image: curlimages/curl:8.10.1
script:
- echo "Replace with your deploy command"
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: on_success
Jobs can depend on earlier jobs with needs to start before the rest of a stage finishes, pass artifacts forward, and use rules to run only on the default branch or on merge requests. Manual jobs (when: manual) gate production. Environments record what was deployed where and link each deployment to the pipeline that produced it.
Keep secrets out of the YAML. Store tokens as CI/CD variables, mark them masked and protected, and restrict protected variables to protected branches.
Runners are the workers. On GitLab.com, GitLab-hosted runners are instance runners: no agent to install, a clean VM per job, Linux, Windows and macOS options. They fit most open-source and SaaS workflows.
Self-managed runners run on your hardware or cloud VMs. You register them to the instance, a group or a single project. Project and group runners are the usual meaning of "specific" runners: dedicated capacity, access to a private network, GPUs or a hardened image. Tag jobs so only the right runner picks them up, and use protected runners for jobs that need production credentials.
Choose hosted runners for zero maintenance and isolation per job. Choose self-managed runners when jobs must reach internal services, use custom executors or keep build caches on warm machines.
Two settings do most of the work. A cache keyed on the lockfile keeps node_modules or the npm cache between pipelines, and needs lets a job start as soon as the jobs it depends on finish instead of waiting for the whole previous stage:
stages:
- test
- build
test:
stage: test
image: node:22
cache:
key:
files:
- package-lock.json
paths:
- .npm/
script:
- npm ci --cache .npm --prefer-offline
- npm test
build:
stage: build
needs: ["test"]
script:
- npm run build
artifacts:
paths:
- dist/
Caches are best effort and can be missing on a fresh runner, so jobs must still work without one. Artifacts are the reliable way to pass build output to later jobs, and needs downloads artifacts only from the jobs it lists. Merge request pipelines that run with rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" keep the default branch pipeline for deploys and the merge request pipeline for tests.
A deploy job is an ordinary job in a later stage. Patterns that show up often:
Whatever you pick, prefer OIDC or short-lived tokens over long-lived keys in variables, run production deploys from protected branches, and keep staging automatic while production waits on a manual job if the team wants a human gate.
A job can start sidecar containers with services. The service is reachable by its image name as the hostname, or by an alias, and the variables you set on the job are passed to the service container as well:
integration_test:
stage: test
image: node:22
services:
- name: postgres:16
alias: db
variables:
POSTGRES_DB: app_test
POSTGRES_USER: app
POSTGRES_PASSWORD: app
DATABASE_URL: "postgres://app:app@db:5432/app_test"
script:
- npm ci
- npm run migrate
- npm run test:integration
The service container starts fresh for every job, so tests should create their schema through migrations rather than rely on state from an earlier run. Use a test-only password like this one and keep real credentials in masked variables.
GitLab issues an OpenID Connect token to a job through id_tokens. Register GitLab as an OIDC identity provider in IAM, create a role whose trust policy matches the token's aud and the sub claim for your project and branch, and exchange the token for temporary credentials in the job:
deploy_production:
stage: deploy
image:
name: amazon/aws-cli:latest
entrypoint: [""]
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
script:
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn "${AWS_ROLE_ARN}"
--role-session-name "gitlab-${CI_PIPELINE_ID}"
--web-identity-token "${GITLAB_OIDC_TOKEN}"
--duration-seconds 3600
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
--output text))
- aws sts get-caller-identity
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
The sub claim looks like project_path:mygroup/myapp:ref_type:branch:ref:main, so a trust policy condition on it limits the role to one project and branch. The credentials expire on their own, and there is no access key to rotate or leak from a variable. The same pattern works for GCP with Workload Identity Federation.
Encore combines an open source infrastructure SDK for TypeScript and Go with a platform that provisions and deploys into your own AWS or GCP account. Native git push deploy integrates with GitHub. Teams that keep the repository on GitLab still run tests in GitLab CI, then start an Encore rollout from a job or build a self-hosted image with the CLI.
The platform API accepts a POST to /api/apps/{APP_ID}/envs/{ENV_NAME}/rollouts with a JSON body that names a branch, a commit sha, or latest_deploy. Authenticate with a Bearer token from the OAuth token endpoint, using an OAuth client created for the app. Store client_id and client_secret as protected CI variables.
# .gitlab-ci.yml (deploy job fragment)
deploy_encore_staging:
stage: deploy
image: curlimages/curl:8.10.1
variables:
APP_ID: "your-app-id"
ENV_NAME: "staging"
script:
- |
TOKEN=$(curl -sS -X POST "https://api.encore.dev/api/oauth/token" \
-d "client_id=${ENCORE_CLIENT_ID}" \
-d "client_secret=${ENCORE_CLIENT_SECRET}" \
-d "grant_type=client_credentials" \
| sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
curl -sS -X POST \
"https://api.encore.dev/api/apps/${APP_ID}/envs/${ENV_NAME}/rollouts" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"sha\":\"${CI_COMMIT_SHA}\"}"
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
The rollout runs Encore's build and test phase, provisions infrastructure changes when needed, and deploys, the same sequence as a git push deploy described in the deploying docs. Monitor progress in the Cloud Dashboard under Deployments. Use a separate job with when: manual for production, and a different ENV_NAME.
If you self-host instead of using platform-managed deploys, follow the CI/CD integration guide: install the Encore CLI in the job, authenticate with an auth key from App Settings > Auth Keys when the app is linked to the platform, run encore build docker, and push the image to your registry.
build_image:
stage: deploy
image: docker:27
services: [docker:27-dind]
script:
- apk add --no-cache bash curl && curl -L https://encore.dev/install.sh | bash
- ~/.encore/bin/encore auth login --auth-key="${ENCORE_AUTH_KEY}"
- ~/.encore/bin/encore build docker --config infra.config.json registry.example.com/myapp:${CI_COMMIT_SHA}
- docker push registry.example.com/myapp:${CI_COMMIT_SHA}
The --config file describes the infrastructure the image connects to; keep secret values out of it.
Run in your terminal to get started locally.
The service declares its database and secret next to the endpoint, with a billing/encore.service.ts file that exports new Service("billing") and a migration that creates the invoices table:
// billing/invoice.ts
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { secret } from "encore.dev/config";
const db = new SQLDatabase("billing", { migrations: "./migrations" });
// Set with: encore secret set --type prod StripeKey
const stripeKey = secret("StripeKey");
export const createInvoice = api(
{ expose: true, method: "POST", path: "/invoices" },
async ({ customerId }: { customerId: string }): Promise<{ id: string }> => {
void stripeKey();
const row = await db.queryRow<{ id: string }>`
INSERT INTO invoices (customer_id) VALUES (${customerId})
RETURNING id::text AS id`;
return { id: row!.id };
},
);
encore run starts Postgres locally for the same schema. Secret values are set with encore secret set --type prod StripeKey and injected at runtime, so they never sit in the repository or in GitLab variables. On a platform deploy to AWS, Encore provisions the database, an Application Load Balancer for public APIs and the secret in AWS Secrets Manager, and Encore provisions an IAM role for the service. GitLab still runs your tests, and the deploy job only needs to start the rollout or build the image.
Best fit: Teams on GitLab who want CI next to the merge request and Encore to own infrastructure and deploy into their AWS or GCP account, or to produce the Docker image for self-hosting.
encore build docker.encore.app build hooks for compile steps that belong in Encore's build, and leave lint or policy checks in GitLab jobs.GitLab CI/CD is the built-in continuous integration and delivery system in GitLab. You define jobs in a .gitlab-ci.yml file, runners execute those jobs, and pipelines group them into stages such as build, test and deploy.
GitLab-hosted (instance) runners are managed by GitLab and available across projects on GitLab.com. Self-managed runners you install yourself can be instance-wide, limited to a group, or dedicated to one project when you need private networks, custom images or isolated credentials.
In a .gitlab-ci.yml file at the root of the repository by default. Jobs list the commands to run; stages define the order. Variables and protected secrets supply credentials that must not live in the file.
After tests pass, a deploy job can call the Encore platform API to start a rollout for a branch or commit, using an OAuth client stored as CI variables. For self-hosted deploys, install the Encore CLI in the job, authenticate with an auth key, run encore build docker and push the image to your registry.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.