Skip to content
  • 10 cloud providers
  • 51 configurations
  • Competitive pricing
  • Developer friendly

CI/CD onto a cloud server without a platform

A reliable deployment pipeline onto a plain server needs four things. None of them require a deployment platform, and all of them are worth having before you have one.

Abstract illustration accompanying this guide on ci/cd to a cloud server

Deployment platforms are convenient and often worth their price. But if you are running an application on a server you control, a pipeline that builds, tests and deploys reliably needs only four things, and building it yourself takes an afternoon.

Doing so is also a good way to understand what the platforms are doing for you before deciding whether to pay for it.

The four requirements

A build that produces an immutable artefact. Something identified by a version that never changes: a container image tagged with the commit hash, or a versioned archive. Deploying from a branch name means you cannot say precisely what is running.

A test gate. Something that fails the pipeline before a broken build reaches a server. Even a smoke test that starts the application and requests one endpoint catches a large share of real breakages.

A deployment mechanism the server can execute. Something that takes the artefact identifier and makes the server run it.

A rollback that is faster than a fix. The previous artefact still exists and can be redeployed in one operation.

Everything else is refinement.

Authentication, done the safe way

The naive approach is to put an SSH private key with full server access into your CI system’s secrets and have the pipeline log in and run commands. It works and it means anyone who can run a pipeline, or modify one, has root on production.

Two better options.

Pull-based deployment. The server polls somewhere for the desired version and applies it. CI publishes the artefact and updates a pointer; it never connects to the server at all, and no inbound access or credential is needed. The trade is a small delay and a slightly less obvious deployment flow.

Push with a narrowly-scoped identity. If CI must connect, give it a dedicated user that can run exactly one deployment script through a restricted sudo rule, and nothing else. Not a shell, not root. Use a deploy key specific to that pipeline so it can be revoked alone.

For cloud-hosted CI connecting to cloud infrastructure, workload identity federation avoids long-lived credentials entirely and is worth the setup where available.

A deployment script that behaves well

Whatever runs on the server should be idempotent, verbose and safe to interrupt.

A sound sequence: pull the new artefact and verify it arrived intact before touching anything running. Start the new version alongside the old. Wait for the new version to report healthy. Switch traffic. Stop the old version after a drain period. Record what was deployed and when.

The crucial property is that failure at any step leaves the previous version serving. A script that stops the old version first and then discovers the new one will not start has created an outage rather than a failed deployment.

Configuration and secrets

Configuration belongs outside the artefact, so the same tested artefact can run in staging and production.

Secrets belong in a secrets manager, or at minimum in a root-owned file on the server with restricted permissions, injected as environment variables at start. Never in the image, never in the repository, never in the pipeline definition.

Rotation should be a routine operation. If rotating a credential requires editing five files across three systems, it will not happen.

Database migrations, the part that breaks

Schema changes are where deployment pipelines most often go wrong, because the code and the schema must both change and they cannot change at the same instant.

The reliable discipline is to make migrations backward compatible, so the previous version of the application still works against the new schema. That is what makes rollback possible at all.

In practice this means splitting destructive changes across releases. To remove a column: first deploy code that no longer reads it, then in a later release drop the column. To rename: add the new one, write to both, migrate, switch reads, then remove the old one in a subsequent release.

Run migrations as a separate step before the application deploy, not as part of application startup, so that several instances starting simultaneously do not race each other.

Knowing whether it worked

A deployment that reports success while the application is broken is worse than a failure, because nobody is looking.

Verify after deploying: request a health endpoint that exercises real dependencies, check the process is actually serving, and watch the error rate for a few minutes afterwards. Automate that check into the pipeline so a bad deployment fails loudly.

Log every deployment with the artefact version and the time somewhere durable. When something breaks at an odd hour, the first useful question is always what changed and when.

A reasonable minimum

If you build only this, you have most of the value:

  1. CI builds a container image tagged with the commit hash and pushes it to a registry.
  2. Tests run and must pass.
  3. A deployment step updates a version pointer or connects with a narrowly-scoped identity.
  4. The server pulls the tagged image, starts it, waits for health, switches traffic, stops the old one.
  5. Rollback is redeploying the previous tag, which is one command.

That is a pipeline. It fits in a single CI configuration file and a fifty-line shell script, and it is enough for a very long time.

Where you run this matters less than how, but if the provider is still open, the cloud account catalogue compares the options side by side.

For the vendor’s own reference on the services involved here, see the Linode documentation on Akamai TechDocs.

Working through this on a real server is worth the hour it takes. We stock Linode accounts in two configurations, and the cloud account catalogue compares all ten providers side by side.

Questions people ask

Should CI have SSH access to my production server?

Preferably not. A pull-based deployment where the server polls for the desired version needs no inbound access at all. If CI must push, give it a dedicated identity restricted to running one deployment script, not a shell and not root.

Why tag images with the commit hash rather than latest?

Because you need to be able to say exactly what is running and to redeploy a specific previous version. A mutable tag makes both impossible.

How should database migrations be handled?

Make them backward compatible so the previous application version still works against the new schema, which is what makes rollback possible. Split destructive changes across releases, and run migrations as a separate step before the application deploy rather than at startup.

What makes a deployment script safe?

Failure at any step must leave the previous version serving. Start the new version alongside the old, wait for it to report healthy, switch traffic, then stop the old one after draining.

Telegram