Three deployment approaches I've tried

The classic journey: from "scp and pray" to "push to main and forget". Here's what hurts in each.

Option 1. Manual: copying code to the host

That's how I started, and it's fine for the very first launch.

# on your machine
rsync -avz --exclude node_modules ./dist/ deploy@server:/var/www/site/
ssh deploy@server
cd /var/www/site && docker compose up -d

Pros: nothing to set up. Cons, which showed up fast: copied the wrong thing / forgot an exclude / old files linger on the host, and no rollback at all. OK for static pages, no-go for anything alive.

Option 2. Manual with image pulls

Next stage: code is built into an image (locally or in CI), I only pull on the host.

docker login registry.example.com
docker compose pull
docker compose up -d
docker image prune -f

Pros: the host always gets a version built and checked by CI, configs and secrets stay next to it. Cons: manual work remains, which means a forgotten docker compose pull and "why is the site still the old version?". By the way, I still use this one for manual tweaks between releases.

Option 3. Automatic via GitLab CI

Now all my projects live like this: push to main → lint, tests, build, deploy.

The general pattern I apply:

stages: [QualityOfCode, Test, Build, Deploy]

Build:build:
  script:
    - pnpm run build                 # or docker build + push to registry

Deploy:site:
  stage: Deploy
  script:
    # release dir + current symlink — rollback in seconds
    - rsync -a dist/ "$DEPLOY_DIR/releases/$CI_COMMIT_SHORT_SHA/"
    - ln -sfn "$DEPLOY_DIR/releases/$CI_COMMIT_SHORT_SHA" "$DEPLOY_DIR/current"
    - docker compose up -d
    - curl -fsS https://example.com | grep -q "ok"   # smoke test is mandatory

Two delivery approaches, I use both:

Main lessons from the automatic path:

  1. A smoke test after deploy is not decoration. Twice it returned me to a working version instead of a broken one.
  2. Releases are directories + a current symlink. Rollback = flip the link.
  3. Secrets live on the host (shared/.env), not in GitLab variables, if runners are shared.

The manual options never went away: manual image pulls are my fallback when CI is down but a fix is needed right now.