The problem you've seen (and we've seen dozens of times)
Your team pushes a new feature and the site crashes. Or you spend days manually configuring servers, forgetting a permission. Or tests only run when the 'senior' dev is around, and when they're not, you deploy blind. If any of this sounds familiar, your development cycle is the bottleneck. We at Meteora Web have been there. Since 2017 we've helped businesses move from chaotic manual releases to automated pipelines that cut errors and downtime. This guide covers everything you need to build a solid DevOps culture and working CI/CD pipelines – from dev environment to production server.
DevOps is not a tool, it's a culture (and a way to stop fighting)
Too often we see teams buy Jenkins, install GitLab, then keep working the same way: devs write code, throw it over the wall to ops, and when something breaks blame flies. DevOps removes that wall. It doesn't mean everyone does everything, but dev and ops share goals, metrics and processes. Infrastructure is code, deployments are repeatable, tests are automated.
Where to start practically
Before touching any tool, answer three questions:
- How long does it take from commit to production deploy?
- How many manual steps are in between?
- How often does a deploy break something that worked?
If the first answer is "days" and the third is "often", you have your first goal: reduce time and increase confidence. You don't need everything at once. Start with one repository, one staging environment, a pipeline that runs tests and deploys to staging automatically. Then expand.
GitHub Actions: the integrated CI/CD we use almost always
For new projects, GitHub Actions is the quickest choice. It's integrated with your repo, no server management, clean YAML syntax. We at Meteora Web use it for custom Laravel, Vue and WordPress projects. Here's a concrete example: workflow that runs tests, builds, and deploys to a Linux server via SSH.
name: Deploy to production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, pdo, bcmath
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run tests
run: php artisan test
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /var/www/project
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize
Secrets matter: never write host, user or keys in the YAML file. Use GitHub secrets. We often see projects with credentials in plain text – that's the first mistake we fix.
Sponsored Protocol
Smart triggers
Not everything needs to go to production on every push. Use filters: branches, paths, tags. For example, run the pipeline only if you touch files in src/ or tests/, not when you edit documentation.
CI pipeline: automated linting, testing and building
CI (Continuous Integration) is the heart of the process. Every time a dev pushes, the platform automatically runs linting (ESLint, PHP_CodeSniffer), unit tests, static analysis, and build. If any step fails, the pipeline blocks and the team is notified. No broken code reaches deployment.
Practical steps for a solid CI
- Lint: enforce shared rules and block the pipeline if they fail.
- Unit tests: minimum coverage (e.g. 70%), but meaningful tests beat inflated percentages.
- Build: compile assets (npm run build), verify the package is deployable.
- Security scan: tools like Dependabot or Snyk for dependency vulnerabilities. We always integrate them.
A tip: don't run the full test suite on every push to every branch. For feature branches, run only fast unit tests. The full suite goes on main branches before merge.
Sponsored Protocol
CD: automated deploy to staging and production
Continuous Delivery (CD) is the natural next step: after CI passes, code is automatically deployed to staging, and with a click (or automatically after approval) to production. The goal is to make deployment a boring, repeatable operation, not a stressful event.
Staging first
Every production deploy should pass through staging first. Staging must be identical to production in terms of infrastructure, data (anonymized) and configurations. We use the same Ansible playbook for both, changing only the target host.
GitLab CI/CD: the self-hosted alternative with full control
If for compliance or sensitive data you cannot rely on GitHub Actions, GitLab CI/CD with self-hosted runners is the choice. We at Meteora Web chose GitLab for several projects handling health data in Southern Italy: full control, no minute limits, private repos without hidden costs.
Quick comparison
| Feature | GitHub Actions | GitLab CI/CD |
|---|---|---|
| Hosted free runners | 2000 min/month (with public repo) | 400 min/month |
| Self-hosted runner | Yes | Yes, native |
| Container registry | GHCR | GitLab Container Registry |
| Auto DevOps | No | Yes (predefined templates) |
GitLab CI/CD is configured with a .gitlab-ci.yml file in the root. Here's an example that deploys to a server using Docker after testing and building:
stages:
- test
- build
- deploy
variables:
DOCKER_TLS_CERTDIR: "/certs"
test:
stage: test
image: php:8.2-cli
script:
- composer install
- php artisan test
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
deploy:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- ssh server.com "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA && docker stack deploy ..."
Note: secrets (protected variables) are set in the CI/CD Settings of the project.
Sponsored Protocol
Infrastructure as Code with Terraform: stop configuring manually
If you still SSH into servers to install packages or tweak Nginx, you're wasting time and compromising security. Infrastructure as Code (IaC) means describing servers, networks, databases and DNS in versioned files. Terraform is the most popular tool: you declare the desired state and it applies it.
Providers, state and modules
Each provider (AWS, DigitalOcean, Cloudflare) has documentation. The state file (terraform.tfstate) tracks real resources – keep it in a remote backend (S3, GitLab). Reusable modules avoid rewriting the same code for each project.
provider "digitalocean" {
token = var.do_token
}
resource "digitalocean_droplet" "web" {
image = "ubuntu-22-04-x64"
name = "web-prod-01"
region = "fra1"
size = "s-2vcpu-4gb"
ssh_keys = [var.ssh_key_id]
}
output "ip" {
value = digitalocean_droplet.web.ipv4_address
}
We use Terraform for every new project: provisioning servers, databases, load balancers and domains in a single command. You never forget a firewall or a DNS record.
Ansible: server configuration and deploy without surprises
Ansible complements Terraform: while Terraform creates infrastructure, Ansible configures it (installs PHP, Nginx, sets up vhost, deploys the application). It's agentless: just SSH and Python on the server.
---
- name: Deploy Laravel application
hosts: webservers
become: yes
tasks:
- name: Ensure git is installed
apt:
name: git
state: present
- name: Clone repository
git:
repo: 'https://github.com/user/project.git'
dest: /var/www/project
version: main
- name: Install composer dependencies
composer:
command: install
working_dir: /var/www/project
no_dev: yes
- name: Set permissions
file:
path: /var/www/project/storage
state: directory
mode: '0775'
owner: www-data
Combine Terraform + Ansible in a CI/CD pipeline: after terraform apply, run an Ansible playbook to configure and deploy. Full automation.
Sponsored Protocol
Feature flags: continuous deployment without breaking production
Even with perfect pipelines, some features need gradual rollout. Feature flags (toggles) let you deploy code turned off and activate it later without a new deploy. Tools like LaunchDarkly or a simple database flags managed by the app. In Laravel projects we use a FeatureFlag model with cache: just an admin panel to enable a new function for a percentage of users.
Concrete benefit
Deploy the code on Monday, test in production on a small group. If it works, increase the percentage. If something breaks, disable the flag in seconds – no rollback. Reduces risk and enables more frequent releases.
Blue-Green and Canary deployment: real zero downtime
Deploying without stopping the service is possible with two complementary strategies.
Blue-Green
Maintain two identical environments (blue and green). Traffic points to blue. Deploy the new version on green, test, then switch the load balancer to green. If something goes wrong, switch back to blue. Zero downtime, instant rollback. Cost: double the servers.
Canary
Instead of a full switch, route a small percentage of traffic to the new version (e.g., 5% of users). Monitor errors and latency. If stable, increase gradually to 100%. Tools: Kubernetes (with service mesh like Istio) or configurable load balancers. For smaller projects, a simple Nginx proxy with weighted upstream can simulate canary.
Monitoring and alerting: if you don't measure, you can't improve
CI/CD pipelines and automated infrastructure are useless if you don't know whether production is healthy. Monitoring is not optional – it's the feedback loop that closes the DevOps cycle.
Sponsored Protocol
Prometheus + Grafana
Prometheus collects metrics (CPU, memory, request rate, error rate). Grafana visualizes them in dashboards. You can also export custom metrics from your application (e.g., number of processed orders, average response time).
SLO/SLA
Service Level Objective (SLO): the threshold you commit to (e.g., uptime 99.9%, latency under 200ms). Service Level Agreement (SLA) is the contract with the customer. Set alerts when you exceed the SLO burn rate, not after it's already broken.
# example alert rule in Prometheus
groups:
- name: example
rules:
- alert: HighErrorRate
expr: job:request_errors:rate1m{job="api"} > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate above 5% for 5 minutes"
We always include a basic Prometheus + Grafana setup in projects that require guaranteed uptime. The cost of neglected monitoring is much higher than a few hours of setup.
In summary – what to do now
- Audit your current process. Time the manual steps. Pick a pilot project (not the most critical one).
- Set up a CI pipeline. GitHub Actions or GitLab CI: start with lint and tests on every push to the main branch.
- Add automated staging. Deploy to staging automatically after CI passes.
- Automate infrastructure. Terraform for provisioning, Ansible for configuration. Version everything.
- Introduce feature flags. Even simple: a configuration file in the repository.
- Choose a deployment strategy. Blue-green if you have budget, canary if you have variable traffic.
- Monitor. At least uptime and error rate. Then application metrics.
Each step reduces risk and increases release frequency. You don't need to do everything at once: start with one piece, measure improvement, continue. We've been guiding clients through this journey for 8 years. If you want to talk, we're based in Sciacca but work everywhere. The digital divide is closed one working pipeline at a time – not with slides.